##// END OF EJS Templates
py3: keep "keypressed" a native str in crecord...
Denis Laxalde -
r43795:7cc91339 stable
parent child Browse files
Show More
@@ -1,2043 +1,2041 b''
1 1 # stuff related specifically to patch manipulation / parsing
2 2 #
3 3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # This code is based on the Mark Edgington's crecord extension.
9 9 # (Itself based on Bryan O'Sullivan's record extension.)
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import locale
14 14 import os
15 15 import re
16 16 import signal
17 17
18 18 from .i18n import _
19 19 from .pycompat import (
20 20 getattr,
21 21 open,
22 22 )
23 23 from . import (
24 24 encoding,
25 25 error,
26 26 patch as patchmod,
27 pycompat,
28 27 scmutil,
29 28 util,
30 29 )
31 30 from .utils import stringutil
32 31
33 32 stringio = util.stringio
34 33
35 34 # patch comments based on the git one
36 35 diffhelptext = _(
37 36 """# To remove '-' lines, make them ' ' lines (context).
38 37 # To remove '+' lines, delete them.
39 38 # Lines starting with # will be removed from the patch.
40 39 """
41 40 )
42 41
43 42 hunkhelptext = _(
44 43 """#
45 44 # If the patch applies cleanly, the edited hunk will immediately be
46 45 # added to the record list. If it does not apply cleanly, a rejects file
47 46 # will be generated. You can use that when you try again. If all lines
48 47 # of the hunk are removed, then the edit is aborted and the hunk is left
49 48 # unchanged.
50 49 """
51 50 )
52 51
53 52 patchhelptext = _(
54 53 """#
55 54 # If the patch applies cleanly, the edited patch will immediately
56 55 # be finalised. If it does not apply cleanly, rejects files will be
57 56 # generated. You can use those when you try again.
58 57 """
59 58 )
60 59
61 60 try:
62 61 import curses
63 62
64 63 curses.error
65 64 except ImportError:
66 65 # I have no idea if wcurses works with crecord...
67 66 try:
68 67 import wcurses as curses
69 68
70 69 curses.error
71 70 except ImportError:
72 71 # wcurses is not shipped on Windows by default, or python is not
73 72 # compiled with curses
74 73 curses = False
75 74
76 75
77 76 class fallbackerror(error.Abort):
78 77 """Error that indicates the client should try to fallback to text mode."""
79 78
80 79 # Inherits from error.Abort so that existing behavior is preserved if the
81 80 # calling code does not know how to fallback.
82 81
83 82
84 83 def checkcurses(ui):
85 84 """Return True if the user wants to use curses
86 85
87 86 This method returns True if curses is found (and that python is built with
88 87 it) and that the user has the correct flag for the ui.
89 88 """
90 89 return curses and ui.interface(b"chunkselector") == b"curses"
91 90
92 91
93 92 class patchnode(object):
94 93 """abstract class for patch graph nodes
95 94 (i.e. patchroot, header, hunk, hunkline)
96 95 """
97 96
98 97 def firstchild(self):
99 98 raise NotImplementedError(b"method must be implemented by subclass")
100 99
101 100 def lastchild(self):
102 101 raise NotImplementedError(b"method must be implemented by subclass")
103 102
104 103 def allchildren(self):
105 104 b"Return a list of all of the direct children of this node"
106 105 raise NotImplementedError(b"method must be implemented by subclass")
107 106
108 107 def nextsibling(self):
109 108 """
110 109 Return the closest next item of the same type where there are no items
111 110 of different types between the current item and this closest item.
112 111 If no such item exists, return None.
113 112 """
114 113 raise NotImplementedError(b"method must be implemented by subclass")
115 114
116 115 def prevsibling(self):
117 116 """
118 117 Return the closest previous item of the same type where there are no
119 118 items of different types between the current item and this closest item.
120 119 If no such item exists, return None.
121 120 """
122 121 raise NotImplementedError(b"method must be implemented by subclass")
123 122
124 123 def parentitem(self):
125 124 raise NotImplementedError(b"method must be implemented by subclass")
126 125
127 126 def nextitem(self, skipfolded=True):
128 127 """
129 128 Try to return the next item closest to this item, regardless of item's
130 129 type (header, hunk, or hunkline).
131 130
132 131 If skipfolded == True, and the current item is folded, then the child
133 132 items that are hidden due to folding will be skipped when determining
134 133 the next item.
135 134
136 135 If it is not possible to get the next item, return None.
137 136 """
138 137 try:
139 138 itemfolded = self.folded
140 139 except AttributeError:
141 140 itemfolded = False
142 141 if skipfolded and itemfolded:
143 142 nextitem = self.nextsibling()
144 143 if nextitem is None:
145 144 try:
146 145 nextitem = self.parentitem().nextsibling()
147 146 except AttributeError:
148 147 nextitem = None
149 148 return nextitem
150 149 else:
151 150 # try child
152 151 item = self.firstchild()
153 152 if item is not None:
154 153 return item
155 154
156 155 # else try next sibling
157 156 item = self.nextsibling()
158 157 if item is not None:
159 158 return item
160 159
161 160 try:
162 161 # else try parent's next sibling
163 162 item = self.parentitem().nextsibling()
164 163 if item is not None:
165 164 return item
166 165
167 166 # else return grandparent's next sibling (or None)
168 167 return self.parentitem().parentitem().nextsibling()
169 168
170 169 except AttributeError: # parent and/or grandparent was None
171 170 return None
172 171
173 172 def previtem(self):
174 173 """
175 174 Try to return the previous item closest to this item, regardless of
176 175 item's type (header, hunk, or hunkline).
177 176
178 177 If it is not possible to get the previous item, return None.
179 178 """
180 179 # try previous sibling's last child's last child,
181 180 # else try previous sibling's last child, else try previous sibling
182 181 prevsibling = self.prevsibling()
183 182 if prevsibling is not None:
184 183 prevsiblinglastchild = prevsibling.lastchild()
185 184 if (prevsiblinglastchild is not None) and not prevsibling.folded:
186 185 prevsiblinglclc = prevsiblinglastchild.lastchild()
187 186 if (
188 187 prevsiblinglclc is not None
189 188 ) and not prevsiblinglastchild.folded:
190 189 return prevsiblinglclc
191 190 else:
192 191 return prevsiblinglastchild
193 192 else:
194 193 return prevsibling
195 194
196 195 # try parent (or None)
197 196 return self.parentitem()
198 197
199 198
200 199 class patch(patchnode, list): # todo: rename patchroot
201 200 """
202 201 list of header objects representing the patch.
203 202 """
204 203
205 204 def __init__(self, headerlist):
206 205 self.extend(headerlist)
207 206 # add parent patch object reference to each header
208 207 for header in self:
209 208 header.patch = self
210 209
211 210
212 211 class uiheader(patchnode):
213 212 """patch header
214 213
215 214 xxx shouldn't we move this to mercurial/patch.py ?
216 215 """
217 216
218 217 def __init__(self, header):
219 218 self.nonuiheader = header
220 219 # flag to indicate whether to apply this chunk
221 220 self.applied = True
222 221 # flag which only affects the status display indicating if a node's
223 222 # children are partially applied (i.e. some applied, some not).
224 223 self.partial = False
225 224
226 225 # flag to indicate whether to display as folded/unfolded to user
227 226 self.folded = True
228 227
229 228 # list of all headers in patch
230 229 self.patch = None
231 230
232 231 # flag is False if this header was ever unfolded from initial state
233 232 self.neverunfolded = True
234 233 self.hunks = [uihunk(h, self) for h in self.hunks]
235 234
236 235 def prettystr(self):
237 236 x = stringio()
238 237 self.pretty(x)
239 238 return x.getvalue()
240 239
241 240 def nextsibling(self):
242 241 numheadersinpatch = len(self.patch)
243 242 indexofthisheader = self.patch.index(self)
244 243
245 244 if indexofthisheader < numheadersinpatch - 1:
246 245 nextheader = self.patch[indexofthisheader + 1]
247 246 return nextheader
248 247 else:
249 248 return None
250 249
251 250 def prevsibling(self):
252 251 indexofthisheader = self.patch.index(self)
253 252 if indexofthisheader > 0:
254 253 previousheader = self.patch[indexofthisheader - 1]
255 254 return previousheader
256 255 else:
257 256 return None
258 257
259 258 def parentitem(self):
260 259 """
261 260 there is no 'real' parent item of a header that can be selected,
262 261 so return None.
263 262 """
264 263 return None
265 264
266 265 def firstchild(self):
267 266 b"return the first child of this item, if one exists. otherwise None."
268 267 if len(self.hunks) > 0:
269 268 return self.hunks[0]
270 269 else:
271 270 return None
272 271
273 272 def lastchild(self):
274 273 b"return the last child of this item, if one exists. otherwise None."
275 274 if len(self.hunks) > 0:
276 275 return self.hunks[-1]
277 276 else:
278 277 return None
279 278
280 279 def allchildren(self):
281 280 b"return a list of all of the direct children of this node"
282 281 return self.hunks
283 282
284 283 def __getattr__(self, name):
285 284 return getattr(self.nonuiheader, name)
286 285
287 286
288 287 class uihunkline(patchnode):
289 288 b"represents a changed line in a hunk"
290 289
291 290 def __init__(self, linetext, hunk):
292 291 self.linetext = linetext
293 292 self.applied = True
294 293 # the parent hunk to which this line belongs
295 294 self.hunk = hunk
296 295 # folding lines currently is not used/needed, but this flag is needed
297 296 # in the previtem method.
298 297 self.folded = False
299 298
300 299 def prettystr(self):
301 300 return self.linetext
302 301
303 302 def nextsibling(self):
304 303 numlinesinhunk = len(self.hunk.changedlines)
305 304 indexofthisline = self.hunk.changedlines.index(self)
306 305
307 306 if indexofthisline < numlinesinhunk - 1:
308 307 nextline = self.hunk.changedlines[indexofthisline + 1]
309 308 return nextline
310 309 else:
311 310 return None
312 311
313 312 def prevsibling(self):
314 313 indexofthisline = self.hunk.changedlines.index(self)
315 314 if indexofthisline > 0:
316 315 previousline = self.hunk.changedlines[indexofthisline - 1]
317 316 return previousline
318 317 else:
319 318 return None
320 319
321 320 def parentitem(self):
322 321 b"return the parent to the current item"
323 322 return self.hunk
324 323
325 324 def firstchild(self):
326 325 b"return the first child of this item, if one exists. otherwise None."
327 326 # hunk-lines don't have children
328 327 return None
329 328
330 329 def lastchild(self):
331 330 b"return the last child of this item, if one exists. otherwise None."
332 331 # hunk-lines don't have children
333 332 return None
334 333
335 334
336 335 class uihunk(patchnode):
337 336 """ui patch hunk, wraps a hunk and keep track of ui behavior """
338 337
339 338 maxcontext = 3
340 339
341 340 def __init__(self, hunk, header):
342 341 self._hunk = hunk
343 342 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
344 343 self.header = header
345 344 # used at end for detecting how many removed lines were un-applied
346 345 self.originalremoved = self.removed
347 346
348 347 # flag to indicate whether to display as folded/unfolded to user
349 348 self.folded = True
350 349 # flag to indicate whether to apply this chunk
351 350 self.applied = True
352 351 # flag which only affects the status display indicating if a node's
353 352 # children are partially applied (i.e. some applied, some not).
354 353 self.partial = False
355 354
356 355 def nextsibling(self):
357 356 numhunksinheader = len(self.header.hunks)
358 357 indexofthishunk = self.header.hunks.index(self)
359 358
360 359 if indexofthishunk < numhunksinheader - 1:
361 360 nexthunk = self.header.hunks[indexofthishunk + 1]
362 361 return nexthunk
363 362 else:
364 363 return None
365 364
366 365 def prevsibling(self):
367 366 indexofthishunk = self.header.hunks.index(self)
368 367 if indexofthishunk > 0:
369 368 previoushunk = self.header.hunks[indexofthishunk - 1]
370 369 return previoushunk
371 370 else:
372 371 return None
373 372
374 373 def parentitem(self):
375 374 b"return the parent to the current item"
376 375 return self.header
377 376
378 377 def firstchild(self):
379 378 b"return the first child of this item, if one exists. otherwise None."
380 379 if len(self.changedlines) > 0:
381 380 return self.changedlines[0]
382 381 else:
383 382 return None
384 383
385 384 def lastchild(self):
386 385 b"return the last child of this item, if one exists. otherwise None."
387 386 if len(self.changedlines) > 0:
388 387 return self.changedlines[-1]
389 388 else:
390 389 return None
391 390
392 391 def allchildren(self):
393 392 b"return a list of all of the direct children of this node"
394 393 return self.changedlines
395 394
396 395 def countchanges(self):
397 396 """changedlines -> (n+,n-)"""
398 397 add = len(
399 398 [
400 399 l
401 400 for l in self.changedlines
402 401 if l.applied and l.prettystr().startswith(b'+')
403 402 ]
404 403 )
405 404 rem = len(
406 405 [
407 406 l
408 407 for l in self.changedlines
409 408 if l.applied and l.prettystr().startswith(b'-')
410 409 ]
411 410 )
412 411 return add, rem
413 412
414 413 def getfromtoline(self):
415 414 # calculate the number of removed lines converted to context lines
416 415 removedconvertedtocontext = self.originalremoved - self.removed
417 416
418 417 contextlen = (
419 418 len(self.before) + len(self.after) + removedconvertedtocontext
420 419 )
421 420 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
422 421 contextlen -= 1
423 422 fromlen = contextlen + self.removed
424 423 tolen = contextlen + self.added
425 424
426 425 # diffutils manual, section "2.2.2.2 detailed description of unified
427 426 # format": "an empty hunk is considered to end at the line that
428 427 # precedes the hunk."
429 428 #
430 429 # so, if either of hunks is empty, decrease its line start. --immerrr
431 430 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
432 431 fromline, toline = self.fromline, self.toline
433 432 if fromline != 0:
434 433 if fromlen == 0:
435 434 fromline -= 1
436 435 if tolen == 0 and toline > 0:
437 436 toline -= 1
438 437
439 438 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
440 439 fromline,
441 440 fromlen,
442 441 toline,
443 442 tolen,
444 443 self.proc and (b' ' + self.proc),
445 444 )
446 445 return fromtoline
447 446
448 447 def write(self, fp):
449 448 # updated self.added/removed, which are used by getfromtoline()
450 449 self.added, self.removed = self.countchanges()
451 450 fp.write(self.getfromtoline())
452 451
453 452 hunklinelist = []
454 453 # add the following to the list: (1) all applied lines, and
455 454 # (2) all unapplied removal lines (convert these to context lines)
456 455 for changedline in self.changedlines:
457 456 changedlinestr = changedline.prettystr()
458 457 if changedline.applied:
459 458 hunklinelist.append(changedlinestr)
460 459 elif changedlinestr.startswith(b"-"):
461 460 hunklinelist.append(b" " + changedlinestr[1:])
462 461
463 462 fp.write(b''.join(self.before + hunklinelist + self.after))
464 463
465 464 pretty = write
466 465
467 466 def prettystr(self):
468 467 x = stringio()
469 468 self.pretty(x)
470 469 return x.getvalue()
471 470
472 471 def reversehunk(self):
473 472 """return a recordhunk which is the reverse of the hunk
474 473
475 474 Assuming the displayed patch is diff(A, B) result. The returned hunk is
476 475 intended to be applied to B, instead of A.
477 476
478 477 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
479 478 the user made the following selection:
480 479
481 480 0
482 481 [x] -1 [x]: selected
483 482 [ ] -2 [ ]: not selected
484 483 [x] +3
485 484 [ ] +4
486 485 [x] +5
487 486 6
488 487
489 488 This function returns a hunk like:
490 489
491 490 0
492 491 -3
493 492 -4
494 493 -5
495 494 +1
496 495 +4
497 496 6
498 497
499 498 Note "4" was first deleted then added. That's because "4" exists in B
500 499 side and "-4" must exist between "-3" and "-5" to make the patch
501 500 applicable to B.
502 501 """
503 502 dels = []
504 503 adds = []
505 504 for line in self.changedlines:
506 505 text = line.linetext
507 506 if line.applied:
508 507 if text.startswith(b'+'):
509 508 dels.append(text[1:])
510 509 elif text.startswith(b'-'):
511 510 adds.append(text[1:])
512 511 elif text.startswith(b'+'):
513 512 dels.append(text[1:])
514 513 adds.append(text[1:])
515 514 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
516 515 h = self._hunk
517 516 return patchmod.recordhunk(
518 517 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
519 518 )
520 519
521 520 def __getattr__(self, name):
522 521 return getattr(self._hunk, name)
523 522
524 523 def __repr__(self):
525 524 return r'<hunk %r@%d>' % (self.filename(), self.fromline)
526 525
527 526
528 527 def filterpatch(ui, chunks, chunkselector, operation=None):
529 528 """interactively filter patch chunks into applied-only chunks"""
530 529 chunks = list(chunks)
531 530 # convert chunks list into structure suitable for displaying/modifying
532 531 # with curses. create a list of headers only.
533 532 headers = [c for c in chunks if isinstance(c, patchmod.header)]
534 533
535 534 # if there are no changed files
536 535 if len(headers) == 0:
537 536 return [], {}
538 537 uiheaders = [uiheader(h) for h in headers]
539 538 # let user choose headers/hunks/lines, and mark their applied flags
540 539 # accordingly
541 540 ret = chunkselector(ui, uiheaders, operation=operation)
542 541 appliedhunklist = []
543 542 for hdr in uiheaders:
544 543 if hdr.applied and (
545 544 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
546 545 ):
547 546 appliedhunklist.append(hdr)
548 547 fixoffset = 0
549 548 for hnk in hdr.hunks:
550 549 if hnk.applied:
551 550 appliedhunklist.append(hnk)
552 551 # adjust the 'to'-line offset of the hunk to be correct
553 552 # after de-activating some of the other hunks for this file
554 553 if fixoffset:
555 554 # hnk = copy.copy(hnk) # necessary??
556 555 hnk.toline += fixoffset
557 556 else:
558 557 fixoffset += hnk.removed - hnk.added
559 558
560 559 return (appliedhunklist, ret)
561 560
562 561
563 562 def chunkselector(ui, headerlist, operation=None):
564 563 """
565 564 curses interface to get selection of chunks, and mark the applied flags
566 565 of the chosen chunks.
567 566 """
568 567 ui.write(_(b'starting interactive selection\n'))
569 568 chunkselector = curseschunkselector(headerlist, ui, operation)
570 569 # This is required for ncurses to display non-ASCII characters in
571 570 # default user locale encoding correctly. --immerrr
572 571 locale.setlocale(locale.LC_ALL, r'')
573 572 origsigtstp = sentinel = object()
574 573 if util.safehasattr(signal, b'SIGTSTP'):
575 574 origsigtstp = signal.getsignal(signal.SIGTSTP)
576 575 try:
577 576 curses.wrapper(chunkselector.main)
578 577 if chunkselector.initexc is not None:
579 578 raise chunkselector.initexc
580 579 # ncurses does not restore signal handler for SIGTSTP
581 580 finally:
582 581 if origsigtstp is not sentinel:
583 582 signal.signal(signal.SIGTSTP, origsigtstp)
584 583 return chunkselector.opts
585 584
586 585
587 586 def testdecorator(testfn, f):
588 587 def u(*args, **kwargs):
589 588 return f(testfn, *args, **kwargs)
590 589
591 590 return u
592 591
593 592
594 593 def testchunkselector(testfn, ui, headerlist, operation=None):
595 594 """
596 595 test interface to get selection of chunks, and mark the applied flags
597 596 of the chosen chunks.
598 597 """
599 598 chunkselector = curseschunkselector(headerlist, ui, operation)
600 599
601 600 class dummystdscr(object):
602 601 def clear(self):
603 602 pass
604 603
605 604 def refresh(self):
606 605 pass
607 606
608 607 chunkselector.stdscr = dummystdscr()
609 608 if testfn and os.path.exists(testfn):
610 testf = open(testfn, b'rb')
611 testcommands = [x.rstrip(b'\n') for x in testf.readlines()]
609 testf = open(testfn, 'r')
610 testcommands = [x.rstrip('\n') for x in testf.readlines()]
612 611 testf.close()
613 612 while True:
614 613 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
615 614 break
616 615 return chunkselector.opts
617 616
618 617
619 618 _headermessages = { # {operation: text}
620 619 b'apply': _(b'Select hunks to apply'),
621 620 b'discard': _(b'Select hunks to discard'),
622 621 b'keep': _(b'Select hunks to keep'),
623 622 None: _(b'Select hunks to record'),
624 623 }
625 624
626 625
627 626 class curseschunkselector(object):
628 627 def __init__(self, headerlist, ui, operation=None):
629 628 # put the headers into a patch object
630 629 self.headerlist = patch(headerlist)
631 630
632 631 self.ui = ui
633 632 self.opts = {}
634 633
635 634 self.errorstr = None
636 635 # list of all chunks
637 636 self.chunklist = []
638 637 for h in headerlist:
639 638 self.chunklist.append(h)
640 639 self.chunklist.extend(h.hunks)
641 640
642 641 # dictionary mapping (fgcolor, bgcolor) pairs to the
643 642 # corresponding curses color-pair value.
644 643 self.colorpairs = {}
645 644 # maps custom nicknames of color-pairs to curses color-pair values
646 645 self.colorpairnames = {}
647 646
648 647 # Honor color setting of ui section. Keep colored setup as
649 648 # long as not explicitly set to a falsy value - especially,
650 649 # when not set at all. This is to stay most compatible with
651 650 # previous (color only) behaviour.
652 651 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
653 652 self.usecolor = uicolor is not False
654 653
655 654 # the currently selected header, hunk, or hunk-line
656 655 self.currentselecteditem = self.headerlist[0]
657 656 self.lastapplieditem = None
658 657
659 658 # updated when printing out patch-display -- the 'lines' here are the
660 659 # line positions *in the pad*, not on the screen.
661 660 self.selecteditemstartline = 0
662 661 self.selecteditemendline = None
663 662
664 663 # define indentation levels
665 664 self.headerindentnumchars = 0
666 665 self.hunkindentnumchars = 3
667 666 self.hunklineindentnumchars = 6
668 667
669 668 # the first line of the pad to print to the screen
670 669 self.firstlineofpadtoprint = 0
671 670
672 671 # keeps track of the number of lines in the pad
673 672 self.numpadlines = None
674 673
675 674 self.numstatuslines = 1
676 675
677 676 # keep a running count of the number of lines printed to the pad
678 677 # (used for determining when the selected item begins/ends)
679 678 self.linesprintedtopadsofar = 0
680 679
681 680 # stores optional text for a commit comment provided by the user
682 681 self.commenttext = b""
683 682
684 683 # if the last 'toggle all' command caused all changes to be applied
685 684 self.waslasttoggleallapplied = True
686 685
687 686 # affects some ui text
688 687 if operation not in _headermessages:
689 688 raise error.ProgrammingError(
690 689 b'unexpected operation: %s' % operation
691 690 )
692 691 self.operation = operation
693 692
694 693 def uparrowevent(self):
695 694 """
696 695 try to select the previous item to the current item that has the
697 696 most-indented level. for example, if a hunk is selected, try to select
698 697 the last hunkline of the hunk prior to the selected hunk. or, if
699 698 the first hunkline of a hunk is currently selected, then select the
700 699 hunk itself.
701 700 """
702 701 currentitem = self.currentselecteditem
703 702
704 703 nextitem = currentitem.previtem()
705 704
706 705 if nextitem is None:
707 706 # if no parent item (i.e. currentitem is the first header), then
708 707 # no change...
709 708 nextitem = currentitem
710 709
711 710 self.currentselecteditem = nextitem
712 711
713 712 def uparrowshiftevent(self):
714 713 """
715 714 select (if possible) the previous item on the same level as the
716 715 currently selected item. otherwise, select (if possible) the
717 716 parent-item of the currently selected item.
718 717 """
719 718 currentitem = self.currentselecteditem
720 719 nextitem = currentitem.prevsibling()
721 720 # if there's no previous sibling, try choosing the parent
722 721 if nextitem is None:
723 722 nextitem = currentitem.parentitem()
724 723 if nextitem is None:
725 724 # if no parent item (i.e. currentitem is the first header), then
726 725 # no change...
727 726 nextitem = currentitem
728 727
729 728 self.currentselecteditem = nextitem
730 729 self.recenterdisplayedarea()
731 730
732 731 def downarrowevent(self):
733 732 """
734 733 try to select the next item to the current item that has the
735 734 most-indented level. for example, if a hunk is selected, select
736 735 the first hunkline of the selected hunk. or, if the last hunkline of
737 736 a hunk is currently selected, then select the next hunk, if one exists,
738 737 or if not, the next header if one exists.
739 738 """
740 739 # self.startprintline += 1 #debug
741 740 currentitem = self.currentselecteditem
742 741
743 742 nextitem = currentitem.nextitem()
744 743 # if there's no next item, keep the selection as-is
745 744 if nextitem is None:
746 745 nextitem = currentitem
747 746
748 747 self.currentselecteditem = nextitem
749 748
750 749 def downarrowshiftevent(self):
751 750 """
752 751 select (if possible) the next item on the same level as the currently
753 752 selected item. otherwise, select (if possible) the next item on the
754 753 same level as the parent item of the currently selected item.
755 754 """
756 755 currentitem = self.currentselecteditem
757 756 nextitem = currentitem.nextsibling()
758 757 # if there's no next sibling, try choosing the parent's nextsibling
759 758 if nextitem is None:
760 759 try:
761 760 nextitem = currentitem.parentitem().nextsibling()
762 761 except AttributeError:
763 762 # parentitem returned None, so nextsibling() can't be called
764 763 nextitem = None
765 764 if nextitem is None:
766 765 # if parent has no next sibling, then no change...
767 766 nextitem = currentitem
768 767
769 768 self.currentselecteditem = nextitem
770 769 self.recenterdisplayedarea()
771 770
772 771 def nextsametype(self, test=False):
773 772 currentitem = self.currentselecteditem
774 773 sametype = lambda item: isinstance(item, type(currentitem))
775 774 nextitem = currentitem.nextitem()
776 775
777 776 while nextitem is not None and not sametype(nextitem):
778 777 nextitem = nextitem.nextitem()
779 778
780 779 if nextitem is None:
781 780 nextitem = currentitem
782 781 else:
783 782 parent = nextitem.parentitem()
784 783 if parent is not None and parent.folded:
785 784 self.togglefolded(parent)
786 785
787 786 self.currentselecteditem = nextitem
788 787 if not test:
789 788 self.recenterdisplayedarea()
790 789
791 790 def rightarrowevent(self):
792 791 """
793 792 select (if possible) the first of this item's child-items.
794 793 """
795 794 currentitem = self.currentselecteditem
796 795 nextitem = currentitem.firstchild()
797 796
798 797 # turn off folding if we want to show a child-item
799 798 if currentitem.folded:
800 799 self.togglefolded(currentitem)
801 800
802 801 if nextitem is None:
803 802 # if no next item on parent-level, then no change...
804 803 nextitem = currentitem
805 804
806 805 self.currentselecteditem = nextitem
807 806
808 807 def leftarrowevent(self):
809 808 """
810 809 if the current item can be folded (i.e. it is an unfolded header or
811 810 hunk), then fold it. otherwise try select (if possible) the parent
812 811 of this item.
813 812 """
814 813 currentitem = self.currentselecteditem
815 814
816 815 # try to fold the item
817 816 if not isinstance(currentitem, uihunkline):
818 817 if not currentitem.folded:
819 818 self.togglefolded(item=currentitem)
820 819 return
821 820
822 821 # if it can't be folded, try to select the parent item
823 822 nextitem = currentitem.parentitem()
824 823
825 824 if nextitem is None:
826 825 # if no item on parent-level, then no change...
827 826 nextitem = currentitem
828 827 if not nextitem.folded:
829 828 self.togglefolded(item=nextitem)
830 829
831 830 self.currentselecteditem = nextitem
832 831
833 832 def leftarrowshiftevent(self):
834 833 """
835 834 select the header of the current item (or fold current item if the
836 835 current item is already a header).
837 836 """
838 837 currentitem = self.currentselecteditem
839 838
840 839 if isinstance(currentitem, uiheader):
841 840 if not currentitem.folded:
842 841 self.togglefolded(item=currentitem)
843 842 return
844 843
845 844 # select the parent item recursively until we're at a header
846 845 while True:
847 846 nextitem = currentitem.parentitem()
848 847 if nextitem is None:
849 848 break
850 849 else:
851 850 currentitem = nextitem
852 851
853 852 self.currentselecteditem = currentitem
854 853
855 854 def updatescroll(self):
856 855 b"scroll the screen to fully show the currently-selected"
857 856 selstart = self.selecteditemstartline
858 857 selend = self.selecteditemendline
859 858
860 859 padstart = self.firstlineofpadtoprint
861 860 padend = padstart + self.yscreensize - self.numstatuslines - 1
862 861 # 'buffered' pad start/end values which scroll with a certain
863 862 # top/bottom context margin
864 863 padstartbuffered = padstart + 3
865 864 padendbuffered = padend - 3
866 865
867 866 if selend > padendbuffered:
868 867 self.scrolllines(selend - padendbuffered)
869 868 elif selstart < padstartbuffered:
870 869 # negative values scroll in pgup direction
871 870 self.scrolllines(selstart - padstartbuffered)
872 871
873 872 def scrolllines(self, numlines):
874 873 b"scroll the screen up (down) by numlines when numlines >0 (<0)."
875 874 self.firstlineofpadtoprint += numlines
876 875 if self.firstlineofpadtoprint < 0:
877 876 self.firstlineofpadtoprint = 0
878 877 if self.firstlineofpadtoprint > self.numpadlines - 1:
879 878 self.firstlineofpadtoprint = self.numpadlines - 1
880 879
881 880 def toggleapply(self, item=None):
882 881 """
883 882 toggle the applied flag of the specified item. if no item is specified,
884 883 toggle the flag of the currently selected item.
885 884 """
886 885 if item is None:
887 886 item = self.currentselecteditem
888 887 # Only set this when NOT using 'toggleall'
889 888 self.lastapplieditem = item
890 889
891 890 item.applied = not item.applied
892 891
893 892 if isinstance(item, uiheader):
894 893 item.partial = False
895 894 if item.applied:
896 895 # apply all its hunks
897 896 for hnk in item.hunks:
898 897 hnk.applied = True
899 898 # apply all their hunklines
900 899 for hunkline in hnk.changedlines:
901 900 hunkline.applied = True
902 901 else:
903 902 # un-apply all its hunks
904 903 for hnk in item.hunks:
905 904 hnk.applied = False
906 905 hnk.partial = False
907 906 # un-apply all their hunklines
908 907 for hunkline in hnk.changedlines:
909 908 hunkline.applied = False
910 909 elif isinstance(item, uihunk):
911 910 item.partial = False
912 911 # apply all it's hunklines
913 912 for hunkline in item.changedlines:
914 913 hunkline.applied = item.applied
915 914
916 915 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
917 916 allsiblingsapplied = not (False in siblingappliedstatus)
918 917 nosiblingsapplied = not (True in siblingappliedstatus)
919 918
920 919 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
921 920 somesiblingspartial = True in siblingspartialstatus
922 921
923 922 # cases where applied or partial should be removed from header
924 923
925 924 # if no 'sibling' hunks are applied (including this hunk)
926 925 if nosiblingsapplied:
927 926 if not item.header.special():
928 927 item.header.applied = False
929 928 item.header.partial = False
930 929 else: # some/all parent siblings are applied
931 930 item.header.applied = True
932 931 item.header.partial = (
933 932 somesiblingspartial or not allsiblingsapplied
934 933 )
935 934
936 935 elif isinstance(item, uihunkline):
937 936 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
938 937 allsiblingsapplied = not (False in siblingappliedstatus)
939 938 nosiblingsapplied = not (True in siblingappliedstatus)
940 939
941 940 # if no 'sibling' lines are applied
942 941 if nosiblingsapplied:
943 942 item.hunk.applied = False
944 943 item.hunk.partial = False
945 944 elif allsiblingsapplied:
946 945 item.hunk.applied = True
947 946 item.hunk.partial = False
948 947 else: # some siblings applied
949 948 item.hunk.applied = True
950 949 item.hunk.partial = True
951 950
952 951 parentsiblingsapplied = [
953 952 hnk.applied for hnk in item.hunk.header.hunks
954 953 ]
955 954 noparentsiblingsapplied = not (True in parentsiblingsapplied)
956 955 allparentsiblingsapplied = not (False in parentsiblingsapplied)
957 956
958 957 parentsiblingspartial = [
959 958 hnk.partial for hnk in item.hunk.header.hunks
960 959 ]
961 960 someparentsiblingspartial = True in parentsiblingspartial
962 961
963 962 # if all parent hunks are not applied, un-apply header
964 963 if noparentsiblingsapplied:
965 964 if not item.hunk.header.special():
966 965 item.hunk.header.applied = False
967 966 item.hunk.header.partial = False
968 967 # set the applied and partial status of the header if needed
969 968 else: # some/all parent siblings are applied
970 969 item.hunk.header.applied = True
971 970 item.hunk.header.partial = (
972 971 someparentsiblingspartial or not allparentsiblingsapplied
973 972 )
974 973
975 974 def toggleall(self):
976 975 b"toggle the applied flag of all items."
977 976 if self.waslasttoggleallapplied: # then unapply them this time
978 977 for item in self.headerlist:
979 978 if item.applied:
980 979 self.toggleapply(item)
981 980 else:
982 981 for item in self.headerlist:
983 982 if not item.applied:
984 983 self.toggleapply(item)
985 984 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
986 985
987 986 def toggleallbetween(self):
988 987 b"toggle applied on or off for all items in range [lastapplied,current]."
989 988 if (
990 989 not self.lastapplieditem
991 990 or self.currentselecteditem == self.lastapplieditem
992 991 ):
993 992 # Treat this like a normal 'x'/' '
994 993 self.toggleapply()
995 994 return
996 995
997 996 startitem = self.lastapplieditem
998 997 enditem = self.currentselecteditem
999 998 # Verify that enditem is "after" startitem, otherwise swap them.
1000 999 for direction in [b'forward', b'reverse']:
1001 1000 nextitem = startitem.nextitem()
1002 1001 while nextitem and nextitem != enditem:
1003 1002 nextitem = nextitem.nextitem()
1004 1003 if nextitem:
1005 1004 break
1006 1005 # Looks like we went the wrong direction :)
1007 1006 startitem, enditem = enditem, startitem
1008 1007
1009 1008 if not nextitem:
1010 1009 # We didn't find a path going either forward or backward? Don't know
1011 1010 # how this can happen, let's not crash though.
1012 1011 return
1013 1012
1014 1013 nextitem = startitem
1015 1014 # Switch all items to be the opposite state of the currently selected
1016 1015 # item. Specifically:
1017 1016 # [ ] startitem
1018 1017 # [x] middleitem
1019 1018 # [ ] enditem <-- currently selected
1020 1019 # This will turn all three on, since the currently selected item is off.
1021 1020 # This does *not* invert each item (i.e. middleitem stays marked/on)
1022 1021 desiredstate = not self.currentselecteditem.applied
1023 1022 while nextitem != enditem.nextitem():
1024 1023 if nextitem.applied != desiredstate:
1025 1024 self.toggleapply(item=nextitem)
1026 1025 nextitem = nextitem.nextitem()
1027 1026
1028 1027 def togglefolded(self, item=None, foldparent=False):
1029 1028 b"toggle folded flag of specified item (defaults to currently selected)"
1030 1029 if item is None:
1031 1030 item = self.currentselecteditem
1032 1031 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1033 1032 if not isinstance(item, uiheader):
1034 1033 # we need to select the parent item in this case
1035 1034 self.currentselecteditem = item = item.parentitem()
1036 1035 elif item.neverunfolded:
1037 1036 item.neverunfolded = False
1038 1037
1039 1038 # also fold any foldable children of the parent/current item
1040 1039 if isinstance(item, uiheader): # the original or 'new' item
1041 1040 for child in item.allchildren():
1042 1041 child.folded = not item.folded
1043 1042
1044 1043 if isinstance(item, (uiheader, uihunk)):
1045 1044 item.folded = not item.folded
1046 1045
1047 1046 def alignstring(self, instr, window):
1048 1047 """
1049 1048 add whitespace to the end of a string in order to make it fill
1050 1049 the screen in the x direction. the current cursor position is
1051 1050 taken into account when making this calculation. the string can span
1052 1051 multiple lines.
1053 1052 """
1054 1053 y, xstart = window.getyx()
1055 1054 width = self.xscreensize
1056 1055 # turn tabs into spaces
1057 1056 instr = instr.expandtabs(4)
1058 1057 strwidth = encoding.colwidth(instr)
1059 1058 numspaces = width - ((strwidth + xstart) % width)
1060 1059 return instr + b" " * numspaces
1061 1060
1062 1061 def printstring(
1063 1062 self,
1064 1063 window,
1065 1064 text,
1066 1065 fgcolor=None,
1067 1066 bgcolor=None,
1068 1067 pair=None,
1069 1068 pairname=None,
1070 1069 attrlist=None,
1071 1070 towin=True,
1072 1071 align=True,
1073 1072 showwhtspc=False,
1074 1073 ):
1075 1074 """
1076 1075 print the string, text, with the specified colors and attributes, to
1077 1076 the specified curses window object.
1078 1077
1079 1078 the foreground and background colors are of the form
1080 1079 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1081 1080 magenta, red, white, yellow]. if pairname is provided, a color
1082 1081 pair will be looked up in the self.colorpairnames dictionary.
1083 1082
1084 1083 attrlist is a list containing text attributes in the form of
1085 1084 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1086 1085 underline].
1087 1086
1088 1087 if align == True, whitespace is added to the printed string such that
1089 1088 the string stretches to the right border of the window.
1090 1089
1091 1090 if showwhtspc == True, trailing whitespace of a string is highlighted.
1092 1091 """
1093 1092 # preprocess the text, converting tabs to spaces
1094 1093 text = text.expandtabs(4)
1095 1094 # strip \n, and convert control characters to ^[char] representation
1096 1095 text = re.sub(
1097 1096 br'[\x00-\x08\x0a-\x1f]',
1098 1097 lambda m: b'^' + chr(ord(m.group()) + 64),
1099 1098 text.strip(b'\n'),
1100 1099 )
1101 1100
1102 1101 if pair is not None:
1103 1102 colorpair = pair
1104 1103 elif pairname is not None:
1105 1104 colorpair = self.colorpairnames[pairname]
1106 1105 else:
1107 1106 if fgcolor is None:
1108 1107 fgcolor = -1
1109 1108 if bgcolor is None:
1110 1109 bgcolor = -1
1111 1110 if (fgcolor, bgcolor) in self.colorpairs:
1112 1111 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1113 1112 else:
1114 1113 colorpair = self.getcolorpair(fgcolor, bgcolor)
1115 1114 # add attributes if possible
1116 1115 if attrlist is None:
1117 1116 attrlist = []
1118 1117 if colorpair < 256:
1119 1118 # then it is safe to apply all attributes
1120 1119 for textattr in attrlist:
1121 1120 colorpair |= textattr
1122 1121 else:
1123 1122 # just apply a select few (safe?) attributes
1124 1123 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1125 1124 if textattr in attrlist:
1126 1125 colorpair |= textattr
1127 1126
1128 1127 y, xstart = self.chunkpad.getyx()
1129 1128 t = b"" # variable for counting lines printed
1130 1129 # if requested, show trailing whitespace
1131 1130 if showwhtspc:
1132 1131 origlen = len(text)
1133 1132 text = text.rstrip(b' \n') # tabs have already been expanded
1134 1133 strippedlen = len(text)
1135 1134 numtrailingspaces = origlen - strippedlen
1136 1135
1137 1136 if towin:
1138 1137 window.addstr(text, colorpair)
1139 1138 t += text
1140 1139
1141 1140 if showwhtspc:
1142 1141 wscolorpair = colorpair | curses.A_REVERSE
1143 1142 if towin:
1144 1143 for i in range(numtrailingspaces):
1145 1144 window.addch(curses.ACS_CKBOARD, wscolorpair)
1146 1145 t += b" " * numtrailingspaces
1147 1146
1148 1147 if align:
1149 1148 if towin:
1150 1149 extrawhitespace = self.alignstring(b"", window)
1151 1150 window.addstr(extrawhitespace, colorpair)
1152 1151 else:
1153 1152 # need to use t, since the x position hasn't incremented
1154 1153 extrawhitespace = self.alignstring(t, window)
1155 1154 t += extrawhitespace
1156 1155
1157 1156 # is reset to 0 at the beginning of printitem()
1158 1157
1159 1158 linesprinted = (xstart + len(t)) // self.xscreensize
1160 1159 self.linesprintedtopadsofar += linesprinted
1161 1160 return t
1162 1161
1163 1162 def _getstatuslinesegments(self):
1164 1163 """-> [str]. return segments"""
1165 1164 selected = self.currentselecteditem.applied
1166 1165 spaceselect = _(b'space/enter: select')
1167 1166 spacedeselect = _(b'space/enter: deselect')
1168 1167 # Format the selected label into a place as long as the longer of the
1169 1168 # two possible labels. This may vary by language.
1170 1169 spacelen = max(len(spaceselect), len(spacedeselect))
1171 1170 selectedlabel = b'%-*s' % (
1172 1171 spacelen,
1173 1172 spacedeselect if selected else spaceselect,
1174 1173 )
1175 1174 segments = [
1176 1175 _headermessages[self.operation],
1177 1176 b'-',
1178 1177 _(b'[x]=selected **=collapsed'),
1179 1178 _(b'c: confirm'),
1180 1179 _(b'q: abort'),
1181 1180 _(b'arrow keys: move/expand/collapse'),
1182 1181 selectedlabel,
1183 1182 _(b'?: help'),
1184 1183 ]
1185 1184 return segments
1186 1185
1187 1186 def _getstatuslines(self):
1188 1187 """() -> [str]. return short help used in the top status window"""
1189 1188 if self.errorstr is not None:
1190 1189 lines = [self.errorstr, _(b'Press any key to continue')]
1191 1190 else:
1192 1191 # wrap segments to lines
1193 1192 segments = self._getstatuslinesegments()
1194 1193 width = self.xscreensize
1195 1194 lines = []
1196 1195 lastwidth = width
1197 1196 for s in segments:
1198 1197 w = encoding.colwidth(s)
1199 1198 sep = b' ' * (1 + (s and s[0] not in b'-['))
1200 1199 if lastwidth + w + len(sep) >= width:
1201 1200 lines.append(s)
1202 1201 lastwidth = w
1203 1202 else:
1204 1203 lines[-1] += sep + s
1205 1204 lastwidth += w + len(sep)
1206 1205 if len(lines) != self.numstatuslines:
1207 1206 self.numstatuslines = len(lines)
1208 1207 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1209 1208 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1210 1209
1211 1210 def updatescreen(self):
1212 1211 self.statuswin.erase()
1213 1212 self.chunkpad.erase()
1214 1213
1215 1214 printstring = self.printstring
1216 1215
1217 1216 # print out the status lines at the top
1218 1217 try:
1219 1218 for line in self._getstatuslines():
1220 1219 printstring(self.statuswin, line, pairname=b"legend")
1221 1220 self.statuswin.refresh()
1222 1221 except curses.error:
1223 1222 pass
1224 1223 if self.errorstr is not None:
1225 1224 return
1226 1225
1227 1226 # print out the patch in the remaining part of the window
1228 1227 try:
1229 1228 self.printitem()
1230 1229 self.updatescroll()
1231 1230 self.chunkpad.refresh(
1232 1231 self.firstlineofpadtoprint,
1233 1232 0,
1234 1233 self.numstatuslines,
1235 1234 0,
1236 1235 self.yscreensize - self.numstatuslines,
1237 1236 self.xscreensize,
1238 1237 )
1239 1238 except curses.error:
1240 1239 pass
1241 1240
1242 1241 def getstatusprefixstring(self, item):
1243 1242 """
1244 1243 create a string to prefix a line with which indicates whether 'item'
1245 1244 is applied and/or folded.
1246 1245 """
1247 1246
1248 1247 # create checkbox string
1249 1248 if item.applied:
1250 1249 if not isinstance(item, uihunkline) and item.partial:
1251 1250 checkbox = b"[~]"
1252 1251 else:
1253 1252 checkbox = b"[x]"
1254 1253 else:
1255 1254 checkbox = b"[ ]"
1256 1255
1257 1256 try:
1258 1257 if item.folded:
1259 1258 checkbox += b"**"
1260 1259 if isinstance(item, uiheader):
1261 1260 # one of "m", "a", or "d" (modified, added, deleted)
1262 1261 filestatus = item.changetype
1263 1262
1264 1263 checkbox += filestatus + b" "
1265 1264 else:
1266 1265 checkbox += b" "
1267 1266 if isinstance(item, uiheader):
1268 1267 # add two more spaces for headers
1269 1268 checkbox += b" "
1270 1269 except AttributeError: # not foldable
1271 1270 checkbox += b" "
1272 1271
1273 1272 return checkbox
1274 1273
1275 1274 def printheader(
1276 1275 self, header, selected=False, towin=True, ignorefolding=False
1277 1276 ):
1278 1277 """
1279 1278 print the header to the pad. if countlines is True, don't print
1280 1279 anything, but just count the number of lines which would be printed.
1281 1280 """
1282 1281
1283 1282 outstr = b""
1284 1283 text = header.prettystr()
1285 1284 chunkindex = self.chunklist.index(header)
1286 1285
1287 1286 if chunkindex != 0 and not header.folded:
1288 1287 # add separating line before headers
1289 1288 outstr += self.printstring(
1290 1289 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1291 1290 )
1292 1291 # select color-pair based on if the header is selected
1293 1292 colorpair = self.getcolorpair(
1294 1293 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1295 1294 )
1296 1295
1297 1296 # print out each line of the chunk, expanding it to screen width
1298 1297
1299 1298 # number of characters to indent lines on this level by
1300 1299 indentnumchars = 0
1301 1300 checkbox = self.getstatusprefixstring(header)
1302 1301 if not header.folded or ignorefolding:
1303 1302 textlist = text.split(b"\n")
1304 1303 linestr = checkbox + textlist[0]
1305 1304 else:
1306 1305 linestr = checkbox + header.filename()
1307 1306 outstr += self.printstring(
1308 1307 self.chunkpad, linestr, pair=colorpair, towin=towin
1309 1308 )
1310 1309 if not header.folded or ignorefolding:
1311 1310 if len(textlist) > 1:
1312 1311 for line in textlist[1:]:
1313 1312 linestr = b" " * (indentnumchars + len(checkbox)) + line
1314 1313 outstr += self.printstring(
1315 1314 self.chunkpad, linestr, pair=colorpair, towin=towin
1316 1315 )
1317 1316
1318 1317 return outstr
1319 1318
1320 1319 def printhunklinesbefore(
1321 1320 self, hunk, selected=False, towin=True, ignorefolding=False
1322 1321 ):
1323 1322 b"includes start/end line indicator"
1324 1323 outstr = b""
1325 1324 # where hunk is in list of siblings
1326 1325 hunkindex = hunk.header.hunks.index(hunk)
1327 1326
1328 1327 if hunkindex != 0:
1329 1328 # add separating line before headers
1330 1329 outstr += self.printstring(
1331 1330 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1332 1331 )
1333 1332
1334 1333 colorpair = self.getcolorpair(
1335 1334 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1336 1335 )
1337 1336
1338 1337 # print out from-to line with checkbox
1339 1338 checkbox = self.getstatusprefixstring(hunk)
1340 1339
1341 1340 lineprefix = b" " * self.hunkindentnumchars + checkbox
1342 1341 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1343 1342
1344 1343 outstr += self.printstring(
1345 1344 self.chunkpad, lineprefix, towin=towin, align=False
1346 1345 ) # add uncolored checkbox/indent
1347 1346 outstr += self.printstring(
1348 1347 self.chunkpad, frtoline, pair=colorpair, towin=towin
1349 1348 )
1350 1349
1351 1350 if hunk.folded and not ignorefolding:
1352 1351 # skip remainder of output
1353 1352 return outstr
1354 1353
1355 1354 # print out lines of the chunk preceeding changed-lines
1356 1355 for line in hunk.before:
1357 1356 linestr = (
1358 1357 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1359 1358 )
1360 1359 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1361 1360
1362 1361 return outstr
1363 1362
1364 1363 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1365 1364 outstr = b""
1366 1365 if hunk.folded and not ignorefolding:
1367 1366 return outstr
1368 1367
1369 1368 # a bit superfluous, but to avoid hard-coding indent amount
1370 1369 checkbox = self.getstatusprefixstring(hunk)
1371 1370 for line in hunk.after:
1372 1371 linestr = (
1373 1372 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1374 1373 )
1375 1374 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1376 1375
1377 1376 return outstr
1378 1377
1379 1378 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1380 1379 outstr = b""
1381 1380 checkbox = self.getstatusprefixstring(hunkline)
1382 1381
1383 1382 linestr = hunkline.prettystr().strip(b"\n")
1384 1383
1385 1384 # select color-pair based on whether line is an addition/removal
1386 1385 if selected:
1387 1386 colorpair = self.getcolorpair(name=b"selected")
1388 1387 elif linestr.startswith(b"+"):
1389 1388 colorpair = self.getcolorpair(name=b"addition")
1390 1389 elif linestr.startswith(b"-"):
1391 1390 colorpair = self.getcolorpair(name=b"deletion")
1392 1391 elif linestr.startswith(b"\\"):
1393 1392 colorpair = self.getcolorpair(name=b"normal")
1394 1393
1395 1394 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1396 1395 outstr += self.printstring(
1397 1396 self.chunkpad, lineprefix, towin=towin, align=False
1398 1397 ) # add uncolored checkbox/indent
1399 1398 outstr += self.printstring(
1400 1399 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1401 1400 )
1402 1401 return outstr
1403 1402
1404 1403 def printitem(
1405 1404 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1406 1405 ):
1407 1406 """
1408 1407 use __printitem() to print the the specified item.applied.
1409 1408 if item is not specified, then print the entire patch.
1410 1409 (hiding folded elements, etc. -- see __printitem() docstring)
1411 1410 """
1412 1411
1413 1412 if item is None:
1414 1413 item = self.headerlist
1415 1414 if recursechildren:
1416 1415 self.linesprintedtopadsofar = 0
1417 1416
1418 1417 outstr = []
1419 1418 self.__printitem(
1420 1419 item, ignorefolding, recursechildren, outstr, towin=towin
1421 1420 )
1422 1421 return b''.join(outstr)
1423 1422
1424 1423 def outofdisplayedarea(self):
1425 1424 y, _ = self.chunkpad.getyx() # cursor location
1426 1425 # * 2 here works but an optimization would be the max number of
1427 1426 # consecutive non selectable lines
1428 1427 # i.e the max number of context line for any hunk in the patch
1429 1428 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1430 1429 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1431 1430 return y < miny or y > maxy
1432 1431
1433 1432 def handleselection(self, item, recursechildren):
1434 1433 selected = item is self.currentselecteditem
1435 1434 if selected and recursechildren:
1436 1435 # assumes line numbering starting from line 0
1437 1436 self.selecteditemstartline = self.linesprintedtopadsofar
1438 1437 selecteditemlines = self.getnumlinesdisplayed(
1439 1438 item, recursechildren=False
1440 1439 )
1441 1440 self.selecteditemendline = (
1442 1441 self.selecteditemstartline + selecteditemlines - 1
1443 1442 )
1444 1443 return selected
1445 1444
1446 1445 def __printitem(
1447 1446 self, item, ignorefolding, recursechildren, outstr, towin=True
1448 1447 ):
1449 1448 """
1450 1449 recursive method for printing out patch/header/hunk/hunk-line data to
1451 1450 screen. also returns a string with all of the content of the displayed
1452 1451 patch (not including coloring, etc.).
1453 1452
1454 1453 if ignorefolding is True, then folded items are printed out.
1455 1454
1456 1455 if recursechildren is False, then only print the item without its
1457 1456 child items.
1458 1457 """
1459 1458
1460 1459 if towin and self.outofdisplayedarea():
1461 1460 return
1462 1461
1463 1462 selected = self.handleselection(item, recursechildren)
1464 1463
1465 1464 # patch object is a list of headers
1466 1465 if isinstance(item, patch):
1467 1466 if recursechildren:
1468 1467 for hdr in item:
1469 1468 self.__printitem(
1470 1469 hdr, ignorefolding, recursechildren, outstr, towin
1471 1470 )
1472 1471 # todo: eliminate all isinstance() calls
1473 1472 if isinstance(item, uiheader):
1474 1473 outstr.append(
1475 1474 self.printheader(
1476 1475 item, selected, towin=towin, ignorefolding=ignorefolding
1477 1476 )
1478 1477 )
1479 1478 if recursechildren:
1480 1479 for hnk in item.hunks:
1481 1480 self.__printitem(
1482 1481 hnk, ignorefolding, recursechildren, outstr, towin
1483 1482 )
1484 1483 elif isinstance(item, uihunk) and (
1485 1484 (not item.header.folded) or ignorefolding
1486 1485 ):
1487 1486 # print the hunk data which comes before the changed-lines
1488 1487 outstr.append(
1489 1488 self.printhunklinesbefore(
1490 1489 item, selected, towin=towin, ignorefolding=ignorefolding
1491 1490 )
1492 1491 )
1493 1492 if recursechildren:
1494 1493 for l in item.changedlines:
1495 1494 self.__printitem(
1496 1495 l, ignorefolding, recursechildren, outstr, towin
1497 1496 )
1498 1497 outstr.append(
1499 1498 self.printhunklinesafter(
1500 1499 item, towin=towin, ignorefolding=ignorefolding
1501 1500 )
1502 1501 )
1503 1502 elif isinstance(item, uihunkline) and (
1504 1503 (not item.hunk.folded) or ignorefolding
1505 1504 ):
1506 1505 outstr.append(
1507 1506 self.printhunkchangedline(item, selected, towin=towin)
1508 1507 )
1509 1508
1510 1509 return outstr
1511 1510
1512 1511 def getnumlinesdisplayed(
1513 1512 self, item=None, ignorefolding=False, recursechildren=True
1514 1513 ):
1515 1514 """
1516 1515 return the number of lines which would be displayed if the item were
1517 1516 to be printed to the display. the item will not be printed to the
1518 1517 display (pad).
1519 1518 if no item is given, assume the entire patch.
1520 1519 if ignorefolding is True, folded items will be unfolded when counting
1521 1520 the number of lines.
1522 1521 """
1523 1522
1524 1523 # temporarily disable printing to windows by printstring
1525 1524 patchdisplaystring = self.printitem(
1526 1525 item, ignorefolding, recursechildren, towin=False
1527 1526 )
1528 1527 numlines = len(patchdisplaystring) // self.xscreensize
1529 1528 return numlines
1530 1529
1531 1530 def sigwinchhandler(self, n, frame):
1532 1531 b"handle window resizing"
1533 1532 try:
1534 1533 curses.endwin()
1535 1534 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1536 1535 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1537 1536 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1538 1537 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1539 1538 except curses.error:
1540 1539 pass
1541 1540
1542 1541 def getcolorpair(
1543 1542 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1544 1543 ):
1545 1544 """
1546 1545 get a curses color pair, adding it to self.colorpairs if it is not
1547 1546 already defined. an optional string, name, can be passed as a shortcut
1548 1547 for referring to the color-pair. by default, if no arguments are
1549 1548 specified, the white foreground / black background color-pair is
1550 1549 returned.
1551 1550
1552 1551 it is expected that this function will be used exclusively for
1553 1552 initializing color pairs, and not curses.init_pair().
1554 1553
1555 1554 attrlist is used to 'flavor' the returned color-pair. this information
1556 1555 is not stored in self.colorpairs. it contains attribute values like
1557 1556 curses.A_BOLD.
1558 1557 """
1559 1558
1560 1559 if (name is not None) and name in self.colorpairnames:
1561 1560 # then get the associated color pair and return it
1562 1561 colorpair = self.colorpairnames[name]
1563 1562 else:
1564 1563 if fgcolor is None:
1565 1564 fgcolor = -1
1566 1565 if bgcolor is None:
1567 1566 bgcolor = -1
1568 1567 if (fgcolor, bgcolor) in self.colorpairs:
1569 1568 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1570 1569 else:
1571 1570 pairindex = len(self.colorpairs) + 1
1572 1571 if self.usecolor:
1573 1572 curses.init_pair(pairindex, fgcolor, bgcolor)
1574 1573 colorpair = self.colorpairs[
1575 1574 (fgcolor, bgcolor)
1576 1575 ] = curses.color_pair(pairindex)
1577 1576 if name is not None:
1578 1577 self.colorpairnames[name] = curses.color_pair(pairindex)
1579 1578 else:
1580 1579 cval = 0
1581 1580 if name is not None:
1582 1581 if name == b'selected':
1583 1582 cval = curses.A_REVERSE
1584 1583 self.colorpairnames[name] = cval
1585 1584 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1586 1585
1587 1586 # add attributes if possible
1588 1587 if attrlist is None:
1589 1588 attrlist = []
1590 1589 if colorpair < 256:
1591 1590 # then it is safe to apply all attributes
1592 1591 for textattr in attrlist:
1593 1592 colorpair |= textattr
1594 1593 else:
1595 1594 # just apply a select few (safe?) attributes
1596 1595 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1597 1596 if textattrib in attrlist:
1598 1597 colorpair |= textattrib
1599 1598 return colorpair
1600 1599
1601 1600 def initcolorpair(self, *args, **kwargs):
1602 1601 b"same as getcolorpair."
1603 1602 self.getcolorpair(*args, **kwargs)
1604 1603
1605 1604 def helpwindow(self):
1606 1605 b"print a help window to the screen. exit after any keypress."
1607 1606 helptext = _(
1608 1607 """ [press any key to return to the patch-display]
1609 1608
1610 1609 crecord allows you to interactively choose among the changes you have made,
1611 1610 and confirm only those changes you select for further processing by the command
1612 1611 you are running (commit/shelve/revert), after confirming the selected
1613 1612 changes, the unselected changes are still present in your working copy, so you
1614 1613 can use crecord multiple times to split large changes into smaller changesets.
1615 1614 the following are valid keystrokes:
1616 1615
1617 1616 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1618 1617 [enter] : (un-)select item and go to next item of same type
1619 1618 A : (un-)select all items
1620 1619 X : (un-)select all items between current and most-recent
1621 1620 up/down-arrow [k/j] : go to previous/next unfolded item
1622 1621 pgup/pgdn [K/J] : go to previous/next item of same type
1623 1622 right/left-arrow [l/h] : go to child item / parent item
1624 1623 shift-left-arrow [H] : go to parent header / fold selected header
1625 1624 g : go to the top
1626 1625 G : go to the bottom
1627 1626 f : fold / unfold item, hiding/revealing its children
1628 1627 F : fold / unfold parent item and all of its ancestors
1629 1628 ctrl-l : scroll the selected line to the top of the screen
1630 1629 m : edit / resume editing the commit message
1631 1630 e : edit the currently selected hunk
1632 1631 a : toggle amend mode, only with commit -i
1633 1632 c : confirm selected changes
1634 1633 r : review/edit and confirm selected changes
1635 1634 q : quit without confirming (no changes will be made)
1636 1635 ? : help (what you're currently reading)"""
1637 1636 )
1638 1637
1639 1638 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1640 1639 helplines = helptext.split(b"\n")
1641 1640 helplines = helplines + [b" "] * (
1642 1641 self.yscreensize - self.numstatuslines - len(helplines) - 1
1643 1642 )
1644 1643 try:
1645 1644 for line in helplines:
1646 1645 self.printstring(helpwin, line, pairname=b"legend")
1647 1646 except curses.error:
1648 1647 pass
1649 1648 helpwin.refresh()
1650 1649 try:
1651 1650 with self.ui.timeblockedsection(b'crecord'):
1652 1651 helpwin.getkey()
1653 1652 except curses.error:
1654 1653 pass
1655 1654
1656 1655 def commitMessageWindow(self):
1657 1656 b"Create a temporary commit message editing window on the screen."
1658 1657
1659 1658 curses.raw()
1660 1659 curses.def_prog_mode()
1661 1660 curses.endwin()
1662 1661 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1663 1662 curses.cbreak()
1664 1663 self.stdscr.refresh()
1665 1664 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1666 1665
1667 1666 def handlefirstlineevent(self):
1668 1667 """
1669 1668 Handle 'g' to navigate to the top most file in the ncurses window.
1670 1669 """
1671 1670 self.currentselecteditem = self.headerlist[0]
1672 1671 currentitem = self.currentselecteditem
1673 1672 # select the parent item recursively until we're at a header
1674 1673 while True:
1675 1674 nextitem = currentitem.parentitem()
1676 1675 if nextitem is None:
1677 1676 break
1678 1677 else:
1679 1678 currentitem = nextitem
1680 1679
1681 1680 self.currentselecteditem = currentitem
1682 1681
1683 1682 def handlelastlineevent(self):
1684 1683 """
1685 1684 Handle 'G' to navigate to the bottom most file/hunk/line depending
1686 1685 on the whether the fold is active or not.
1687 1686
1688 1687 If the bottom most file is folded, it navigates to that file and
1689 1688 stops there. If the bottom most file is unfolded, it navigates to
1690 1689 the bottom most hunk in that file and stops there. If the bottom most
1691 1690 hunk is unfolded, it navigates to the bottom most line in that hunk.
1692 1691 """
1693 1692 currentitem = self.currentselecteditem
1694 1693 nextitem = currentitem.nextitem()
1695 1694 # select the child item recursively until we're at a footer
1696 1695 while nextitem is not None:
1697 1696 nextitem = currentitem.nextitem()
1698 1697 if nextitem is None:
1699 1698 break
1700 1699 else:
1701 1700 currentitem = nextitem
1702 1701
1703 1702 self.currentselecteditem = currentitem
1704 1703 self.recenterdisplayedarea()
1705 1704
1706 1705 def confirmationwindow(self, windowtext):
1707 1706 b"display an informational window, then wait for and return a keypress."
1708 1707
1709 1708 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1710 1709 try:
1711 1710 lines = windowtext.split(b"\n")
1712 1711 for line in lines:
1713 1712 self.printstring(confirmwin, line, pairname=b"selected")
1714 1713 except curses.error:
1715 1714 pass
1716 1715 self.stdscr.refresh()
1717 1716 confirmwin.refresh()
1718 1717 try:
1719 1718 with self.ui.timeblockedsection(b'crecord'):
1720 1719 response = chr(self.stdscr.getch())
1721 1720 except ValueError:
1722 1721 response = None
1723 1722
1724 1723 return response
1725 1724
1726 1725 def reviewcommit(self):
1727 1726 """ask for 'y' to be pressed to confirm selected. return True if
1728 1727 confirmed."""
1729 1728 confirmtext = _(
1730 1729 """If you answer yes to the following, your currently chosen patch chunks
1731 1730 will be loaded into an editor. To modify the patch, make the changes in your
1732 1731 editor and save. To accept the current patch as-is, close the editor without
1733 1732 saving.
1734 1733
1735 1734 note: don't add/remove lines unless you also modify the range information.
1736 1735 failing to follow this rule will result in the commit aborting.
1737 1736
1738 1737 are you sure you want to review/edit and confirm the selected changes [yn]?
1739 1738 """
1740 1739 )
1741 1740 with self.ui.timeblockedsection(b'crecord'):
1742 1741 response = self.confirmationwindow(confirmtext)
1743 1742 if response is None:
1744 1743 response = "n"
1745 1744 if response.lower().startswith("y"):
1746 1745 return True
1747 1746 else:
1748 1747 return False
1749 1748
1750 1749 def toggleamend(self, opts, test):
1751 1750 """Toggle the amend flag.
1752 1751
1753 1752 When the amend flag is set, a commit will modify the most recently
1754 1753 committed changeset, instead of creating a new changeset. Otherwise, a
1755 1754 new changeset will be created (the normal commit behavior).
1756 1755 """
1757 1756
1758 1757 if opts.get(b'amend') is None:
1759 1758 opts[b'amend'] = True
1760 1759 msg = _(
1761 1760 b"Amend option is turned on -- committing the currently "
1762 1761 b"selected changes will not create a new changeset, but "
1763 1762 b"instead update the most recently committed changeset.\n\n"
1764 1763 b"Press any key to continue."
1765 1764 )
1766 1765 elif opts.get(b'amend') is True:
1767 1766 opts[b'amend'] = None
1768 1767 msg = _(
1769 1768 b"Amend option is turned off -- committing the currently "
1770 1769 b"selected changes will create a new changeset.\n\n"
1771 1770 b"Press any key to continue."
1772 1771 )
1773 1772 if not test:
1774 1773 self.confirmationwindow(msg)
1775 1774
1776 1775 def recenterdisplayedarea(self):
1777 1776 """
1778 1777 once we scrolled with pg up pg down we can be pointing outside of the
1779 1778 display zone. we print the patch with towin=False to compute the
1780 1779 location of the selected item even though it is outside of the displayed
1781 1780 zone and then update the scroll.
1782 1781 """
1783 1782 self.printitem(towin=False)
1784 1783 self.updatescroll()
1785 1784
1786 1785 def toggleedit(self, item=None, test=False):
1787 1786 """
1788 1787 edit the currently selected chunk
1789 1788 """
1790 1789
1791 1790 def updateui(self):
1792 1791 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1793 1792 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1794 1793 self.updatescroll()
1795 1794 self.stdscr.refresh()
1796 1795 self.statuswin.refresh()
1797 1796 self.stdscr.keypad(1)
1798 1797
1799 1798 def editpatchwitheditor(self, chunk):
1800 1799 if chunk is None:
1801 1800 self.ui.write(_(b'cannot edit patch for whole file'))
1802 1801 self.ui.write(b"\n")
1803 1802 return None
1804 1803 if chunk.header.binary():
1805 1804 self.ui.write(_(b'cannot edit patch for binary file'))
1806 1805 self.ui.write(b"\n")
1807 1806 return None
1808 1807
1809 1808 # write the initial patch
1810 1809 patch = stringio()
1811 1810 patch.write(diffhelptext + hunkhelptext)
1812 1811 chunk.header.write(patch)
1813 1812 chunk.write(patch)
1814 1813
1815 1814 # start the editor and wait for it to complete
1816 1815 try:
1817 1816 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1818 1817 except error.Abort as exc:
1819 1818 self.errorstr = stringutil.forcebytestr(exc)
1820 1819 return None
1821 1820 finally:
1822 1821 self.stdscr.clear()
1823 1822 self.stdscr.refresh()
1824 1823
1825 1824 # remove comment lines
1826 1825 patch = [
1827 1826 line + b'\n'
1828 1827 for line in patch.splitlines()
1829 1828 if not line.startswith(b'#')
1830 1829 ]
1831 1830 return patchmod.parsepatch(patch)
1832 1831
1833 1832 if item is None:
1834 1833 item = self.currentselecteditem
1835 1834 if isinstance(item, uiheader):
1836 1835 return
1837 1836 if isinstance(item, uihunkline):
1838 1837 item = item.parentitem()
1839 1838 if not isinstance(item, uihunk):
1840 1839 return
1841 1840
1842 1841 # To go back to that hunk or its replacement at the end of the edit
1843 1842 itemindex = item.parentitem().hunks.index(item)
1844 1843
1845 1844 beforeadded, beforeremoved = item.added, item.removed
1846 1845 newpatches = editpatchwitheditor(self, item)
1847 1846 if newpatches is None:
1848 1847 if not test:
1849 1848 updateui(self)
1850 1849 return
1851 1850 header = item.header
1852 1851 editedhunkindex = header.hunks.index(item)
1853 1852 hunksbefore = header.hunks[:editedhunkindex]
1854 1853 hunksafter = header.hunks[editedhunkindex + 1 :]
1855 1854 newpatchheader = newpatches[0]
1856 1855 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1857 1856 newadded = sum([h.added for h in newhunks])
1858 1857 newremoved = sum([h.removed for h in newhunks])
1859 1858 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1860 1859
1861 1860 for h in hunksafter:
1862 1861 h.toline += offset
1863 1862 for h in newhunks:
1864 1863 h.folded = False
1865 1864 header.hunks = hunksbefore + newhunks + hunksafter
1866 1865 if self.emptypatch():
1867 1866 header.hunks = hunksbefore + [item] + hunksafter
1868 1867 self.currentselecteditem = header
1869 1868 if len(header.hunks) > itemindex:
1870 1869 self.currentselecteditem = header.hunks[itemindex]
1871 1870
1872 1871 if not test:
1873 1872 updateui(self)
1874 1873
1875 1874 def emptypatch(self):
1876 1875 item = self.headerlist
1877 1876 if not item:
1878 1877 return True
1879 1878 for header in item:
1880 1879 if header.hunks:
1881 1880 return False
1882 1881 return True
1883 1882
1884 1883 def handlekeypressed(self, keypressed, test=False):
1885 1884 """
1886 1885 Perform actions based on pressed keys.
1887 1886
1888 1887 Return true to exit the main loop.
1889 1888 """
1890 keypressed = pycompat.bytestr(keypressed)
1891 if keypressed in [b"k", b"KEY_UP"]:
1889 if keypressed in ["k", "KEY_UP"]:
1892 1890 self.uparrowevent()
1893 elif keypressed in [b"K", b"KEY_PPAGE"]:
1891 elif keypressed in ["K", "KEY_PPAGE"]:
1894 1892 self.uparrowshiftevent()
1895 elif keypressed in [b"j", b"KEY_DOWN"]:
1893 elif keypressed in ["j", "KEY_DOWN"]:
1896 1894 self.downarrowevent()
1897 elif keypressed in [b"J", b"KEY_NPAGE"]:
1895 elif keypressed in ["J", "KEY_NPAGE"]:
1898 1896 self.downarrowshiftevent()
1899 elif keypressed in [b"l", b"KEY_RIGHT"]:
1897 elif keypressed in ["l", "KEY_RIGHT"]:
1900 1898 self.rightarrowevent()
1901 elif keypressed in [b"h", b"KEY_LEFT"]:
1899 elif keypressed in ["h", "KEY_LEFT"]:
1902 1900 self.leftarrowevent()
1903 elif keypressed in [b"H", b"KEY_SLEFT"]:
1901 elif keypressed in ["H", "KEY_SLEFT"]:
1904 1902 self.leftarrowshiftevent()
1905 elif keypressed in [b"q"]:
1903 elif keypressed in ["q"]:
1906 1904 raise error.Abort(_(b'user quit'))
1907 elif keypressed in [b'a']:
1905 elif keypressed in ['a']:
1908 1906 self.toggleamend(self.opts, test)
1909 elif keypressed in [b"c"]:
1907 elif keypressed in ["c"]:
1910 1908 return True
1911 elif keypressed in [b"r"]:
1909 elif keypressed in ["r"]:
1912 1910 if self.reviewcommit():
1913 1911 self.opts[b'review'] = True
1914 1912 return True
1915 elif test and keypressed in [b'R']:
1913 elif test and keypressed in ["R"]:
1916 1914 self.opts[b'review'] = True
1917 1915 return True
1918 elif keypressed in [b' ', b'x']:
1916 elif keypressed in [" ", "x"]:
1919 1917 self.toggleapply()
1920 elif keypressed in [b'\n', b'KEY_ENTER']:
1918 elif keypressed in ["\n", "KEY_ENTER"]:
1921 1919 self.toggleapply()
1922 1920 self.nextsametype(test=test)
1923 elif keypressed in [b'X']:
1921 elif keypressed in ["X"]:
1924 1922 self.toggleallbetween()
1925 elif keypressed in [b'A']:
1923 elif keypressed in ["A"]:
1926 1924 self.toggleall()
1927 elif keypressed in [b'e']:
1925 elif keypressed in ["e"]:
1928 1926 self.toggleedit(test=test)
1929 elif keypressed in [b"f"]:
1927 elif keypressed in ["f"]:
1930 1928 self.togglefolded()
1931 elif keypressed in [b"F"]:
1929 elif keypressed in ["F"]:
1932 1930 self.togglefolded(foldparent=True)
1933 elif keypressed in [b"m"]:
1931 elif keypressed in ["m"]:
1934 1932 self.commitMessageWindow()
1935 elif keypressed in [b"g", b"KEY_HOME"]:
1933 elif keypressed in ["g", "KEY_HOME"]:
1936 1934 self.handlefirstlineevent()
1937 elif keypressed in [b"G", b"KEY_END"]:
1935 elif keypressed in ["G", "KEY_END"]:
1938 1936 self.handlelastlineevent()
1939 elif keypressed in [b"?"]:
1937 elif keypressed in ["?"]:
1940 1938 self.helpwindow()
1941 1939 self.stdscr.clear()
1942 1940 self.stdscr.refresh()
1943 elif curses.unctrl(keypressed) in [b"^L"]:
1941 elif curses.unctrl(keypressed) in ["^L"]:
1944 1942 # scroll the current line to the top of the screen, and redraw
1945 1943 # everything
1946 1944 self.scrolllines(self.selecteditemstartline)
1947 1945 self.stdscr.clear()
1948 1946 self.stdscr.refresh()
1949 1947
1950 1948 def main(self, stdscr):
1951 1949 """
1952 1950 method to be wrapped by curses.wrapper() for selecting chunks.
1953 1951 """
1954 1952
1955 1953 origsigwinch = sentinel = object()
1956 1954 if util.safehasattr(signal, b'SIGWINCH'):
1957 1955 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1958 1956 try:
1959 1957 return self._main(stdscr)
1960 1958 finally:
1961 1959 if origsigwinch is not sentinel:
1962 1960 signal.signal(signal.SIGWINCH, origsigwinch)
1963 1961
1964 1962 def _main(self, stdscr):
1965 1963 self.stdscr = stdscr
1966 1964 # error during initialization, cannot be printed in the curses
1967 1965 # interface, it should be printed by the calling code
1968 1966 self.initexc = None
1969 1967 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1970 1968
1971 1969 curses.start_color()
1972 1970 try:
1973 1971 curses.use_default_colors()
1974 1972 except curses.error:
1975 1973 self.usecolor = False
1976 1974
1977 1975 # In some situations we may have some cruft left on the "alternate
1978 1976 # screen" from another program (or previous iterations of ourself), and
1979 1977 # we won't clear it if the scroll region is small enough to comfortably
1980 1978 # fit on the terminal.
1981 1979 self.stdscr.clear()
1982 1980
1983 1981 # don't display the cursor
1984 1982 try:
1985 1983 curses.curs_set(0)
1986 1984 except curses.error:
1987 1985 pass
1988 1986
1989 1987 # available colors: black, blue, cyan, green, magenta, white, yellow
1990 1988 # init_pair(color_id, foreground_color, background_color)
1991 1989 self.initcolorpair(None, None, name=b"normal")
1992 1990 self.initcolorpair(
1993 1991 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1994 1992 )
1995 1993 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1996 1994 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1997 1995 self.initcolorpair(
1998 1996 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1999 1997 )
2000 1998 # newwin([height, width,] begin_y, begin_x)
2001 1999 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
2002 2000 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
2003 2001
2004 2002 # figure out how much space to allocate for the chunk-pad which is
2005 2003 # used for displaying the patch
2006 2004
2007 2005 # stupid hack to prevent getnumlinesdisplayed from failing
2008 2006 self.chunkpad = curses.newpad(1, self.xscreensize)
2009 2007
2010 2008 # add 1 so to account for last line text reaching end of line
2011 2009 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2012 2010
2013 2011 try:
2014 2012 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2015 2013 except curses.error:
2016 2014 self.initexc = fallbackerror(
2017 2015 _(b'this diff is too large to be displayed')
2018 2016 )
2019 2017 return
2020 2018 # initialize selecteditemendline (initial start-line is 0)
2021 2019 self.selecteditemendline = self.getnumlinesdisplayed(
2022 2020 self.currentselecteditem, recursechildren=False
2023 2021 )
2024 2022
2025 2023 while True:
2026 2024 self.updatescreen()
2027 2025 try:
2028 2026 with self.ui.timeblockedsection(b'crecord'):
2029 2027 keypressed = self.statuswin.getkey()
2030 2028 if self.errorstr is not None:
2031 2029 self.errorstr = None
2032 2030 continue
2033 2031 except curses.error:
2034 2032 keypressed = b"foobar"
2035 2033 if self.handlekeypressed(keypressed):
2036 2034 break
2037 2035
2038 2036 if self.commenttext != b"":
2039 2037 whitespaceremoved = re.sub(
2040 2038 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2041 2039 )
2042 2040 if whitespaceremoved != b"":
2043 2041 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now