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