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