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