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