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