##// END OF EJS Templates
crecord: render chunkpad on Windows (issue6427)...
Barret Rennie -
r46415:91c41ea1 stable
parent child Browse files
Show More
@@ -1,2034 +1,2034 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 """# 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 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, 'r')
615 615 testcommands = [x.rstrip('\n') for x in testf.readlines()]
616 616 testf.close()
617 617 while True:
618 618 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
619 619 break
620 620 return chunkselector.opts
621 621
622 622
623 623 _headermessages = { # {operation: text}
624 624 b'apply': _(b'Select hunks to apply'),
625 625 b'discard': _(b'Select hunks to discard'),
626 626 b'keep': _(b'Select hunks to keep'),
627 627 None: _(b'Select hunks to record'),
628 628 }
629 629
630 630
631 631 class curseschunkselector(object):
632 632 def __init__(self, headerlist, ui, operation=None):
633 633 # put the headers into a patch object
634 634 self.headerlist = patch(headerlist)
635 635
636 636 self.ui = ui
637 637 self.opts = {}
638 638
639 639 self.errorstr = None
640 640 # list of all chunks
641 641 self.chunklist = []
642 642 for h in headerlist:
643 643 self.chunklist.append(h)
644 644 self.chunklist.extend(h.hunks)
645 645
646 646 # dictionary mapping (fgcolor, bgcolor) pairs to the
647 647 # corresponding curses color-pair value.
648 648 self.colorpairs = {}
649 649 # maps custom nicknames of color-pairs to curses color-pair values
650 650 self.colorpairnames = {}
651 651
652 652 # Honor color setting of ui section. Keep colored setup as
653 653 # long as not explicitly set to a falsy value - especially,
654 654 # when not set at all. This is to stay most compatible with
655 655 # previous (color only) behaviour.
656 656 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
657 657 self.usecolor = uicolor is not False
658 658
659 659 # the currently selected header, hunk, or hunk-line
660 660 self.currentselecteditem = self.headerlist[0]
661 661 self.lastapplieditem = None
662 662
663 663 # updated when printing out patch-display -- the 'lines' here are the
664 664 # line positions *in the pad*, not on the screen.
665 665 self.selecteditemstartline = 0
666 666 self.selecteditemendline = None
667 667
668 668 # define indentation levels
669 669 self.headerindentnumchars = 0
670 670 self.hunkindentnumchars = 3
671 671 self.hunklineindentnumchars = 6
672 672
673 673 # the first line of the pad to print to the screen
674 674 self.firstlineofpadtoprint = 0
675 675
676 676 # keeps track of the number of lines in the pad
677 677 self.numpadlines = None
678 678
679 679 self.numstatuslines = 1
680 680
681 681 # keep a running count of the number of lines printed to the pad
682 682 # (used for determining when the selected item begins/ends)
683 683 self.linesprintedtopadsofar = 0
684 684
685 685 # stores optional text for a commit comment provided by the user
686 686 self.commenttext = b""
687 687
688 688 # if the last 'toggle all' command caused all changes to be applied
689 689 self.waslasttoggleallapplied = True
690 690
691 691 # affects some ui text
692 692 if operation not in _headermessages:
693 693 raise error.ProgrammingError(
694 694 b'unexpected operation: %s' % operation
695 695 )
696 696 self.operation = operation
697 697
698 698 def uparrowevent(self):
699 699 """
700 700 try to select the previous item to the current item that has the
701 701 most-indented level. for example, if a hunk is selected, try to select
702 702 the last hunkline of the hunk prior to the selected hunk. or, if
703 703 the first hunkline of a hunk is currently selected, then select the
704 704 hunk itself.
705 705 """
706 706 currentitem = self.currentselecteditem
707 707
708 708 nextitem = currentitem.previtem()
709 709
710 710 if nextitem is None:
711 711 # if no parent item (i.e. currentitem is the first header), then
712 712 # no change...
713 713 nextitem = currentitem
714 714
715 715 self.currentselecteditem = nextitem
716 716
717 717 def uparrowshiftevent(self):
718 718 """
719 719 select (if possible) the previous item on the same level as the
720 720 currently selected item. otherwise, select (if possible) the
721 721 parent-item of the currently selected item.
722 722 """
723 723 currentitem = self.currentselecteditem
724 724 nextitem = currentitem.prevsibling()
725 725 # if there's no previous sibling, try choosing the parent
726 726 if nextitem is None:
727 727 nextitem = currentitem.parentitem()
728 728 if nextitem is None:
729 729 # if no parent item (i.e. currentitem is the first header), then
730 730 # no change...
731 731 nextitem = currentitem
732 732
733 733 self.currentselecteditem = nextitem
734 734 self.recenterdisplayedarea()
735 735
736 736 def downarrowevent(self):
737 737 """
738 738 try to select the next item to the current item that has the
739 739 most-indented level. for example, if a hunk is selected, select
740 740 the first hunkline of the selected hunk. or, if the last hunkline of
741 741 a hunk is currently selected, then select the next hunk, if one exists,
742 742 or if not, the next header if one exists.
743 743 """
744 744 # self.startprintline += 1 #debug
745 745 currentitem = self.currentselecteditem
746 746
747 747 nextitem = currentitem.nextitem()
748 748 # if there's no next item, keep the selection as-is
749 749 if nextitem is None:
750 750 nextitem = currentitem
751 751
752 752 self.currentselecteditem = nextitem
753 753
754 754 def downarrowshiftevent(self):
755 755 """
756 756 select (if possible) the next item on the same level as the currently
757 757 selected item. otherwise, select (if possible) the next item on the
758 758 same level as the parent item of the currently selected item.
759 759 """
760 760 currentitem = self.currentselecteditem
761 761 nextitem = currentitem.nextsibling()
762 762 # if there's no next sibling, try choosing the parent's nextsibling
763 763 if nextitem is None:
764 764 try:
765 765 nextitem = currentitem.parentitem().nextsibling()
766 766 except AttributeError:
767 767 # parentitem returned None, so nextsibling() can't be called
768 768 nextitem = None
769 769 if nextitem is None:
770 770 # if parent has no next sibling, then no change...
771 771 nextitem = currentitem
772 772
773 773 self.currentselecteditem = nextitem
774 774 self.recenterdisplayedarea()
775 775
776 776 def nextsametype(self, test=False):
777 777 currentitem = self.currentselecteditem
778 778 sametype = lambda item: isinstance(item, type(currentitem))
779 779 nextitem = currentitem.nextitem()
780 780
781 781 while nextitem is not None and not sametype(nextitem):
782 782 nextitem = nextitem.nextitem()
783 783
784 784 if nextitem is None:
785 785 nextitem = currentitem
786 786 else:
787 787 parent = nextitem.parentitem()
788 788 if parent is not None and parent.folded:
789 789 self.togglefolded(parent)
790 790
791 791 self.currentselecteditem = nextitem
792 792 if not test:
793 793 self.recenterdisplayedarea()
794 794
795 795 def rightarrowevent(self):
796 796 """
797 797 select (if possible) the first of this item's child-items.
798 798 """
799 799 currentitem = self.currentselecteditem
800 800 nextitem = currentitem.firstchild()
801 801
802 802 # turn off folding if we want to show a child-item
803 803 if currentitem.folded:
804 804 self.togglefolded(currentitem)
805 805
806 806 if nextitem is None:
807 807 # if no next item on parent-level, then no change...
808 808 nextitem = currentitem
809 809
810 810 self.currentselecteditem = nextitem
811 811
812 812 def leftarrowevent(self):
813 813 """
814 814 if the current item can be folded (i.e. it is an unfolded header or
815 815 hunk), then fold it. otherwise try select (if possible) the parent
816 816 of this item.
817 817 """
818 818 currentitem = self.currentselecteditem
819 819
820 820 # try to fold the item
821 821 if not isinstance(currentitem, uihunkline):
822 822 if not currentitem.folded:
823 823 self.togglefolded(item=currentitem)
824 824 return
825 825
826 826 # if it can't be folded, try to select the parent item
827 827 nextitem = currentitem.parentitem()
828 828
829 829 if nextitem is None:
830 830 # if no item on parent-level, then no change...
831 831 nextitem = currentitem
832 832 if not nextitem.folded:
833 833 self.togglefolded(item=nextitem)
834 834
835 835 self.currentselecteditem = nextitem
836 836
837 837 def leftarrowshiftevent(self):
838 838 """
839 839 select the header of the current item (or fold current item if the
840 840 current item is already a header).
841 841 """
842 842 currentitem = self.currentselecteditem
843 843
844 844 if isinstance(currentitem, uiheader):
845 845 if not currentitem.folded:
846 846 self.togglefolded(item=currentitem)
847 847 return
848 848
849 849 # select the parent item recursively until we're at a header
850 850 while True:
851 851 nextitem = currentitem.parentitem()
852 852 if nextitem is None:
853 853 break
854 854 else:
855 855 currentitem = nextitem
856 856
857 857 self.currentselecteditem = currentitem
858 858
859 859 def updatescroll(self):
860 860 """scroll the screen to fully show the currently-selected"""
861 861 selstart = self.selecteditemstartline
862 862 selend = self.selecteditemendline
863 863
864 864 padstart = self.firstlineofpadtoprint
865 865 padend = padstart + self.yscreensize - self.numstatuslines - 1
866 866 # 'buffered' pad start/end values which scroll with a certain
867 867 # top/bottom context margin
868 868 padstartbuffered = padstart + 3
869 869 padendbuffered = padend - 3
870 870
871 871 if selend > padendbuffered:
872 872 self.scrolllines(selend - padendbuffered)
873 873 elif selstart < padstartbuffered:
874 874 # negative values scroll in pgup direction
875 875 self.scrolllines(selstart - padstartbuffered)
876 876
877 877 def scrolllines(self, numlines):
878 878 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
879 879 self.firstlineofpadtoprint += numlines
880 880 if self.firstlineofpadtoprint < 0:
881 881 self.firstlineofpadtoprint = 0
882 882 if self.firstlineofpadtoprint > self.numpadlines - 1:
883 883 self.firstlineofpadtoprint = self.numpadlines - 1
884 884
885 885 def toggleapply(self, item=None):
886 886 """
887 887 toggle the applied flag of the specified item. if no item is specified,
888 888 toggle the flag of the currently selected item.
889 889 """
890 890 if item is None:
891 891 item = self.currentselecteditem
892 892 # Only set this when NOT using 'toggleall'
893 893 self.lastapplieditem = item
894 894
895 895 item.applied = not item.applied
896 896
897 897 if isinstance(item, uiheader):
898 898 item.partial = False
899 899 if item.applied:
900 900 # apply all its hunks
901 901 for hnk in item.hunks:
902 902 hnk.applied = True
903 903 # apply all their hunklines
904 904 for hunkline in hnk.changedlines:
905 905 hunkline.applied = True
906 906 else:
907 907 # un-apply all its hunks
908 908 for hnk in item.hunks:
909 909 hnk.applied = False
910 910 hnk.partial = False
911 911 # un-apply all their hunklines
912 912 for hunkline in hnk.changedlines:
913 913 hunkline.applied = False
914 914 elif isinstance(item, uihunk):
915 915 item.partial = False
916 916 # apply all it's hunklines
917 917 for hunkline in item.changedlines:
918 918 hunkline.applied = item.applied
919 919
920 920 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
921 921 allsiblingsapplied = not (False in siblingappliedstatus)
922 922 nosiblingsapplied = not (True in siblingappliedstatus)
923 923
924 924 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
925 925 somesiblingspartial = True in siblingspartialstatus
926 926
927 927 # cases where applied or partial should be removed from header
928 928
929 929 # if no 'sibling' hunks are applied (including this hunk)
930 930 if nosiblingsapplied:
931 931 if not item.header.special():
932 932 item.header.applied = False
933 933 item.header.partial = False
934 934 else: # some/all parent siblings are applied
935 935 item.header.applied = True
936 936 item.header.partial = (
937 937 somesiblingspartial or not allsiblingsapplied
938 938 )
939 939
940 940 elif isinstance(item, uihunkline):
941 941 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
942 942 allsiblingsapplied = not (False in siblingappliedstatus)
943 943 nosiblingsapplied = not (True in siblingappliedstatus)
944 944
945 945 # if no 'sibling' lines are applied
946 946 if nosiblingsapplied:
947 947 item.hunk.applied = False
948 948 item.hunk.partial = False
949 949 elif allsiblingsapplied:
950 950 item.hunk.applied = True
951 951 item.hunk.partial = False
952 952 else: # some siblings applied
953 953 item.hunk.applied = True
954 954 item.hunk.partial = True
955 955
956 956 parentsiblingsapplied = [
957 957 hnk.applied for hnk in item.hunk.header.hunks
958 958 ]
959 959 noparentsiblingsapplied = not (True in parentsiblingsapplied)
960 960 allparentsiblingsapplied = not (False in parentsiblingsapplied)
961 961
962 962 parentsiblingspartial = [
963 963 hnk.partial for hnk in item.hunk.header.hunks
964 964 ]
965 965 someparentsiblingspartial = True in parentsiblingspartial
966 966
967 967 # if all parent hunks are not applied, un-apply header
968 968 if noparentsiblingsapplied:
969 969 if not item.hunk.header.special():
970 970 item.hunk.header.applied = False
971 971 item.hunk.header.partial = False
972 972 # set the applied and partial status of the header if needed
973 973 else: # some/all parent siblings are applied
974 974 item.hunk.header.applied = True
975 975 item.hunk.header.partial = (
976 976 someparentsiblingspartial or not allparentsiblingsapplied
977 977 )
978 978
979 979 def toggleall(self):
980 980 """toggle the applied flag of all items."""
981 981 if self.waslasttoggleallapplied: # then unapply them this time
982 982 for item in self.headerlist:
983 983 if item.applied:
984 984 self.toggleapply(item)
985 985 else:
986 986 for item in self.headerlist:
987 987 if not item.applied:
988 988 self.toggleapply(item)
989 989 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
990 990
991 991 def flipselections(self):
992 992 """
993 993 Flip all selections. Every selected line is unselected and vice
994 994 versa.
995 995 """
996 996 for header in self.headerlist:
997 997 for hunk in header.allchildren():
998 998 for line in hunk.allchildren():
999 999 self.toggleapply(line)
1000 1000
1001 1001 def toggleallbetween(self):
1002 1002 """toggle applied on or off for all items in range [lastapplied,
1003 1003 current]. """
1004 1004 if (
1005 1005 not self.lastapplieditem
1006 1006 or self.currentselecteditem == self.lastapplieditem
1007 1007 ):
1008 1008 # Treat this like a normal 'x'/' '
1009 1009 self.toggleapply()
1010 1010 return
1011 1011
1012 1012 startitem = self.lastapplieditem
1013 1013 enditem = self.currentselecteditem
1014 1014 # Verify that enditem is "after" startitem, otherwise swap them.
1015 1015 for direction in [b'forward', b'reverse']:
1016 1016 nextitem = startitem.nextitem()
1017 1017 while nextitem and nextitem != enditem:
1018 1018 nextitem = nextitem.nextitem()
1019 1019 if nextitem:
1020 1020 break
1021 1021 # Looks like we went the wrong direction :)
1022 1022 startitem, enditem = enditem, startitem
1023 1023
1024 1024 if not nextitem:
1025 1025 # We didn't find a path going either forward or backward? Don't know
1026 1026 # how this can happen, let's not crash though.
1027 1027 return
1028 1028
1029 1029 nextitem = startitem
1030 1030 # Switch all items to be the opposite state of the currently selected
1031 1031 # item. Specifically:
1032 1032 # [ ] startitem
1033 1033 # [x] middleitem
1034 1034 # [ ] enditem <-- currently selected
1035 1035 # This will turn all three on, since the currently selected item is off.
1036 1036 # This does *not* invert each item (i.e. middleitem stays marked/on)
1037 1037 desiredstate = not self.currentselecteditem.applied
1038 1038 while nextitem != enditem.nextitem():
1039 1039 if nextitem.applied != desiredstate:
1040 1040 self.toggleapply(item=nextitem)
1041 1041 nextitem = nextitem.nextitem()
1042 1042
1043 1043 def togglefolded(self, item=None, foldparent=False):
1044 1044 """toggle folded flag of specified item (defaults to currently
1045 1045 selected)"""
1046 1046 if item is None:
1047 1047 item = self.currentselecteditem
1048 1048 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1049 1049 if not isinstance(item, uiheader):
1050 1050 # we need to select the parent item in this case
1051 1051 self.currentselecteditem = item = item.parentitem()
1052 1052 elif item.neverunfolded:
1053 1053 item.neverunfolded = False
1054 1054
1055 1055 # also fold any foldable children of the parent/current item
1056 1056 if isinstance(item, uiheader): # the original or 'new' item
1057 1057 for child in item.allchildren():
1058 1058 child.folded = not item.folded
1059 1059
1060 1060 if isinstance(item, (uiheader, uihunk)):
1061 1061 item.folded = not item.folded
1062 1062
1063 1063 def alignstring(self, instr, window):
1064 1064 """
1065 1065 add whitespace to the end of a string in order to make it fill
1066 1066 the screen in the x direction. the current cursor position is
1067 1067 taken into account when making this calculation. the string can span
1068 1068 multiple lines.
1069 1069 """
1070 1070 y, xstart = window.getyx()
1071 1071 width = self.xscreensize
1072 1072 # turn tabs into spaces
1073 1073 instr = instr.expandtabs(4)
1074 1074 strwidth = encoding.colwidth(instr)
1075 1075 numspaces = width - ((strwidth + xstart) % width)
1076 1076 return instr + b" " * numspaces
1077 1077
1078 1078 def printstring(
1079 1079 self,
1080 1080 window,
1081 1081 text,
1082 1082 fgcolor=None,
1083 1083 bgcolor=None,
1084 1084 pair=None,
1085 1085 pairname=None,
1086 1086 attrlist=None,
1087 1087 towin=True,
1088 1088 align=True,
1089 1089 showwhtspc=False,
1090 1090 ):
1091 1091 """
1092 1092 print the string, text, with the specified colors and attributes, to
1093 1093 the specified curses window object.
1094 1094
1095 1095 the foreground and background colors are of the form
1096 1096 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1097 1097 magenta, red, white, yellow]. if pairname is provided, a color
1098 1098 pair will be looked up in the self.colorpairnames dictionary.
1099 1099
1100 1100 attrlist is a list containing text attributes in the form of
1101 1101 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1102 1102 underline].
1103 1103
1104 1104 if align == True, whitespace is added to the printed string such that
1105 1105 the string stretches to the right border of the window.
1106 1106
1107 1107 if showwhtspc == True, trailing whitespace of a string is highlighted.
1108 1108 """
1109 1109 # preprocess the text, converting tabs to spaces
1110 1110 text = text.expandtabs(4)
1111 1111 # strip \n, and convert control characters to ^[char] representation
1112 1112 text = re.sub(
1113 1113 br'[\x00-\x08\x0a-\x1f]',
1114 1114 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1115 1115 text.strip(b'\n'),
1116 1116 )
1117 1117
1118 1118 if pair is not None:
1119 1119 colorpair = pair
1120 1120 elif pairname is not None:
1121 1121 colorpair = self.colorpairnames[pairname]
1122 1122 else:
1123 1123 if fgcolor is None:
1124 1124 fgcolor = -1
1125 1125 if bgcolor is None:
1126 1126 bgcolor = -1
1127 1127 if (fgcolor, bgcolor) in self.colorpairs:
1128 1128 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1129 1129 else:
1130 1130 colorpair = self.getcolorpair(fgcolor, bgcolor)
1131 1131 # add attributes if possible
1132 1132 if attrlist is None:
1133 1133 attrlist = []
1134 1134 if colorpair < 256:
1135 1135 # then it is safe to apply all attributes
1136 1136 for textattr in attrlist:
1137 1137 colorpair |= textattr
1138 1138 else:
1139 1139 # just apply a select few (safe?) attributes
1140 1140 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1141 1141 if textattr in attrlist:
1142 1142 colorpair |= textattr
1143 1143
1144 1144 y, xstart = self.chunkpad.getyx()
1145 1145 t = b"" # variable for counting lines printed
1146 1146 # if requested, show trailing whitespace
1147 1147 if showwhtspc:
1148 1148 origlen = len(text)
1149 1149 text = text.rstrip(b' \n') # tabs have already been expanded
1150 1150 strippedlen = len(text)
1151 1151 numtrailingspaces = origlen - strippedlen
1152 1152
1153 1153 if towin:
1154 1154 window.addstr(text, colorpair)
1155 1155 t += text
1156 1156
1157 1157 if showwhtspc:
1158 1158 wscolorpair = colorpair | curses.A_REVERSE
1159 1159 if towin:
1160 1160 for i in range(numtrailingspaces):
1161 1161 window.addch(curses.ACS_CKBOARD, wscolorpair)
1162 1162 t += b" " * numtrailingspaces
1163 1163
1164 1164 if align:
1165 1165 if towin:
1166 1166 extrawhitespace = self.alignstring(b"", window)
1167 1167 window.addstr(extrawhitespace, colorpair)
1168 1168 else:
1169 1169 # need to use t, since the x position hasn't incremented
1170 1170 extrawhitespace = self.alignstring(t, window)
1171 1171 t += extrawhitespace
1172 1172
1173 1173 # is reset to 0 at the beginning of printitem()
1174 1174
1175 1175 linesprinted = (xstart + len(t)) // self.xscreensize
1176 1176 self.linesprintedtopadsofar += linesprinted
1177 1177 return t
1178 1178
1179 1179 def _getstatuslinesegments(self):
1180 1180 """-> [str]. return segments"""
1181 1181 selected = self.currentselecteditem.applied
1182 1182 spaceselect = _(b'space/enter: select')
1183 1183 spacedeselect = _(b'space/enter: deselect')
1184 1184 # Format the selected label into a place as long as the longer of the
1185 1185 # two possible labels. This may vary by language.
1186 1186 spacelen = max(len(spaceselect), len(spacedeselect))
1187 1187 selectedlabel = b'%-*s' % (
1188 1188 spacelen,
1189 1189 spacedeselect if selected else spaceselect,
1190 1190 )
1191 1191 segments = [
1192 1192 _headermessages[self.operation],
1193 1193 b'-',
1194 1194 _(b'[x]=selected **=collapsed'),
1195 1195 _(b'c: confirm'),
1196 1196 _(b'q: abort'),
1197 1197 _(b'arrow keys: move/expand/collapse'),
1198 1198 selectedlabel,
1199 1199 _(b'?: help'),
1200 1200 ]
1201 1201 return segments
1202 1202
1203 1203 def _getstatuslines(self):
1204 1204 """() -> [str]. return short help used in the top status window"""
1205 1205 if self.errorstr is not None:
1206 1206 lines = [self.errorstr, _(b'Press any key to continue')]
1207 1207 else:
1208 1208 # wrap segments to lines
1209 1209 segments = self._getstatuslinesegments()
1210 1210 width = self.xscreensize
1211 1211 lines = []
1212 1212 lastwidth = width
1213 1213 for s in segments:
1214 1214 w = encoding.colwidth(s)
1215 1215 sep = b' ' * (1 + (s and s[0] not in b'-['))
1216 1216 if lastwidth + w + len(sep) >= width:
1217 1217 lines.append(s)
1218 1218 lastwidth = w
1219 1219 else:
1220 1220 lines[-1] += sep + s
1221 1221 lastwidth += w + len(sep)
1222 1222 if len(lines) != self.numstatuslines:
1223 1223 self.numstatuslines = len(lines)
1224 1224 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1225 1225 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1226 1226
1227 1227 def updatescreen(self):
1228 1228 self.statuswin.erase()
1229 1229 self.chunkpad.erase()
1230 1230
1231 1231 printstring = self.printstring
1232 1232
1233 1233 # print out the status lines at the top
1234 1234 try:
1235 1235 for line in self._getstatuslines():
1236 1236 printstring(self.statuswin, line, pairname=b"legend")
1237 1237 self.statuswin.refresh()
1238 1238 except curses.error:
1239 1239 pass
1240 1240 if self.errorstr is not None:
1241 1241 return
1242 1242
1243 1243 # print out the patch in the remaining part of the window
1244 1244 try:
1245 1245 self.printitem()
1246 1246 self.updatescroll()
1247 1247 self.chunkpad.refresh(
1248 1248 self.firstlineofpadtoprint,
1249 1249 0,
1250 1250 self.numstatuslines,
1251 1251 0,
1252 1252 self.yscreensize - self.numstatuslines,
1253 self.xscreensize,
1253 self.xscreensize - 1,
1254 1254 )
1255 1255 except curses.error:
1256 1256 pass
1257 1257
1258 1258 def getstatusprefixstring(self, item):
1259 1259 """
1260 1260 create a string to prefix a line with which indicates whether 'item'
1261 1261 is applied and/or folded.
1262 1262 """
1263 1263
1264 1264 # create checkbox string
1265 1265 if item.applied:
1266 1266 if not isinstance(item, uihunkline) and item.partial:
1267 1267 checkbox = b"[~]"
1268 1268 else:
1269 1269 checkbox = b"[x]"
1270 1270 else:
1271 1271 checkbox = b"[ ]"
1272 1272
1273 1273 try:
1274 1274 if item.folded:
1275 1275 checkbox += b"**"
1276 1276 if isinstance(item, uiheader):
1277 1277 # one of "m", "a", or "d" (modified, added, deleted)
1278 1278 filestatus = item.changetype
1279 1279
1280 1280 checkbox += filestatus + b" "
1281 1281 else:
1282 1282 checkbox += b" "
1283 1283 if isinstance(item, uiheader):
1284 1284 # add two more spaces for headers
1285 1285 checkbox += b" "
1286 1286 except AttributeError: # not foldable
1287 1287 checkbox += b" "
1288 1288
1289 1289 return checkbox
1290 1290
1291 1291 def printheader(
1292 1292 self, header, selected=False, towin=True, ignorefolding=False
1293 1293 ):
1294 1294 """
1295 1295 print the header to the pad. if countlines is True, don't print
1296 1296 anything, but just count the number of lines which would be printed.
1297 1297 """
1298 1298
1299 1299 outstr = b""
1300 1300 text = header.prettystr()
1301 1301 chunkindex = self.chunklist.index(header)
1302 1302
1303 1303 if chunkindex != 0 and not header.folded:
1304 1304 # add separating line before headers
1305 1305 outstr += self.printstring(
1306 1306 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1307 1307 )
1308 1308 # select color-pair based on if the header is selected
1309 1309 colorpair = self.getcolorpair(
1310 1310 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1311 1311 )
1312 1312
1313 1313 # print out each line of the chunk, expanding it to screen width
1314 1314
1315 1315 # number of characters to indent lines on this level by
1316 1316 indentnumchars = 0
1317 1317 checkbox = self.getstatusprefixstring(header)
1318 1318 if not header.folded or ignorefolding:
1319 1319 textlist = text.split(b"\n")
1320 1320 linestr = checkbox + textlist[0]
1321 1321 else:
1322 1322 linestr = checkbox + header.filename()
1323 1323 outstr += self.printstring(
1324 1324 self.chunkpad, linestr, pair=colorpair, towin=towin
1325 1325 )
1326 1326 if not header.folded or ignorefolding:
1327 1327 if len(textlist) > 1:
1328 1328 for line in textlist[1:]:
1329 1329 linestr = b" " * (indentnumchars + len(checkbox)) + line
1330 1330 outstr += self.printstring(
1331 1331 self.chunkpad, linestr, pair=colorpair, towin=towin
1332 1332 )
1333 1333
1334 1334 return outstr
1335 1335
1336 1336 def printhunklinesbefore(
1337 1337 self, hunk, selected=False, towin=True, ignorefolding=False
1338 1338 ):
1339 1339 """includes start/end line indicator"""
1340 1340 outstr = b""
1341 1341 # where hunk is in list of siblings
1342 1342 hunkindex = hunk.header.hunks.index(hunk)
1343 1343
1344 1344 if hunkindex != 0:
1345 1345 # add separating line before headers
1346 1346 outstr += self.printstring(
1347 1347 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1348 1348 )
1349 1349
1350 1350 colorpair = self.getcolorpair(
1351 1351 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1352 1352 )
1353 1353
1354 1354 # print out from-to line with checkbox
1355 1355 checkbox = self.getstatusprefixstring(hunk)
1356 1356
1357 1357 lineprefix = b" " * self.hunkindentnumchars + checkbox
1358 1358 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1359 1359
1360 1360 outstr += self.printstring(
1361 1361 self.chunkpad, lineprefix, towin=towin, align=False
1362 1362 ) # add uncolored checkbox/indent
1363 1363 outstr += self.printstring(
1364 1364 self.chunkpad, frtoline, pair=colorpair, towin=towin
1365 1365 )
1366 1366
1367 1367 if hunk.folded and not ignorefolding:
1368 1368 # skip remainder of output
1369 1369 return outstr
1370 1370
1371 1371 # print out lines of the chunk preceeding changed-lines
1372 1372 for line in hunk.before:
1373 1373 linestr = (
1374 1374 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1375 1375 )
1376 1376 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1377 1377
1378 1378 return outstr
1379 1379
1380 1380 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1381 1381 outstr = b""
1382 1382 if hunk.folded and not ignorefolding:
1383 1383 return outstr
1384 1384
1385 1385 # a bit superfluous, but to avoid hard-coding indent amount
1386 1386 checkbox = self.getstatusprefixstring(hunk)
1387 1387 for line in hunk.after:
1388 1388 linestr = (
1389 1389 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1390 1390 )
1391 1391 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1392 1392
1393 1393 return outstr
1394 1394
1395 1395 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1396 1396 outstr = b""
1397 1397 checkbox = self.getstatusprefixstring(hunkline)
1398 1398
1399 1399 linestr = hunkline.prettystr().strip(b"\n")
1400 1400
1401 1401 # select color-pair based on whether line is an addition/removal
1402 1402 if selected:
1403 1403 colorpair = self.getcolorpair(name=b"selected")
1404 1404 elif linestr.startswith(b"+"):
1405 1405 colorpair = self.getcolorpair(name=b"addition")
1406 1406 elif linestr.startswith(b"-"):
1407 1407 colorpair = self.getcolorpair(name=b"deletion")
1408 1408 elif linestr.startswith(b"\\"):
1409 1409 colorpair = self.getcolorpair(name=b"normal")
1410 1410
1411 1411 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1412 1412 outstr += self.printstring(
1413 1413 self.chunkpad, lineprefix, towin=towin, align=False
1414 1414 ) # add uncolored checkbox/indent
1415 1415 outstr += self.printstring(
1416 1416 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1417 1417 )
1418 1418 return outstr
1419 1419
1420 1420 def printitem(
1421 1421 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1422 1422 ):
1423 1423 """
1424 1424 use __printitem() to print the the specified item.applied.
1425 1425 if item is not specified, then print the entire patch.
1426 1426 (hiding folded elements, etc. -- see __printitem() docstring)
1427 1427 """
1428 1428
1429 1429 if item is None:
1430 1430 item = self.headerlist
1431 1431 if recursechildren:
1432 1432 self.linesprintedtopadsofar = 0
1433 1433
1434 1434 outstr = []
1435 1435 self.__printitem(
1436 1436 item, ignorefolding, recursechildren, outstr, towin=towin
1437 1437 )
1438 1438 return b''.join(outstr)
1439 1439
1440 1440 def outofdisplayedarea(self):
1441 1441 y, _ = self.chunkpad.getyx() # cursor location
1442 1442 # * 2 here works but an optimization would be the max number of
1443 1443 # consecutive non selectable lines
1444 1444 # i.e the max number of context line for any hunk in the patch
1445 1445 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1446 1446 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1447 1447 return y < miny or y > maxy
1448 1448
1449 1449 def handleselection(self, item, recursechildren):
1450 1450 selected = item is self.currentselecteditem
1451 1451 if selected and recursechildren:
1452 1452 # assumes line numbering starting from line 0
1453 1453 self.selecteditemstartline = self.linesprintedtopadsofar
1454 1454 selecteditemlines = self.getnumlinesdisplayed(
1455 1455 item, recursechildren=False
1456 1456 )
1457 1457 self.selecteditemendline = (
1458 1458 self.selecteditemstartline + selecteditemlines - 1
1459 1459 )
1460 1460 return selected
1461 1461
1462 1462 def __printitem(
1463 1463 self, item, ignorefolding, recursechildren, outstr, towin=True
1464 1464 ):
1465 1465 """
1466 1466 recursive method for printing out patch/header/hunk/hunk-line data to
1467 1467 screen. also returns a string with all of the content of the displayed
1468 1468 patch (not including coloring, etc.).
1469 1469
1470 1470 if ignorefolding is True, then folded items are printed out.
1471 1471
1472 1472 if recursechildren is False, then only print the item without its
1473 1473 child items.
1474 1474 """
1475 1475
1476 1476 if towin and self.outofdisplayedarea():
1477 1477 return
1478 1478
1479 1479 selected = self.handleselection(item, recursechildren)
1480 1480
1481 1481 # patch object is a list of headers
1482 1482 if isinstance(item, patch):
1483 1483 if recursechildren:
1484 1484 for hdr in item:
1485 1485 self.__printitem(
1486 1486 hdr, ignorefolding, recursechildren, outstr, towin
1487 1487 )
1488 1488 # todo: eliminate all isinstance() calls
1489 1489 if isinstance(item, uiheader):
1490 1490 outstr.append(
1491 1491 self.printheader(
1492 1492 item, selected, towin=towin, ignorefolding=ignorefolding
1493 1493 )
1494 1494 )
1495 1495 if recursechildren:
1496 1496 for hnk in item.hunks:
1497 1497 self.__printitem(
1498 1498 hnk, ignorefolding, recursechildren, outstr, towin
1499 1499 )
1500 1500 elif isinstance(item, uihunk) and (
1501 1501 (not item.header.folded) or ignorefolding
1502 1502 ):
1503 1503 # print the hunk data which comes before the changed-lines
1504 1504 outstr.append(
1505 1505 self.printhunklinesbefore(
1506 1506 item, selected, towin=towin, ignorefolding=ignorefolding
1507 1507 )
1508 1508 )
1509 1509 if recursechildren:
1510 1510 for l in item.changedlines:
1511 1511 self.__printitem(
1512 1512 l, ignorefolding, recursechildren, outstr, towin
1513 1513 )
1514 1514 outstr.append(
1515 1515 self.printhunklinesafter(
1516 1516 item, towin=towin, ignorefolding=ignorefolding
1517 1517 )
1518 1518 )
1519 1519 elif isinstance(item, uihunkline) and (
1520 1520 (not item.hunk.folded) or ignorefolding
1521 1521 ):
1522 1522 outstr.append(
1523 1523 self.printhunkchangedline(item, selected, towin=towin)
1524 1524 )
1525 1525
1526 1526 return outstr
1527 1527
1528 1528 def getnumlinesdisplayed(
1529 1529 self, item=None, ignorefolding=False, recursechildren=True
1530 1530 ):
1531 1531 """
1532 1532 return the number of lines which would be displayed if the item were
1533 1533 to be printed to the display. the item will not be printed to the
1534 1534 display (pad).
1535 1535 if no item is given, assume the entire patch.
1536 1536 if ignorefolding is True, folded items will be unfolded when counting
1537 1537 the number of lines.
1538 1538 """
1539 1539
1540 1540 # temporarily disable printing to windows by printstring
1541 1541 patchdisplaystring = self.printitem(
1542 1542 item, ignorefolding, recursechildren, towin=False
1543 1543 )
1544 1544 numlines = len(patchdisplaystring) // self.xscreensize
1545 1545 return numlines
1546 1546
1547 1547 def sigwinchhandler(self, n, frame):
1548 1548 """handle window resizing"""
1549 1549 try:
1550 1550 curses.endwin()
1551 1551 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1552 1552 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1553 1553 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1554 1554 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1555 1555 except curses.error:
1556 1556 pass
1557 1557
1558 1558 def getcolorpair(
1559 1559 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1560 1560 ):
1561 1561 """
1562 1562 get a curses color pair, adding it to self.colorpairs if it is not
1563 1563 already defined. an optional string, name, can be passed as a shortcut
1564 1564 for referring to the color-pair. by default, if no arguments are
1565 1565 specified, the white foreground / black background color-pair is
1566 1566 returned.
1567 1567
1568 1568 it is expected that this function will be used exclusively for
1569 1569 initializing color pairs, and not curses.init_pair().
1570 1570
1571 1571 attrlist is used to 'flavor' the returned color-pair. this information
1572 1572 is not stored in self.colorpairs. it contains attribute values like
1573 1573 curses.A_BOLD.
1574 1574 """
1575 1575
1576 1576 if (name is not None) and name in self.colorpairnames:
1577 1577 # then get the associated color pair and return it
1578 1578 colorpair = self.colorpairnames[name]
1579 1579 else:
1580 1580 if fgcolor is None:
1581 1581 fgcolor = -1
1582 1582 if bgcolor is None:
1583 1583 bgcolor = -1
1584 1584 if (fgcolor, bgcolor) in self.colorpairs:
1585 1585 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1586 1586 else:
1587 1587 pairindex = len(self.colorpairs) + 1
1588 1588 if self.usecolor:
1589 1589 curses.init_pair(pairindex, fgcolor, bgcolor)
1590 1590 colorpair = self.colorpairs[
1591 1591 (fgcolor, bgcolor)
1592 1592 ] = curses.color_pair(pairindex)
1593 1593 if name is not None:
1594 1594 self.colorpairnames[name] = curses.color_pair(pairindex)
1595 1595 else:
1596 1596 cval = 0
1597 1597 if name is not None:
1598 1598 if name == b'selected':
1599 1599 cval = curses.A_REVERSE
1600 1600 self.colorpairnames[name] = cval
1601 1601 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1602 1602
1603 1603 # add attributes if possible
1604 1604 if attrlist is None:
1605 1605 attrlist = []
1606 1606 if colorpair < 256:
1607 1607 # then it is safe to apply all attributes
1608 1608 for textattr in attrlist:
1609 1609 colorpair |= textattr
1610 1610 else:
1611 1611 # just apply a select few (safe?) attributes
1612 1612 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1613 1613 if textattrib in attrlist:
1614 1614 colorpair |= textattrib
1615 1615 return colorpair
1616 1616
1617 1617 def initcolorpair(self, *args, **kwargs):
1618 1618 """same as getcolorpair."""
1619 1619 self.getcolorpair(*args, **kwargs)
1620 1620
1621 1621 def helpwindow(self):
1622 1622 """print a help window to the screen. exit after any keypress."""
1623 1623 helptext = _(
1624 1624 """ [press any key to return to the patch-display]
1625 1625
1626 1626 The curses hunk selector allows you to interactively choose among the
1627 1627 changes you have made, and confirm only those changes you select for
1628 1628 further processing by the command you are running (such as commit,
1629 1629 shelve, or revert). After confirming the selected changes, the
1630 1630 unselected changes are still present in your working copy, so you can
1631 1631 use the hunk selector multiple times to split large changes into
1632 1632 smaller changesets. the following are valid keystrokes:
1633 1633
1634 1634 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1635 1635 [enter] : (un-)select item and go to next item of same type
1636 1636 A : (un-)select all items
1637 1637 X : (un-)select all items between current and most-recent
1638 1638 up/down-arrow [k/j] : go to previous/next unfolded item
1639 1639 pgup/pgdn [K/J] : go to previous/next item of same type
1640 1640 right/left-arrow [l/h] : go to child item / parent item
1641 1641 shift-left-arrow [H] : go to parent header / fold selected header
1642 1642 g : go to the top
1643 1643 G : go to the bottom
1644 1644 f : fold / unfold item, hiding/revealing its children
1645 1645 F : fold / unfold parent item and all of its ancestors
1646 1646 ctrl-l : scroll the selected line to the top of the screen
1647 1647 m : edit / resume editing the commit message
1648 1648 e : edit the currently selected hunk
1649 1649 a : toggle all selections
1650 1650 c : confirm selected changes
1651 1651 r : review/edit and confirm selected changes
1652 1652 q : quit without confirming (no changes will be made)
1653 1653 ? : help (what you're currently reading)"""
1654 1654 )
1655 1655
1656 1656 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1657 1657 helplines = helptext.split(b"\n")
1658 1658 helplines = helplines + [b" "] * (
1659 1659 self.yscreensize - self.numstatuslines - len(helplines) - 1
1660 1660 )
1661 1661 try:
1662 1662 for line in helplines:
1663 1663 self.printstring(helpwin, line, pairname=b"legend")
1664 1664 except curses.error:
1665 1665 pass
1666 1666 helpwin.refresh()
1667 1667 try:
1668 1668 with self.ui.timeblockedsection(b'crecord'):
1669 1669 helpwin.getkey()
1670 1670 except curses.error:
1671 1671 pass
1672 1672
1673 1673 def commitMessageWindow(self):
1674 1674 """Create a temporary commit message editing window on the screen."""
1675 1675
1676 1676 curses.raw()
1677 1677 curses.def_prog_mode()
1678 1678 curses.endwin()
1679 1679 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1680 1680 curses.cbreak()
1681 1681 self.stdscr.refresh()
1682 1682 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1683 1683
1684 1684 def handlefirstlineevent(self):
1685 1685 """
1686 1686 Handle 'g' to navigate to the top most file in the ncurses window.
1687 1687 """
1688 1688 self.currentselecteditem = self.headerlist[0]
1689 1689 currentitem = self.currentselecteditem
1690 1690 # select the parent item recursively until we're at a header
1691 1691 while True:
1692 1692 nextitem = currentitem.parentitem()
1693 1693 if nextitem is None:
1694 1694 break
1695 1695 else:
1696 1696 currentitem = nextitem
1697 1697
1698 1698 self.currentselecteditem = currentitem
1699 1699
1700 1700 def handlelastlineevent(self):
1701 1701 """
1702 1702 Handle 'G' to navigate to the bottom most file/hunk/line depending
1703 1703 on the whether the fold is active or not.
1704 1704
1705 1705 If the bottom most file is folded, it navigates to that file and
1706 1706 stops there. If the bottom most file is unfolded, it navigates to
1707 1707 the bottom most hunk in that file and stops there. If the bottom most
1708 1708 hunk is unfolded, it navigates to the bottom most line in that hunk.
1709 1709 """
1710 1710 currentitem = self.currentselecteditem
1711 1711 nextitem = currentitem.nextitem()
1712 1712 # select the child item recursively until we're at a footer
1713 1713 while nextitem is not None:
1714 1714 nextitem = currentitem.nextitem()
1715 1715 if nextitem is None:
1716 1716 break
1717 1717 else:
1718 1718 currentitem = nextitem
1719 1719
1720 1720 self.currentselecteditem = currentitem
1721 1721 self.recenterdisplayedarea()
1722 1722
1723 1723 def confirmationwindow(self, windowtext):
1724 1724 """display an informational window, then wait for and return a
1725 1725 keypress."""
1726 1726
1727 1727 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1728 1728 try:
1729 1729 lines = windowtext.split(b"\n")
1730 1730 for line in lines:
1731 1731 self.printstring(confirmwin, line, pairname=b"selected")
1732 1732 except curses.error:
1733 1733 pass
1734 1734 self.stdscr.refresh()
1735 1735 confirmwin.refresh()
1736 1736 try:
1737 1737 with self.ui.timeblockedsection(b'crecord'):
1738 1738 response = chr(self.stdscr.getch())
1739 1739 except ValueError:
1740 1740 response = None
1741 1741
1742 1742 return response
1743 1743
1744 1744 def reviewcommit(self):
1745 1745 """ask for 'y' to be pressed to confirm selected. return True if
1746 1746 confirmed."""
1747 1747 confirmtext = _(
1748 1748 """If you answer yes to the following, your currently chosen patch chunks
1749 1749 will be loaded into an editor. To modify the patch, make the changes in your
1750 1750 editor and save. To accept the current patch as-is, close the editor without
1751 1751 saving.
1752 1752
1753 1753 note: don't add/remove lines unless you also modify the range information.
1754 1754 failing to follow this rule will result in the commit aborting.
1755 1755
1756 1756 are you sure you want to review/edit and confirm the selected changes [yn]?
1757 1757 """
1758 1758 )
1759 1759 with self.ui.timeblockedsection(b'crecord'):
1760 1760 response = self.confirmationwindow(confirmtext)
1761 1761 if response is None:
1762 1762 response = "n"
1763 1763 if response.lower().startswith("y"):
1764 1764 return True
1765 1765 else:
1766 1766 return False
1767 1767
1768 1768 def recenterdisplayedarea(self):
1769 1769 """
1770 1770 once we scrolled with pg up pg down we can be pointing outside of the
1771 1771 display zone. we print the patch with towin=False to compute the
1772 1772 location of the selected item even though it is outside of the displayed
1773 1773 zone and then update the scroll.
1774 1774 """
1775 1775 self.printitem(towin=False)
1776 1776 self.updatescroll()
1777 1777
1778 1778 def toggleedit(self, item=None, test=False):
1779 1779 """
1780 1780 edit the currently selected chunk
1781 1781 """
1782 1782
1783 1783 def updateui(self):
1784 1784 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1785 1785 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1786 1786 self.updatescroll()
1787 1787 self.stdscr.refresh()
1788 1788 self.statuswin.refresh()
1789 1789 self.stdscr.keypad(1)
1790 1790
1791 1791 def editpatchwitheditor(self, chunk):
1792 1792 if chunk is None:
1793 1793 self.ui.write(_(b'cannot edit patch for whole file'))
1794 1794 self.ui.write(b"\n")
1795 1795 return None
1796 1796 if chunk.header.binary():
1797 1797 self.ui.write(_(b'cannot edit patch for binary file'))
1798 1798 self.ui.write(b"\n")
1799 1799 return None
1800 1800
1801 1801 # write the initial patch
1802 1802 patch = stringio()
1803 1803 patch.write(diffhelptext + hunkhelptext)
1804 1804 chunk.header.write(patch)
1805 1805 chunk.write(patch)
1806 1806
1807 1807 # start the editor and wait for it to complete
1808 1808 try:
1809 1809 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1810 1810 except error.Abort as exc:
1811 1811 self.errorstr = exc.message
1812 1812 return None
1813 1813 finally:
1814 1814 self.stdscr.clear()
1815 1815 self.stdscr.refresh()
1816 1816
1817 1817 # remove comment lines
1818 1818 patch = [
1819 1819 line + b'\n'
1820 1820 for line in patch.splitlines()
1821 1821 if not line.startswith(b'#')
1822 1822 ]
1823 1823 return patchmod.parsepatch(patch)
1824 1824
1825 1825 if item is None:
1826 1826 item = self.currentselecteditem
1827 1827 if isinstance(item, uiheader):
1828 1828 return
1829 1829 if isinstance(item, uihunkline):
1830 1830 item = item.parentitem()
1831 1831 if not isinstance(item, uihunk):
1832 1832 return
1833 1833
1834 1834 # To go back to that hunk or its replacement at the end of the edit
1835 1835 itemindex = item.parentitem().hunks.index(item)
1836 1836
1837 1837 beforeadded, beforeremoved = item.added, item.removed
1838 1838 newpatches = editpatchwitheditor(self, item)
1839 1839 if newpatches is None:
1840 1840 if not test:
1841 1841 updateui(self)
1842 1842 return
1843 1843 header = item.header
1844 1844 editedhunkindex = header.hunks.index(item)
1845 1845 hunksbefore = header.hunks[:editedhunkindex]
1846 1846 hunksafter = header.hunks[editedhunkindex + 1 :]
1847 1847 newpatchheader = newpatches[0]
1848 1848 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1849 1849 newadded = sum([h.added for h in newhunks])
1850 1850 newremoved = sum([h.removed for h in newhunks])
1851 1851 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1852 1852
1853 1853 for h in hunksafter:
1854 1854 h.toline += offset
1855 1855 for h in newhunks:
1856 1856 h.folded = False
1857 1857 header.hunks = hunksbefore + newhunks + hunksafter
1858 1858 if self.emptypatch():
1859 1859 header.hunks = hunksbefore + [item] + hunksafter
1860 1860 self.currentselecteditem = header
1861 1861 if len(header.hunks) > itemindex:
1862 1862 self.currentselecteditem = header.hunks[itemindex]
1863 1863
1864 1864 if not test:
1865 1865 updateui(self)
1866 1866
1867 1867 def emptypatch(self):
1868 1868 item = self.headerlist
1869 1869 if not item:
1870 1870 return True
1871 1871 for header in item:
1872 1872 if header.hunks:
1873 1873 return False
1874 1874 return True
1875 1875
1876 1876 def handlekeypressed(self, keypressed, test=False):
1877 1877 """
1878 1878 Perform actions based on pressed keys.
1879 1879
1880 1880 Return true to exit the main loop.
1881 1881 """
1882 1882 if keypressed in ["k", "KEY_UP"]:
1883 1883 self.uparrowevent()
1884 1884 elif keypressed in ["K", "KEY_PPAGE"]:
1885 1885 self.uparrowshiftevent()
1886 1886 elif keypressed in ["j", "KEY_DOWN"]:
1887 1887 self.downarrowevent()
1888 1888 elif keypressed in ["J", "KEY_NPAGE"]:
1889 1889 self.downarrowshiftevent()
1890 1890 elif keypressed in ["l", "KEY_RIGHT"]:
1891 1891 self.rightarrowevent()
1892 1892 elif keypressed in ["h", "KEY_LEFT"]:
1893 1893 self.leftarrowevent()
1894 1894 elif keypressed in ["H", "KEY_SLEFT"]:
1895 1895 self.leftarrowshiftevent()
1896 1896 elif keypressed in ["q"]:
1897 1897 raise error.Abort(_(b'user quit'))
1898 1898 elif keypressed in ['a']:
1899 1899 self.flipselections()
1900 1900 elif keypressed in ["c"]:
1901 1901 return True
1902 1902 elif keypressed in ["r"]:
1903 1903 if self.reviewcommit():
1904 1904 self.opts[b'review'] = True
1905 1905 return True
1906 1906 elif test and keypressed in ["R"]:
1907 1907 self.opts[b'review'] = True
1908 1908 return True
1909 1909 elif keypressed in [" ", "x"]:
1910 1910 self.toggleapply()
1911 1911 elif keypressed in ["\n", "KEY_ENTER"]:
1912 1912 self.toggleapply()
1913 1913 self.nextsametype(test=test)
1914 1914 elif keypressed in ["X"]:
1915 1915 self.toggleallbetween()
1916 1916 elif keypressed in ["A"]:
1917 1917 self.toggleall()
1918 1918 elif keypressed in ["e"]:
1919 1919 self.toggleedit(test=test)
1920 1920 elif keypressed in ["f"]:
1921 1921 self.togglefolded()
1922 1922 elif keypressed in ["F"]:
1923 1923 self.togglefolded(foldparent=True)
1924 1924 elif keypressed in ["m"]:
1925 1925 self.commitMessageWindow()
1926 1926 elif keypressed in ["g", "KEY_HOME"]:
1927 1927 self.handlefirstlineevent()
1928 1928 elif keypressed in ["G", "KEY_END"]:
1929 1929 self.handlelastlineevent()
1930 1930 elif keypressed in ["?"]:
1931 1931 self.helpwindow()
1932 1932 self.stdscr.clear()
1933 1933 self.stdscr.refresh()
1934 1934 elif keypressed in [curses.ascii.ctrl("L")]:
1935 1935 # scroll the current line to the top of the screen, and redraw
1936 1936 # everything
1937 1937 self.scrolllines(self.selecteditemstartline)
1938 1938 self.stdscr.clear()
1939 1939 self.stdscr.refresh()
1940 1940
1941 1941 def main(self, stdscr):
1942 1942 """
1943 1943 method to be wrapped by curses.wrapper() for selecting chunks.
1944 1944 """
1945 1945
1946 1946 origsigwinch = sentinel = object()
1947 1947 if util.safehasattr(signal, b'SIGWINCH'):
1948 1948 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1949 1949 try:
1950 1950 return self._main(stdscr)
1951 1951 finally:
1952 1952 if origsigwinch is not sentinel:
1953 1953 signal.signal(signal.SIGWINCH, origsigwinch)
1954 1954
1955 1955 def _main(self, stdscr):
1956 1956 self.stdscr = stdscr
1957 1957 # error during initialization, cannot be printed in the curses
1958 1958 # interface, it should be printed by the calling code
1959 1959 self.initexc = None
1960 1960 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1961 1961
1962 1962 curses.start_color()
1963 1963 try:
1964 1964 curses.use_default_colors()
1965 1965 except curses.error:
1966 1966 self.usecolor = False
1967 1967
1968 1968 # In some situations we may have some cruft left on the "alternate
1969 1969 # screen" from another program (or previous iterations of ourself), and
1970 1970 # we won't clear it if the scroll region is small enough to comfortably
1971 1971 # fit on the terminal.
1972 1972 self.stdscr.clear()
1973 1973
1974 1974 # don't display the cursor
1975 1975 try:
1976 1976 curses.curs_set(0)
1977 1977 except curses.error:
1978 1978 pass
1979 1979
1980 1980 # available colors: black, blue, cyan, green, magenta, white, yellow
1981 1981 # init_pair(color_id, foreground_color, background_color)
1982 1982 self.initcolorpair(None, None, name=b"normal")
1983 1983 self.initcolorpair(
1984 1984 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1985 1985 )
1986 1986 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1987 1987 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1988 1988 self.initcolorpair(
1989 1989 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1990 1990 )
1991 1991 # newwin([height, width,] begin_y, begin_x)
1992 1992 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1993 1993 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1994 1994
1995 1995 # figure out how much space to allocate for the chunk-pad which is
1996 1996 # used for displaying the patch
1997 1997
1998 1998 # stupid hack to prevent getnumlinesdisplayed from failing
1999 1999 self.chunkpad = curses.newpad(1, self.xscreensize)
2000 2000
2001 2001 # add 1 so to account for last line text reaching end of line
2002 2002 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2003 2003
2004 2004 try:
2005 2005 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2006 2006 except curses.error:
2007 2007 self.initexc = fallbackerror(
2008 2008 _(b'this diff is too large to be displayed')
2009 2009 )
2010 2010 return
2011 2011 # initialize selecteditemendline (initial start-line is 0)
2012 2012 self.selecteditemendline = self.getnumlinesdisplayed(
2013 2013 self.currentselecteditem, recursechildren=False
2014 2014 )
2015 2015
2016 2016 while True:
2017 2017 self.updatescreen()
2018 2018 try:
2019 2019 with self.ui.timeblockedsection(b'crecord'):
2020 2020 keypressed = self.statuswin.getkey()
2021 2021 if self.errorstr is not None:
2022 2022 self.errorstr = None
2023 2023 continue
2024 2024 except curses.error:
2025 2025 keypressed = b"foobar"
2026 2026 if self.handlekeypressed(keypressed):
2027 2027 break
2028 2028
2029 2029 if self.commenttext != b"":
2030 2030 whitespaceremoved = re.sub(
2031 2031 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2032 2032 )
2033 2033 if whitespaceremoved != b"":
2034 2034 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now