##// END OF EJS Templates
crecord: add docblock to handlekeypressed...
Ryan McElroy -
r28581:3c8f0a60 default
parent child Browse files
Show More
@@ -1,1679 +1,1684 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 cStringIO
14 14 import locale
15 15 import os
16 16 import re
17 17 import signal
18 18 import struct
19 19 import sys
20 20 import tempfile
21 21
22 22 from .i18n import _
23 23 from . import (
24 24 encoding,
25 25 error,
26 26 patch as patchmod,
27 27 util,
28 28 )
29 29
30 30 # This is required for ncurses to display non-ASCII characters in default user
31 31 # locale encoding correctly. --immerrr
32 32 locale.setlocale(locale.LC_ALL, '')
33 33
34 34 try:
35 35 import curses
36 36 import fcntl
37 37 import termios
38 38 curses.error
39 39 fcntl.ioctl
40 40 termios.TIOCGWINSZ
41 41 except ImportError:
42 42 # I have no idea if wcurses works with crecord...
43 43 try:
44 44 import wcurses as curses
45 45 curses.error
46 46 except ImportError:
47 47 # wcurses is not shipped on Windows by default, or python is not
48 48 # compiled with curses
49 49 curses = False
50 50
51 51 def checkcurses(ui):
52 52 """Return True if the user wants to use curses
53 53
54 54 This method returns True if curses is found (and that python is built with
55 55 it) and that the user has the correct flag for the ui.
56 56 """
57 57 return curses and ui.interface("chunkselector") == "curses"
58 58
59 59 _origstdout = sys.__stdout__ # used by gethw()
60 60
61 61 class patchnode(object):
62 62 """abstract class for patch graph nodes
63 63 (i.e. patchroot, header, hunk, hunkline)
64 64 """
65 65
66 66 def firstchild(self):
67 67 raise NotImplementedError("method must be implemented by subclass")
68 68
69 69 def lastchild(self):
70 70 raise NotImplementedError("method must be implemented by subclass")
71 71
72 72 def allchildren(self):
73 73 "Return a list of all of the direct children of this node"
74 74 raise NotImplementedError("method must be implemented by subclass")
75 75 def nextsibling(self):
76 76 """
77 77 Return the closest next item of the same type where there are no items
78 78 of different types between the current item and this closest item.
79 79 If no such item exists, return None.
80 80 """
81 81 raise NotImplementedError("method must be implemented by subclass")
82 82
83 83 def prevsibling(self):
84 84 """
85 85 Return the closest previous item of the same type where there are no
86 86 items of different types between the current item and this closest item.
87 87 If no such item exists, return None.
88 88 """
89 89 raise NotImplementedError("method must be implemented by subclass")
90 90
91 91 def parentitem(self):
92 92 raise NotImplementedError("method must be implemented by subclass")
93 93
94 94
95 95 def nextitem(self, constrainlevel=True, skipfolded=True):
96 96 """
97 97 If constrainLevel == True, return the closest next item
98 98 of the same type where there are no items of different types between
99 99 the current item and this closest item.
100 100
101 101 If constrainLevel == False, then try to return the next item
102 102 closest to this item, regardless of item's type (header, hunk, or
103 103 HunkLine).
104 104
105 105 If skipFolded == True, and the current item is folded, then the child
106 106 items that are hidden due to folding will be skipped when determining
107 107 the next item.
108 108
109 109 If it is not possible to get the next item, return None.
110 110 """
111 111 try:
112 112 itemfolded = self.folded
113 113 except AttributeError:
114 114 itemfolded = False
115 115 if constrainlevel:
116 116 return self.nextsibling()
117 117 elif skipfolded and itemfolded:
118 118 nextitem = self.nextsibling()
119 119 if nextitem is None:
120 120 try:
121 121 nextitem = self.parentitem().nextsibling()
122 122 except AttributeError:
123 123 nextitem = None
124 124 return nextitem
125 125 else:
126 126 # try child
127 127 item = self.firstchild()
128 128 if item is not None:
129 129 return item
130 130
131 131 # else try next sibling
132 132 item = self.nextsibling()
133 133 if item is not None:
134 134 return item
135 135
136 136 try:
137 137 # else try parent's next sibling
138 138 item = self.parentitem().nextsibling()
139 139 if item is not None:
140 140 return item
141 141
142 142 # else return grandparent's next sibling (or None)
143 143 return self.parentitem().parentitem().nextsibling()
144 144
145 145 except AttributeError: # parent and/or grandparent was None
146 146 return None
147 147
148 148 def previtem(self, constrainlevel=True, skipfolded=True):
149 149 """
150 150 If constrainLevel == True, return the closest previous item
151 151 of the same type where there are no items of different types between
152 152 the current item and this closest item.
153 153
154 154 If constrainLevel == False, then try to return the previous item
155 155 closest to this item, regardless of item's type (header, hunk, or
156 156 HunkLine).
157 157
158 158 If skipFolded == True, and the current item is folded, then the items
159 159 that are hidden due to folding will be skipped when determining the
160 160 next item.
161 161
162 162 If it is not possible to get the previous item, return None.
163 163 """
164 164 if constrainlevel:
165 165 return self.prevsibling()
166 166 else:
167 167 # try previous sibling's last child's last child,
168 168 # else try previous sibling's last child, else try previous sibling
169 169 prevsibling = self.prevsibling()
170 170 if prevsibling is not None:
171 171 prevsiblinglastchild = prevsibling.lastchild()
172 172 if ((prevsiblinglastchild is not None) and
173 173 not prevsibling.folded):
174 174 prevsiblinglclc = prevsiblinglastchild.lastchild()
175 175 if ((prevsiblinglclc is not None) and
176 176 not prevsiblinglastchild.folded):
177 177 return prevsiblinglclc
178 178 else:
179 179 return prevsiblinglastchild
180 180 else:
181 181 return prevsibling
182 182
183 183 # try parent (or None)
184 184 return self.parentitem()
185 185
186 186 class patch(patchnode, list): # todo: rename patchroot
187 187 """
188 188 list of header objects representing the patch.
189 189 """
190 190 def __init__(self, headerlist):
191 191 self.extend(headerlist)
192 192 # add parent patch object reference to each header
193 193 for header in self:
194 194 header.patch = self
195 195
196 196 class uiheader(patchnode):
197 197 """patch header
198 198
199 199 xxx shouldn't we move this to mercurial/patch.py ?
200 200 """
201 201
202 202 def __init__(self, header):
203 203 self.nonuiheader = header
204 204 # flag to indicate whether to apply this chunk
205 205 self.applied = True
206 206 # flag which only affects the status display indicating if a node's
207 207 # children are partially applied (i.e. some applied, some not).
208 208 self.partial = False
209 209
210 210 # flag to indicate whether to display as folded/unfolded to user
211 211 self.folded = True
212 212
213 213 # list of all headers in patch
214 214 self.patch = None
215 215
216 216 # flag is False if this header was ever unfolded from initial state
217 217 self.neverunfolded = True
218 218 self.hunks = [uihunk(h, self) for h in self.hunks]
219 219
220 220
221 221 def prettystr(self):
222 222 x = cStringIO.StringIO()
223 223 self.pretty(x)
224 224 return x.getvalue()
225 225
226 226 def nextsibling(self):
227 227 numheadersinpatch = len(self.patch)
228 228 indexofthisheader = self.patch.index(self)
229 229
230 230 if indexofthisheader < numheadersinpatch - 1:
231 231 nextheader = self.patch[indexofthisheader + 1]
232 232 return nextheader
233 233 else:
234 234 return None
235 235
236 236 def prevsibling(self):
237 237 indexofthisheader = self.patch.index(self)
238 238 if indexofthisheader > 0:
239 239 previousheader = self.patch[indexofthisheader - 1]
240 240 return previousheader
241 241 else:
242 242 return None
243 243
244 244 def parentitem(self):
245 245 """
246 246 there is no 'real' parent item of a header that can be selected,
247 247 so return None.
248 248 """
249 249 return None
250 250
251 251 def firstchild(self):
252 252 "return the first child of this item, if one exists. otherwise None."
253 253 if len(self.hunks) > 0:
254 254 return self.hunks[0]
255 255 else:
256 256 return None
257 257
258 258 def lastchild(self):
259 259 "return the last child of this item, if one exists. otherwise None."
260 260 if len(self.hunks) > 0:
261 261 return self.hunks[-1]
262 262 else:
263 263 return None
264 264
265 265 def allchildren(self):
266 266 "return a list of all of the direct children of this node"
267 267 return self.hunks
268 268
269 269 def __getattr__(self, name):
270 270 return getattr(self.nonuiheader, name)
271 271
272 272 class uihunkline(patchnode):
273 273 "represents a changed line in a hunk"
274 274 def __init__(self, linetext, hunk):
275 275 self.linetext = linetext
276 276 self.applied = True
277 277 # the parent hunk to which this line belongs
278 278 self.hunk = hunk
279 279 # folding lines currently is not used/needed, but this flag is needed
280 280 # in the previtem method.
281 281 self.folded = False
282 282
283 283 def prettystr(self):
284 284 return self.linetext
285 285
286 286 def nextsibling(self):
287 287 numlinesinhunk = len(self.hunk.changedlines)
288 288 indexofthisline = self.hunk.changedlines.index(self)
289 289
290 290 if (indexofthisline < numlinesinhunk - 1):
291 291 nextline = self.hunk.changedlines[indexofthisline + 1]
292 292 return nextline
293 293 else:
294 294 return None
295 295
296 296 def prevsibling(self):
297 297 indexofthisline = self.hunk.changedlines.index(self)
298 298 if indexofthisline > 0:
299 299 previousline = self.hunk.changedlines[indexofthisline - 1]
300 300 return previousline
301 301 else:
302 302 return None
303 303
304 304 def parentitem(self):
305 305 "return the parent to the current item"
306 306 return self.hunk
307 307
308 308 def firstchild(self):
309 309 "return the first child of this item, if one exists. otherwise None."
310 310 # hunk-lines don't have children
311 311 return None
312 312
313 313 def lastchild(self):
314 314 "return the last child of this item, if one exists. otherwise None."
315 315 # hunk-lines don't have children
316 316 return None
317 317
318 318 class uihunk(patchnode):
319 319 """ui patch hunk, wraps a hunk and keep track of ui behavior """
320 320 maxcontext = 3
321 321
322 322 def __init__(self, hunk, header):
323 323 self._hunk = hunk
324 324 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
325 325 self.header = header
326 326 # used at end for detecting how many removed lines were un-applied
327 327 self.originalremoved = self.removed
328 328
329 329 # flag to indicate whether to display as folded/unfolded to user
330 330 self.folded = True
331 331 # flag to indicate whether to apply this chunk
332 332 self.applied = True
333 333 # flag which only affects the status display indicating if a node's
334 334 # children are partially applied (i.e. some applied, some not).
335 335 self.partial = False
336 336
337 337 def nextsibling(self):
338 338 numhunksinheader = len(self.header.hunks)
339 339 indexofthishunk = self.header.hunks.index(self)
340 340
341 341 if (indexofthishunk < numhunksinheader - 1):
342 342 nexthunk = self.header.hunks[indexofthishunk + 1]
343 343 return nexthunk
344 344 else:
345 345 return None
346 346
347 347 def prevsibling(self):
348 348 indexofthishunk = self.header.hunks.index(self)
349 349 if indexofthishunk > 0:
350 350 previoushunk = self.header.hunks[indexofthishunk - 1]
351 351 return previoushunk
352 352 else:
353 353 return None
354 354
355 355 def parentitem(self):
356 356 "return the parent to the current item"
357 357 return self.header
358 358
359 359 def firstchild(self):
360 360 "return the first child of this item, if one exists. otherwise None."
361 361 if len(self.changedlines) > 0:
362 362 return self.changedlines[0]
363 363 else:
364 364 return None
365 365
366 366 def lastchild(self):
367 367 "return the last child of this item, if one exists. otherwise None."
368 368 if len(self.changedlines) > 0:
369 369 return self.changedlines[-1]
370 370 else:
371 371 return None
372 372
373 373 def allchildren(self):
374 374 "return a list of all of the direct children of this node"
375 375 return self.changedlines
376 376 def countchanges(self):
377 377 """changedlines -> (n+,n-)"""
378 378 add = len([l for l in self.changedlines if l.applied
379 379 and l.prettystr()[0] == '+'])
380 380 rem = len([l for l in self.changedlines if l.applied
381 381 and l.prettystr()[0] == '-'])
382 382 return add, rem
383 383
384 384 def getfromtoline(self):
385 385 # calculate the number of removed lines converted to context lines
386 386 removedconvertedtocontext = self.originalremoved - self.removed
387 387
388 388 contextlen = (len(self.before) + len(self.after) +
389 389 removedconvertedtocontext)
390 390 if self.after and self.after[-1] == '\\ no newline at end of file\n':
391 391 contextlen -= 1
392 392 fromlen = contextlen + self.removed
393 393 tolen = contextlen + self.added
394 394
395 395 # diffutils manual, section "2.2.2.2 detailed description of unified
396 396 # format": "an empty hunk is considered to end at the line that
397 397 # precedes the hunk."
398 398 #
399 399 # so, if either of hunks is empty, decrease its line start. --immerrr
400 400 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
401 401 fromline, toline = self.fromline, self.toline
402 402 if fromline != 0:
403 403 if fromlen == 0:
404 404 fromline -= 1
405 405 if tolen == 0:
406 406 toline -= 1
407 407
408 408 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
409 409 fromline, fromlen, toline, tolen,
410 410 self.proc and (' ' + self.proc))
411 411 return fromtoline
412 412
413 413 def write(self, fp):
414 414 # updated self.added/removed, which are used by getfromtoline()
415 415 self.added, self.removed = self.countchanges()
416 416 fp.write(self.getfromtoline())
417 417
418 418 hunklinelist = []
419 419 # add the following to the list: (1) all applied lines, and
420 420 # (2) all unapplied removal lines (convert these to context lines)
421 421 for changedline in self.changedlines:
422 422 changedlinestr = changedline.prettystr()
423 423 if changedline.applied:
424 424 hunklinelist.append(changedlinestr)
425 425 elif changedlinestr[0] == "-":
426 426 hunklinelist.append(" " + changedlinestr[1:])
427 427
428 428 fp.write(''.join(self.before + hunklinelist + self.after))
429 429
430 430 pretty = write
431 431
432 432 def prettystr(self):
433 433 x = cStringIO.StringIO()
434 434 self.pretty(x)
435 435 return x.getvalue()
436 436
437 437 def __getattr__(self, name):
438 438 return getattr(self._hunk, name)
439 439 def __repr__(self):
440 440 return '<hunk %r@%d>' % (self.filename(), self.fromline)
441 441
442 442 def filterpatch(ui, chunks, chunkselector, operation=None):
443 443 """interactively filter patch chunks into applied-only chunks"""
444 444
445 445 if operation is None:
446 446 operation = _('confirm')
447 447 chunks = list(chunks)
448 448 # convert chunks list into structure suitable for displaying/modifying
449 449 # with curses. create a list of headers only.
450 450 headers = [c for c in chunks if isinstance(c, patchmod.header)]
451 451
452 452 # if there are no changed files
453 453 if len(headers) == 0:
454 454 return [], {}
455 455 uiheaders = [uiheader(h) for h in headers]
456 456 # let user choose headers/hunks/lines, and mark their applied flags
457 457 # accordingly
458 458 ret = chunkselector(ui, uiheaders)
459 459 appliedhunklist = []
460 460 for hdr in uiheaders:
461 461 if (hdr.applied and
462 462 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
463 463 appliedhunklist.append(hdr)
464 464 fixoffset = 0
465 465 for hnk in hdr.hunks:
466 466 if hnk.applied:
467 467 appliedhunklist.append(hnk)
468 468 # adjust the 'to'-line offset of the hunk to be correct
469 469 # after de-activating some of the other hunks for this file
470 470 if fixoffset:
471 471 #hnk = copy.copy(hnk) # necessary??
472 472 hnk.toline += fixoffset
473 473 else:
474 474 fixoffset += hnk.removed - hnk.added
475 475
476 476 return (appliedhunklist, ret)
477 477
478 478 def gethw():
479 479 """
480 480 magically get the current height and width of the window (without initscr)
481 481
482 482 this is a rip-off of a rip-off - taken from the bpython code. it is
483 483 useful / necessary because otherwise curses.initscr() must be called,
484 484 which can leave the terminal in a nasty state after exiting.
485 485 """
486 486 h, w = struct.unpack(
487 487 "hhhh", fcntl.ioctl(_origstdout, termios.TIOCGWINSZ, "\000"*8))[0:2]
488 488 return h, w
489 489
490 490 def chunkselector(ui, headerlist):
491 491 """
492 492 curses interface to get selection of chunks, and mark the applied flags
493 493 of the chosen chunks.
494 494 """
495 495 ui.write(_('starting interactive selection\n'))
496 496 chunkselector = curseschunkselector(headerlist, ui)
497 497 f = signal.getsignal(signal.SIGTSTP)
498 498 curses.wrapper(chunkselector.main)
499 499 if chunkselector.initerr is not None:
500 500 raise error.Abort(chunkselector.initerr)
501 501 # ncurses does not restore signal handler for SIGTSTP
502 502 signal.signal(signal.SIGTSTP, f)
503 503 return chunkselector.opts
504 504
505 505 def testdecorator(testfn, f):
506 506 def u(*args, **kwargs):
507 507 return f(testfn, *args, **kwargs)
508 508 return u
509 509
510 510 def testchunkselector(testfn, ui, headerlist):
511 511 """
512 512 test interface to get selection of chunks, and mark the applied flags
513 513 of the chosen chunks.
514 514 """
515 515 chunkselector = curseschunkselector(headerlist, ui)
516 516 if testfn and os.path.exists(testfn):
517 517 testf = open(testfn)
518 518 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
519 519 testf.close()
520 520 while True:
521 521 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
522 522 break
523 523 return chunkselector.opts
524 524
525 525 class curseschunkselector(object):
526 526 def __init__(self, headerlist, ui):
527 527 # put the headers into a patch object
528 528 self.headerlist = patch(headerlist)
529 529
530 530 self.ui = ui
531 531 self.opts = {}
532 532
533 533 self.errorstr = None
534 534 # list of all chunks
535 535 self.chunklist = []
536 536 for h in headerlist:
537 537 self.chunklist.append(h)
538 538 self.chunklist.extend(h.hunks)
539 539
540 540 # dictionary mapping (fgcolor, bgcolor) pairs to the
541 541 # corresponding curses color-pair value.
542 542 self.colorpairs = {}
543 543 # maps custom nicknames of color-pairs to curses color-pair values
544 544 self.colorpairnames = {}
545 545
546 546 # the currently selected header, hunk, or hunk-line
547 547 self.currentselecteditem = self.headerlist[0]
548 548
549 549 # updated when printing out patch-display -- the 'lines' here are the
550 550 # line positions *in the pad*, not on the screen.
551 551 self.selecteditemstartline = 0
552 552 self.selecteditemendline = None
553 553
554 554 # define indentation levels
555 555 self.headerindentnumchars = 0
556 556 self.hunkindentnumchars = 3
557 557 self.hunklineindentnumchars = 6
558 558
559 559 # the first line of the pad to print to the screen
560 560 self.firstlineofpadtoprint = 0
561 561
562 562 # keeps track of the number of lines in the pad
563 563 self.numpadlines = None
564 564
565 565 self.numstatuslines = 2
566 566
567 567 # keep a running count of the number of lines printed to the pad
568 568 # (used for determining when the selected item begins/ends)
569 569 self.linesprintedtopadsofar = 0
570 570
571 571 # the first line of the pad which is visible on the screen
572 572 self.firstlineofpadtoprint = 0
573 573
574 574 # stores optional text for a commit comment provided by the user
575 575 self.commenttext = ""
576 576
577 577 # if the last 'toggle all' command caused all changes to be applied
578 578 self.waslasttoggleallapplied = True
579 579
580 580 def uparrowevent(self):
581 581 """
582 582 try to select the previous item to the current item that has the
583 583 most-indented level. for example, if a hunk is selected, try to select
584 584 the last hunkline of the hunk prior to the selected hunk. or, if
585 585 the first hunkline of a hunk is currently selected, then select the
586 586 hunk itself.
587 587
588 588 if the currently selected item is already at the top of the screen,
589 589 scroll the screen down to show the new-selected item.
590 590 """
591 591 currentitem = self.currentselecteditem
592 592
593 593 nextitem = currentitem.previtem(constrainlevel=False)
594 594
595 595 if nextitem is None:
596 596 # if no parent item (i.e. currentitem is the first header), then
597 597 # no change...
598 598 nextitem = currentitem
599 599
600 600 self.currentselecteditem = nextitem
601 601
602 602 def uparrowshiftevent(self):
603 603 """
604 604 select (if possible) the previous item on the same level as the
605 605 currently selected item. otherwise, select (if possible) the
606 606 parent-item of the currently selected item.
607 607
608 608 if the currently selected item is already at the top of the screen,
609 609 scroll the screen down to show the new-selected item.
610 610 """
611 611 currentitem = self.currentselecteditem
612 612 nextitem = currentitem.previtem()
613 613 # if there's no previous item on this level, try choosing the parent
614 614 if nextitem is None:
615 615 nextitem = currentitem.parentitem()
616 616 if nextitem is None:
617 617 # if no parent item (i.e. currentitem is the first header), then
618 618 # no change...
619 619 nextitem = currentitem
620 620
621 621 self.currentselecteditem = nextitem
622 622
623 623 def downarrowevent(self):
624 624 """
625 625 try to select the next item to the current item that has the
626 626 most-indented level. for example, if a hunk is selected, select
627 627 the first hunkline of the selected hunk. or, if the last hunkline of
628 628 a hunk is currently selected, then select the next hunk, if one exists,
629 629 or if not, the next header if one exists.
630 630
631 631 if the currently selected item is already at the bottom of the screen,
632 632 scroll the screen up to show the new-selected item.
633 633 """
634 634 #self.startprintline += 1 #debug
635 635 currentitem = self.currentselecteditem
636 636
637 637 nextitem = currentitem.nextitem(constrainlevel=False)
638 638 # if there's no next item, keep the selection as-is
639 639 if nextitem is None:
640 640 nextitem = currentitem
641 641
642 642 self.currentselecteditem = nextitem
643 643
644 644 def downarrowshiftevent(self):
645 645 """
646 646 if the cursor is already at the bottom chunk, scroll the screen up and
647 647 move the cursor-position to the subsequent chunk. otherwise, only move
648 648 the cursor position down one chunk.
649 649 """
650 650 # todo: update docstring
651 651
652 652 currentitem = self.currentselecteditem
653 653 nextitem = currentitem.nextitem()
654 654 # if there's no previous item on this level, try choosing the parent's
655 655 # nextitem.
656 656 if nextitem is None:
657 657 try:
658 658 nextitem = currentitem.parentitem().nextitem()
659 659 except AttributeError:
660 660 # parentitem returned None, so nextitem() can't be called
661 661 nextitem = None
662 662 if nextitem is None:
663 663 # if no next item on parent-level, then no change...
664 664 nextitem = currentitem
665 665
666 666 self.currentselecteditem = nextitem
667 667
668 668 def rightarrowevent(self):
669 669 """
670 670 select (if possible) the first of this item's child-items.
671 671 """
672 672 currentitem = self.currentselecteditem
673 673 nextitem = currentitem.firstchild()
674 674
675 675 # turn off folding if we want to show a child-item
676 676 if currentitem.folded:
677 677 self.togglefolded(currentitem)
678 678
679 679 if nextitem is None:
680 680 # if no next item on parent-level, then no change...
681 681 nextitem = currentitem
682 682
683 683 self.currentselecteditem = nextitem
684 684
685 685 def leftarrowevent(self):
686 686 """
687 687 if the current item can be folded (i.e. it is an unfolded header or
688 688 hunk), then fold it. otherwise try select (if possible) the parent
689 689 of this item.
690 690 """
691 691 currentitem = self.currentselecteditem
692 692
693 693 # try to fold the item
694 694 if not isinstance(currentitem, uihunkline):
695 695 if not currentitem.folded:
696 696 self.togglefolded(item=currentitem)
697 697 return
698 698
699 699 # if it can't be folded, try to select the parent item
700 700 nextitem = currentitem.parentitem()
701 701
702 702 if nextitem is None:
703 703 # if no item on parent-level, then no change...
704 704 nextitem = currentitem
705 705 if not nextitem.folded:
706 706 self.togglefolded(item=nextitem)
707 707
708 708 self.currentselecteditem = nextitem
709 709
710 710 def leftarrowshiftevent(self):
711 711 """
712 712 select the header of the current item (or fold current item if the
713 713 current item is already a header).
714 714 """
715 715 currentitem = self.currentselecteditem
716 716
717 717 if isinstance(currentitem, uiheader):
718 718 if not currentitem.folded:
719 719 self.togglefolded(item=currentitem)
720 720 return
721 721
722 722 # select the parent item recursively until we're at a header
723 723 while True:
724 724 nextitem = currentitem.parentitem()
725 725 if nextitem is None:
726 726 break
727 727 else:
728 728 currentitem = nextitem
729 729
730 730 self.currentselecteditem = currentitem
731 731
732 732 def updatescroll(self):
733 733 "scroll the screen to fully show the currently-selected"
734 734 selstart = self.selecteditemstartline
735 735 selend = self.selecteditemendline
736 736 #selnumlines = selend - selstart
737 737 padstart = self.firstlineofpadtoprint
738 738 padend = padstart + self.yscreensize - self.numstatuslines - 1
739 739 # 'buffered' pad start/end values which scroll with a certain
740 740 # top/bottom context margin
741 741 padstartbuffered = padstart + 3
742 742 padendbuffered = padend - 3
743 743
744 744 if selend > padendbuffered:
745 745 self.scrolllines(selend - padendbuffered)
746 746 elif selstart < padstartbuffered:
747 747 # negative values scroll in pgup direction
748 748 self.scrolllines(selstart - padstartbuffered)
749 749
750 750
751 751 def scrolllines(self, numlines):
752 752 "scroll the screen up (down) by numlines when numlines >0 (<0)."
753 753 self.firstlineofpadtoprint += numlines
754 754 if self.firstlineofpadtoprint < 0:
755 755 self.firstlineofpadtoprint = 0
756 756 if self.firstlineofpadtoprint > self.numpadlines - 1:
757 757 self.firstlineofpadtoprint = self.numpadlines - 1
758 758
759 759 def toggleapply(self, item=None):
760 760 """
761 761 toggle the applied flag of the specified item. if no item is specified,
762 762 toggle the flag of the currently selected item.
763 763 """
764 764 if item is None:
765 765 item = self.currentselecteditem
766 766
767 767 item.applied = not item.applied
768 768
769 769 if isinstance(item, uiheader):
770 770 item.partial = False
771 771 if item.applied:
772 772 # apply all its hunks
773 773 for hnk in item.hunks:
774 774 hnk.applied = True
775 775 # apply all their hunklines
776 776 for hunkline in hnk.changedlines:
777 777 hunkline.applied = True
778 778 else:
779 779 # un-apply all its hunks
780 780 for hnk in item.hunks:
781 781 hnk.applied = False
782 782 hnk.partial = False
783 783 # un-apply all their hunklines
784 784 for hunkline in hnk.changedlines:
785 785 hunkline.applied = False
786 786 elif isinstance(item, uihunk):
787 787 item.partial = False
788 788 # apply all it's hunklines
789 789 for hunkline in item.changedlines:
790 790 hunkline.applied = item.applied
791 791
792 792 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
793 793 allsiblingsapplied = not (False in siblingappliedstatus)
794 794 nosiblingsapplied = not (True in siblingappliedstatus)
795 795
796 796 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
797 797 somesiblingspartial = (True in siblingspartialstatus)
798 798
799 799 #cases where applied or partial should be removed from header
800 800
801 801 # if no 'sibling' hunks are applied (including this hunk)
802 802 if nosiblingsapplied:
803 803 if not item.header.special():
804 804 item.header.applied = False
805 805 item.header.partial = False
806 806 else: # some/all parent siblings are applied
807 807 item.header.applied = True
808 808 item.header.partial = (somesiblingspartial or
809 809 not allsiblingsapplied)
810 810
811 811 elif isinstance(item, uihunkline):
812 812 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
813 813 allsiblingsapplied = not (False in siblingappliedstatus)
814 814 nosiblingsapplied = not (True in siblingappliedstatus)
815 815
816 816 # if no 'sibling' lines are applied
817 817 if nosiblingsapplied:
818 818 item.hunk.applied = False
819 819 item.hunk.partial = False
820 820 elif allsiblingsapplied:
821 821 item.hunk.applied = True
822 822 item.hunk.partial = False
823 823 else: # some siblings applied
824 824 item.hunk.applied = True
825 825 item.hunk.partial = True
826 826
827 827 parentsiblingsapplied = [hnk.applied for hnk
828 828 in item.hunk.header.hunks]
829 829 noparentsiblingsapplied = not (True in parentsiblingsapplied)
830 830 allparentsiblingsapplied = not (False in parentsiblingsapplied)
831 831
832 832 parentsiblingspartial = [hnk.partial for hnk
833 833 in item.hunk.header.hunks]
834 834 someparentsiblingspartial = (True in parentsiblingspartial)
835 835
836 836 # if all parent hunks are not applied, un-apply header
837 837 if noparentsiblingsapplied:
838 838 if not item.hunk.header.special():
839 839 item.hunk.header.applied = False
840 840 item.hunk.header.partial = False
841 841 # set the applied and partial status of the header if needed
842 842 else: # some/all parent siblings are applied
843 843 item.hunk.header.applied = True
844 844 item.hunk.header.partial = (someparentsiblingspartial or
845 845 not allparentsiblingsapplied)
846 846
847 847 def toggleall(self):
848 848 "toggle the applied flag of all items."
849 849 if self.waslasttoggleallapplied: # then unapply them this time
850 850 for item in self.headerlist:
851 851 if item.applied:
852 852 self.toggleapply(item)
853 853 else:
854 854 for item in self.headerlist:
855 855 if not item.applied:
856 856 self.toggleapply(item)
857 857 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
858 858
859 859 def togglefolded(self, item=None, foldparent=False):
860 860 "toggle folded flag of specified item (defaults to currently selected)"
861 861 if item is None:
862 862 item = self.currentselecteditem
863 863 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
864 864 if not isinstance(item, uiheader):
865 865 # we need to select the parent item in this case
866 866 self.currentselecteditem = item = item.parentitem()
867 867 elif item.neverunfolded:
868 868 item.neverunfolded = False
869 869
870 870 # also fold any foldable children of the parent/current item
871 871 if isinstance(item, uiheader): # the original or 'new' item
872 872 for child in item.allchildren():
873 873 child.folded = not item.folded
874 874
875 875 if isinstance(item, (uiheader, uihunk)):
876 876 item.folded = not item.folded
877 877
878 878
879 879 def alignstring(self, instr, window):
880 880 """
881 881 add whitespace to the end of a string in order to make it fill
882 882 the screen in the x direction. the current cursor position is
883 883 taken into account when making this calculation. the string can span
884 884 multiple lines.
885 885 """
886 886 y, xstart = window.getyx()
887 887 width = self.xscreensize
888 888 # turn tabs into spaces
889 889 instr = instr.expandtabs(4)
890 890 strwidth = encoding.colwidth(instr)
891 891 numspaces = (width - ((strwidth + xstart) % width) - 1)
892 892 return instr + " " * numspaces + "\n"
893 893
894 894 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
895 895 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
896 896 """
897 897 print the string, text, with the specified colors and attributes, to
898 898 the specified curses window object.
899 899
900 900 the foreground and background colors are of the form
901 901 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
902 902 magenta, red, white, yellow]. if pairname is provided, a color
903 903 pair will be looked up in the self.colorpairnames dictionary.
904 904
905 905 attrlist is a list containing text attributes in the form of
906 906 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
907 907 underline].
908 908
909 909 if align == True, whitespace is added to the printed string such that
910 910 the string stretches to the right border of the window.
911 911
912 912 if showwhtspc == True, trailing whitespace of a string is highlighted.
913 913 """
914 914 # preprocess the text, converting tabs to spaces
915 915 text = text.expandtabs(4)
916 916 # strip \n, and convert control characters to ^[char] representation
917 917 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
918 918 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
919 919
920 920 if pair is not None:
921 921 colorpair = pair
922 922 elif pairname is not None:
923 923 colorpair = self.colorpairnames[pairname]
924 924 else:
925 925 if fgcolor is None:
926 926 fgcolor = -1
927 927 if bgcolor is None:
928 928 bgcolor = -1
929 929 if (fgcolor, bgcolor) in self.colorpairs:
930 930 colorpair = self.colorpairs[(fgcolor, bgcolor)]
931 931 else:
932 932 colorpair = self.getcolorpair(fgcolor, bgcolor)
933 933 # add attributes if possible
934 934 if attrlist is None:
935 935 attrlist = []
936 936 if colorpair < 256:
937 937 # then it is safe to apply all attributes
938 938 for textattr in attrlist:
939 939 colorpair |= textattr
940 940 else:
941 941 # just apply a select few (safe?) attributes
942 942 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
943 943 if textattr in attrlist:
944 944 colorpair |= textattr
945 945
946 946 y, xstart = self.chunkpad.getyx()
947 947 t = "" # variable for counting lines printed
948 948 # if requested, show trailing whitespace
949 949 if showwhtspc:
950 950 origlen = len(text)
951 951 text = text.rstrip(' \n') # tabs have already been expanded
952 952 strippedlen = len(text)
953 953 numtrailingspaces = origlen - strippedlen
954 954
955 955 if towin:
956 956 window.addstr(text, colorpair)
957 957 t += text
958 958
959 959 if showwhtspc:
960 960 wscolorpair = colorpair | curses.A_REVERSE
961 961 if towin:
962 962 for i in range(numtrailingspaces):
963 963 window.addch(curses.ACS_CKBOARD, wscolorpair)
964 964 t += " " * numtrailingspaces
965 965
966 966 if align:
967 967 if towin:
968 968 extrawhitespace = self.alignstring("", window)
969 969 window.addstr(extrawhitespace, colorpair)
970 970 else:
971 971 # need to use t, since the x position hasn't incremented
972 972 extrawhitespace = self.alignstring(t, window)
973 973 t += extrawhitespace
974 974
975 975 # is reset to 0 at the beginning of printitem()
976 976
977 977 linesprinted = (xstart + len(t)) / self.xscreensize
978 978 self.linesprintedtopadsofar += linesprinted
979 979 return t
980 980
981 981 def updatescreen(self):
982 982 self.statuswin.erase()
983 983 self.chunkpad.erase()
984 984
985 985 printstring = self.printstring
986 986
987 987 # print out the status lines at the top
988 988 try:
989 989 if self.errorstr is not None:
990 990 printstring(self.statuswin, self.errorstr, pairname='legend')
991 991 printstring(self.statuswin, 'Press any key to continue',
992 992 pairname='legend')
993 993 self.statuswin.refresh()
994 994 return
995 995 line1 = ("SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
996 996 "(space/A) toggle hunk/all; (e)dit hunk;")
997 997 line2 = (" (f)old/unfold; (c)onfirm applied; (q)uit; (?) help "
998 998 "| [X]=hunk applied **=folded, toggle [a]mend mode")
999 999
1000 1000 printstring(self.statuswin,
1001 1001 util.ellipsis(line1, self.xscreensize - 1),
1002 1002 pairname="legend")
1003 1003 printstring(self.statuswin,
1004 1004 util.ellipsis(line2, self.xscreensize - 1),
1005 1005 pairname="legend")
1006 1006 except curses.error:
1007 1007 pass
1008 1008
1009 1009 # print out the patch in the remaining part of the window
1010 1010 try:
1011 1011 self.printitem()
1012 1012 self.updatescroll()
1013 1013 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1014 1014 self.numstatuslines, 0,
1015 1015 self.yscreensize + 1 - self.numstatuslines,
1016 1016 self.xscreensize)
1017 1017 except curses.error:
1018 1018 pass
1019 1019
1020 1020 # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
1021 1021 self.statuswin.refresh()
1022 1022
1023 1023 def getstatusprefixstring(self, item):
1024 1024 """
1025 1025 create a string to prefix a line with which indicates whether 'item'
1026 1026 is applied and/or folded.
1027 1027 """
1028 1028
1029 1029 # create checkbox string
1030 1030 if item.applied:
1031 1031 if not isinstance(item, uihunkline) and item.partial:
1032 1032 checkbox = "[~]"
1033 1033 else:
1034 1034 checkbox = "[x]"
1035 1035 else:
1036 1036 checkbox = "[ ]"
1037 1037
1038 1038 try:
1039 1039 if item.folded:
1040 1040 checkbox += "**"
1041 1041 if isinstance(item, uiheader):
1042 1042 # one of "m", "a", or "d" (modified, added, deleted)
1043 1043 filestatus = item.changetype
1044 1044
1045 1045 checkbox += filestatus + " "
1046 1046 else:
1047 1047 checkbox += " "
1048 1048 if isinstance(item, uiheader):
1049 1049 # add two more spaces for headers
1050 1050 checkbox += " "
1051 1051 except AttributeError: # not foldable
1052 1052 checkbox += " "
1053 1053
1054 1054 return checkbox
1055 1055
1056 1056 def printheader(self, header, selected=False, towin=True,
1057 1057 ignorefolding=False):
1058 1058 """
1059 1059 print the header to the pad. if countlines is True, don't print
1060 1060 anything, but just count the number of lines which would be printed.
1061 1061 """
1062 1062
1063 1063 outstr = ""
1064 1064 text = header.prettystr()
1065 1065 chunkindex = self.chunklist.index(header)
1066 1066
1067 1067 if chunkindex != 0 and not header.folded:
1068 1068 # add separating line before headers
1069 1069 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1070 1070 towin=towin, align=False)
1071 1071 # select color-pair based on if the header is selected
1072 1072 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1073 1073 attrlist=[curses.A_BOLD])
1074 1074
1075 1075 # print out each line of the chunk, expanding it to screen width
1076 1076
1077 1077 # number of characters to indent lines on this level by
1078 1078 indentnumchars = 0
1079 1079 checkbox = self.getstatusprefixstring(header)
1080 1080 if not header.folded or ignorefolding:
1081 1081 textlist = text.split("\n")
1082 1082 linestr = checkbox + textlist[0]
1083 1083 else:
1084 1084 linestr = checkbox + header.filename()
1085 1085 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1086 1086 towin=towin)
1087 1087 if not header.folded or ignorefolding:
1088 1088 if len(textlist) > 1:
1089 1089 for line in textlist[1:]:
1090 1090 linestr = " "*(indentnumchars + len(checkbox)) + line
1091 1091 outstr += self.printstring(self.chunkpad, linestr,
1092 1092 pair=colorpair, towin=towin)
1093 1093
1094 1094 return outstr
1095 1095
1096 1096 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1097 1097 ignorefolding=False):
1098 1098 "includes start/end line indicator"
1099 1099 outstr = ""
1100 1100 # where hunk is in list of siblings
1101 1101 hunkindex = hunk.header.hunks.index(hunk)
1102 1102
1103 1103 if hunkindex != 0:
1104 1104 # add separating line before headers
1105 1105 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1106 1106 towin=towin, align=False)
1107 1107
1108 1108 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1109 1109 attrlist=[curses.A_BOLD])
1110 1110
1111 1111 # print out from-to line with checkbox
1112 1112 checkbox = self.getstatusprefixstring(hunk)
1113 1113
1114 1114 lineprefix = " "*self.hunkindentnumchars + checkbox
1115 1115 frtoline = " " + hunk.getfromtoline().strip("\n")
1116 1116
1117 1117
1118 1118 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1119 1119 align=False) # add uncolored checkbox/indent
1120 1120 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1121 1121 towin=towin)
1122 1122
1123 1123 if hunk.folded and not ignorefolding:
1124 1124 # skip remainder of output
1125 1125 return outstr
1126 1126
1127 1127 # print out lines of the chunk preceeding changed-lines
1128 1128 for line in hunk.before:
1129 1129 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1130 1130 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1131 1131
1132 1132 return outstr
1133 1133
1134 1134 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1135 1135 outstr = ""
1136 1136 if hunk.folded and not ignorefolding:
1137 1137 return outstr
1138 1138
1139 1139 # a bit superfluous, but to avoid hard-coding indent amount
1140 1140 checkbox = self.getstatusprefixstring(hunk)
1141 1141 for line in hunk.after:
1142 1142 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1143 1143 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1144 1144
1145 1145 return outstr
1146 1146
1147 1147 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1148 1148 outstr = ""
1149 1149 checkbox = self.getstatusprefixstring(hunkline)
1150 1150
1151 1151 linestr = hunkline.prettystr().strip("\n")
1152 1152
1153 1153 # select color-pair based on whether line is an addition/removal
1154 1154 if selected:
1155 1155 colorpair = self.getcolorpair(name="selected")
1156 1156 elif linestr.startswith("+"):
1157 1157 colorpair = self.getcolorpair(name="addition")
1158 1158 elif linestr.startswith("-"):
1159 1159 colorpair = self.getcolorpair(name="deletion")
1160 1160 elif linestr.startswith("\\"):
1161 1161 colorpair = self.getcolorpair(name="normal")
1162 1162
1163 1163 lineprefix = " "*self.hunklineindentnumchars + checkbox
1164 1164 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1165 1165 align=False) # add uncolored checkbox/indent
1166 1166 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1167 1167 towin=towin, showwhtspc=True)
1168 1168 return outstr
1169 1169
1170 1170 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1171 1171 towin=True):
1172 1172 """
1173 1173 use __printitem() to print the the specified item.applied.
1174 1174 if item is not specified, then print the entire patch.
1175 1175 (hiding folded elements, etc. -- see __printitem() docstring)
1176 1176 """
1177 1177
1178 1178 if item is None:
1179 1179 item = self.headerlist
1180 1180 if recursechildren:
1181 1181 self.linesprintedtopadsofar = 0
1182 1182
1183 1183 outstr = []
1184 1184 self.__printitem(item, ignorefolding, recursechildren, outstr,
1185 1185 towin=towin)
1186 1186 return ''.join(outstr)
1187 1187
1188 1188 def outofdisplayedarea(self):
1189 1189 y, _ = self.chunkpad.getyx() # cursor location
1190 1190 # * 2 here works but an optimization would be the max number of
1191 1191 # consecutive non selectable lines
1192 1192 # i.e the max number of context line for any hunk in the patch
1193 1193 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1194 1194 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1195 1195 return y < miny or y > maxy
1196 1196
1197 1197 def handleselection(self, item, recursechildren):
1198 1198 selected = (item is self.currentselecteditem)
1199 1199 if selected and recursechildren:
1200 1200 # assumes line numbering starting from line 0
1201 1201 self.selecteditemstartline = self.linesprintedtopadsofar
1202 1202 selecteditemlines = self.getnumlinesdisplayed(item,
1203 1203 recursechildren=False)
1204 1204 self.selecteditemendline = (self.selecteditemstartline +
1205 1205 selecteditemlines - 1)
1206 1206 return selected
1207 1207
1208 1208 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1209 1209 towin=True):
1210 1210 """
1211 1211 recursive method for printing out patch/header/hunk/hunk-line data to
1212 1212 screen. also returns a string with all of the content of the displayed
1213 1213 patch (not including coloring, etc.).
1214 1214
1215 1215 if ignorefolding is True, then folded items are printed out.
1216 1216
1217 1217 if recursechildren is False, then only print the item without its
1218 1218 child items.
1219 1219 """
1220 1220
1221 1221 if towin and self.outofdisplayedarea():
1222 1222 return
1223 1223
1224 1224 selected = self.handleselection(item, recursechildren)
1225 1225
1226 1226 # patch object is a list of headers
1227 1227 if isinstance(item, patch):
1228 1228 if recursechildren:
1229 1229 for hdr in item:
1230 1230 self.__printitem(hdr, ignorefolding,
1231 1231 recursechildren, outstr, towin)
1232 1232 # todo: eliminate all isinstance() calls
1233 1233 if isinstance(item, uiheader):
1234 1234 outstr.append(self.printheader(item, selected, towin=towin,
1235 1235 ignorefolding=ignorefolding))
1236 1236 if recursechildren:
1237 1237 for hnk in item.hunks:
1238 1238 self.__printitem(hnk, ignorefolding,
1239 1239 recursechildren, outstr, towin)
1240 1240 elif (isinstance(item, uihunk) and
1241 1241 ((not item.header.folded) or ignorefolding)):
1242 1242 # print the hunk data which comes before the changed-lines
1243 1243 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1244 1244 ignorefolding=ignorefolding))
1245 1245 if recursechildren:
1246 1246 for l in item.changedlines:
1247 1247 self.__printitem(l, ignorefolding,
1248 1248 recursechildren, outstr, towin)
1249 1249 outstr.append(self.printhunklinesafter(item, towin=towin,
1250 1250 ignorefolding=ignorefolding))
1251 1251 elif (isinstance(item, uihunkline) and
1252 1252 ((not item.hunk.folded) or ignorefolding)):
1253 1253 outstr.append(self.printhunkchangedline(item, selected,
1254 1254 towin=towin))
1255 1255
1256 1256 return outstr
1257 1257
1258 1258 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1259 1259 recursechildren=True):
1260 1260 """
1261 1261 return the number of lines which would be displayed if the item were
1262 1262 to be printed to the display. the item will not be printed to the
1263 1263 display (pad).
1264 1264 if no item is given, assume the entire patch.
1265 1265 if ignorefolding is True, folded items will be unfolded when counting
1266 1266 the number of lines.
1267 1267 """
1268 1268
1269 1269 # temporarily disable printing to windows by printstring
1270 1270 patchdisplaystring = self.printitem(item, ignorefolding,
1271 1271 recursechildren, towin=False)
1272 1272 numlines = len(patchdisplaystring) / self.xscreensize
1273 1273 return numlines
1274 1274
1275 1275 def sigwinchhandler(self, n, frame):
1276 1276 "handle window resizing"
1277 1277 try:
1278 1278 curses.endwin()
1279 1279 self.yscreensize, self.xscreensize = gethw()
1280 1280 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1281 1281 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1282 1282 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1283 1283 # todo: try to resize commit message window if possible
1284 1284 except curses.error:
1285 1285 pass
1286 1286
1287 1287 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1288 1288 attrlist=None):
1289 1289 """
1290 1290 get a curses color pair, adding it to self.colorpairs if it is not
1291 1291 already defined. an optional string, name, can be passed as a shortcut
1292 1292 for referring to the color-pair. by default, if no arguments are
1293 1293 specified, the white foreground / black background color-pair is
1294 1294 returned.
1295 1295
1296 1296 it is expected that this function will be used exclusively for
1297 1297 initializing color pairs, and not curses.init_pair().
1298 1298
1299 1299 attrlist is used to 'flavor' the returned color-pair. this information
1300 1300 is not stored in self.colorpairs. it contains attribute values like
1301 1301 curses.A_BOLD.
1302 1302 """
1303 1303
1304 1304 if (name is not None) and name in self.colorpairnames:
1305 1305 # then get the associated color pair and return it
1306 1306 colorpair = self.colorpairnames[name]
1307 1307 else:
1308 1308 if fgcolor is None:
1309 1309 fgcolor = -1
1310 1310 if bgcolor is None:
1311 1311 bgcolor = -1
1312 1312 if (fgcolor, bgcolor) in self.colorpairs:
1313 1313 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1314 1314 else:
1315 1315 pairindex = len(self.colorpairs) + 1
1316 1316 curses.init_pair(pairindex, fgcolor, bgcolor)
1317 1317 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1318 1318 curses.color_pair(pairindex))
1319 1319 if name is not None:
1320 1320 self.colorpairnames[name] = curses.color_pair(pairindex)
1321 1321
1322 1322 # add attributes if possible
1323 1323 if attrlist is None:
1324 1324 attrlist = []
1325 1325 if colorpair < 256:
1326 1326 # then it is safe to apply all attributes
1327 1327 for textattr in attrlist:
1328 1328 colorpair |= textattr
1329 1329 else:
1330 1330 # just apply a select few (safe?) attributes
1331 1331 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1332 1332 if textattrib in attrlist:
1333 1333 colorpair |= textattrib
1334 1334 return colorpair
1335 1335
1336 1336 def initcolorpair(self, *args, **kwargs):
1337 1337 "same as getcolorpair."
1338 1338 self.getcolorpair(*args, **kwargs)
1339 1339
1340 1340 def helpwindow(self):
1341 1341 "print a help window to the screen. exit after any keypress."
1342 1342 helptext = """ [press any key to return to the patch-display]
1343 1343
1344 1344 crecord allows you to interactively choose among the changes you have made,
1345 1345 and confirm only those changes you select for further processing by the command
1346 1346 you are running (commit/shelve/revert), after confirming the selected
1347 1347 changes, the unselected changes are still present in your working copy, so you
1348 1348 can use crecord multiple times to split large changes into smaller changesets.
1349 1349 the following are valid keystrokes:
1350 1350
1351 1351 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1352 1352 A : (un-)select all items
1353 1353 up/down-arrow [k/j] : go to previous/next unfolded item
1354 1354 pgup/pgdn [K/J] : go to previous/next item of same type
1355 1355 right/left-arrow [l/h] : go to child item / parent item
1356 1356 shift-left-arrow [H] : go to parent header / fold selected header
1357 1357 f : fold / unfold item, hiding/revealing its children
1358 1358 F : fold / unfold parent item and all of its ancestors
1359 1359 m : edit / resume editing the commit message
1360 1360 e : edit the currently selected hunk
1361 1361 a : toggle amend mode (hg rev >= 2.2), only with commit -i
1362 1362 c : confirm selected changes
1363 1363 r : review/edit and confirm selected changes
1364 1364 q : quit without confirming (no changes will be made)
1365 1365 ? : help (what you're currently reading)"""
1366 1366
1367 1367 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1368 1368 helplines = helptext.split("\n")
1369 1369 helplines = helplines + [" "]*(
1370 1370 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1371 1371 try:
1372 1372 for line in helplines:
1373 1373 self.printstring(helpwin, line, pairname="legend")
1374 1374 except curses.error:
1375 1375 pass
1376 1376 helpwin.refresh()
1377 1377 try:
1378 1378 helpwin.getkey()
1379 1379 except curses.error:
1380 1380 pass
1381 1381
1382 1382 def confirmationwindow(self, windowtext):
1383 1383 "display an informational window, then wait for and return a keypress."
1384 1384
1385 1385 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1386 1386 try:
1387 1387 lines = windowtext.split("\n")
1388 1388 for line in lines:
1389 1389 self.printstring(confirmwin, line, pairname="selected")
1390 1390 except curses.error:
1391 1391 pass
1392 1392 self.stdscr.refresh()
1393 1393 confirmwin.refresh()
1394 1394 try:
1395 1395 response = chr(self.stdscr.getch())
1396 1396 except ValueError:
1397 1397 response = None
1398 1398
1399 1399 return response
1400 1400
1401 1401 def confirmcommit(self, review=False):
1402 1402 """ask for 'y' to be pressed to confirm selected. return True if
1403 1403 confirmed."""
1404 1404 if review:
1405 1405 confirmtext = (
1406 1406 """if you answer yes to the following, the your currently chosen patch chunks
1407 1407 will be loaded into an editor. you may modify the patch from the editor, and
1408 1408 save the changes if you wish to change the patch. otherwise, you can just
1409 1409 close the editor without saving to accept the current patch as-is.
1410 1410
1411 1411 note: don't add/remove lines unless you also modify the range information.
1412 1412 failing to follow this rule will result in the commit aborting.
1413 1413
1414 1414 are you sure you want to review/edit and confirm the selected changes [yn]?
1415 1415 """)
1416 1416 else:
1417 1417 confirmtext = (
1418 1418 "are you sure you want to confirm the selected changes [yn]? ")
1419 1419
1420 1420 response = self.confirmationwindow(confirmtext)
1421 1421 if response is None:
1422 1422 response = "n"
1423 1423 if response.lower().startswith("y"):
1424 1424 return True
1425 1425 else:
1426 1426 return False
1427 1427
1428 1428 def toggleamend(self, opts, test):
1429 1429 """Toggle the amend flag.
1430 1430
1431 1431 When the amend flag is set, a commit will modify the most recently
1432 1432 committed changeset, instead of creating a new changeset. Otherwise, a
1433 1433 new changeset will be created (the normal commit behavior).
1434 1434 """
1435 1435
1436 1436 try:
1437 1437 ver = float(util.version()[:3])
1438 1438 except ValueError:
1439 1439 ver = 1
1440 1440 if ver < 2.19:
1441 1441 msg = ("The amend option is unavailable with hg versions < 2.2\n\n"
1442 1442 "Press any key to continue.")
1443 1443 elif opts.get('amend') is None:
1444 1444 opts['amend'] = True
1445 1445 msg = ("Amend option is turned on -- commiting the currently "
1446 1446 "selected changes will not create a new changeset, but "
1447 1447 "instead update the most recently committed changeset.\n\n"
1448 1448 "Press any key to continue.")
1449 1449 elif opts.get('amend') is True:
1450 1450 opts['amend'] = None
1451 1451 msg = ("Amend option is turned off -- commiting the currently "
1452 1452 "selected changes will create a new changeset.\n\n"
1453 1453 "Press any key to continue.")
1454 1454 if not test:
1455 1455 self.confirmationwindow(msg)
1456 1456
1457 1457 def recenterdisplayedarea(self):
1458 1458 """
1459 1459 once we scrolled with pg up pg down we can be pointing outside of the
1460 1460 display zone. we print the patch with towin=False to compute the
1461 1461 location of the selected item even though it is outside of the displayed
1462 1462 zone and then update the scroll.
1463 1463 """
1464 1464 self.printitem(towin=False)
1465 1465 self.updatescroll()
1466 1466
1467 1467 def toggleedit(self, item=None, test=False):
1468 1468 """
1469 1469 edit the currently selected chunk
1470 1470 """
1471 1471 def updateui(self):
1472 1472 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1473 1473 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1474 1474 self.updatescroll()
1475 1475 self.stdscr.refresh()
1476 1476 self.statuswin.refresh()
1477 1477 self.stdscr.keypad(1)
1478 1478
1479 1479 def editpatchwitheditor(self, chunk):
1480 1480 if chunk is None:
1481 1481 self.ui.write(_('cannot edit patch for whole file'))
1482 1482 self.ui.write("\n")
1483 1483 return None
1484 1484 if chunk.header.binary():
1485 1485 self.ui.write(_('cannot edit patch for binary file'))
1486 1486 self.ui.write("\n")
1487 1487 return None
1488 1488 # patch comment based on the git one (based on comment at end of
1489 1489 # https://mercurial-scm.org/wiki/recordextension)
1490 1490 phelp = '---' + _("""
1491 1491 to remove '-' lines, make them ' ' lines (context).
1492 1492 to remove '+' lines, delete them.
1493 1493 lines starting with # will be removed from the patch.
1494 1494
1495 1495 if the patch applies cleanly, the edited hunk will immediately be
1496 1496 added to the record list. if it does not apply cleanly, a rejects
1497 1497 file will be generated: you can use that when you try again. if
1498 1498 all lines of the hunk are removed, then the edit is aborted and
1499 1499 the hunk is left unchanged.
1500 1500 """)
1501 1501 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1502 1502 suffix=".diff", text=True)
1503 1503 ncpatchfp = None
1504 1504 try:
1505 1505 # write the initial patch
1506 1506 f = os.fdopen(patchfd, "w")
1507 1507 chunk.header.write(f)
1508 1508 chunk.write(f)
1509 1509 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1510 1510 f.close()
1511 1511 # start the editor and wait for it to complete
1512 1512 editor = self.ui.geteditor()
1513 1513 ret = self.ui.system("%s \"%s\"" % (editor, patchfn),
1514 1514 environ={'hguser': self.ui.username()})
1515 1515 if ret != 0:
1516 1516 self.errorstr = "Editor exited with status %d" % ret
1517 1517 return None
1518 1518 # remove comment lines
1519 1519 patchfp = open(patchfn)
1520 1520 ncpatchfp = cStringIO.StringIO()
1521 1521 for line in patchfp:
1522 1522 if not line.startswith('#'):
1523 1523 ncpatchfp.write(line)
1524 1524 patchfp.close()
1525 1525 ncpatchfp.seek(0)
1526 1526 newpatches = patchmod.parsepatch(ncpatchfp)
1527 1527 finally:
1528 1528 os.unlink(patchfn)
1529 1529 del ncpatchfp
1530 1530 return newpatches
1531 1531 if item is None:
1532 1532 item = self.currentselecteditem
1533 1533 if isinstance(item, uiheader):
1534 1534 return
1535 1535 if isinstance(item, uihunkline):
1536 1536 item = item.parentitem()
1537 1537 if not isinstance(item, uihunk):
1538 1538 return
1539 1539
1540 1540 # To go back to that hunk or its replacement at the end of the edit
1541 1541 itemindex = item.parentitem().hunks.index(item)
1542 1542
1543 1543 beforeadded, beforeremoved = item.added, item.removed
1544 1544 newpatches = editpatchwitheditor(self, item)
1545 1545 if newpatches is None:
1546 1546 if not test:
1547 1547 updateui(self)
1548 1548 return
1549 1549 header = item.header
1550 1550 editedhunkindex = header.hunks.index(item)
1551 1551 hunksbefore = header.hunks[:editedhunkindex]
1552 1552 hunksafter = header.hunks[editedhunkindex + 1:]
1553 1553 newpatchheader = newpatches[0]
1554 1554 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1555 1555 newadded = sum([h.added for h in newhunks])
1556 1556 newremoved = sum([h.removed for h in newhunks])
1557 1557 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1558 1558
1559 1559 for h in hunksafter:
1560 1560 h.toline += offset
1561 1561 for h in newhunks:
1562 1562 h.folded = False
1563 1563 header.hunks = hunksbefore + newhunks + hunksafter
1564 1564 if self.emptypatch():
1565 1565 header.hunks = hunksbefore + [item] + hunksafter
1566 1566 self.currentselecteditem = header
1567 1567 if len(header.hunks) > itemindex:
1568 1568 self.currentselecteditem = header.hunks[itemindex]
1569 1569
1570 1570 if not test:
1571 1571 updateui(self)
1572 1572
1573 1573 def emptypatch(self):
1574 1574 item = self.headerlist
1575 1575 if not item:
1576 1576 return True
1577 1577 for header in item:
1578 1578 if header.hunks:
1579 1579 return False
1580 1580 return True
1581 1581
1582 1582 def handlekeypressed(self, keypressed, test=False):
1583 """
1584 Perform actions based on pressed keys.
1585
1586 Return true to exit the main loop.
1587 """
1583 1588 if keypressed in ["k", "KEY_UP"]:
1584 1589 self.uparrowevent()
1585 1590 if keypressed in ["K", "KEY_PPAGE"]:
1586 1591 self.uparrowshiftevent()
1587 1592 elif keypressed in ["j", "KEY_DOWN"]:
1588 1593 self.downarrowevent()
1589 1594 elif keypressed in ["J", "KEY_NPAGE"]:
1590 1595 self.downarrowshiftevent()
1591 1596 elif keypressed in ["l", "KEY_RIGHT"]:
1592 1597 self.rightarrowevent()
1593 1598 elif keypressed in ["h", "KEY_LEFT"]:
1594 1599 self.leftarrowevent()
1595 1600 elif keypressed in ["H", "KEY_SLEFT"]:
1596 1601 self.leftarrowshiftevent()
1597 1602 elif keypressed in ["q"]:
1598 1603 raise error.Abort(_('user quit'))
1599 1604 elif keypressed in ['a']:
1600 1605 self.toggleamend(self.opts, test)
1601 1606 elif keypressed in ["c"]:
1602 1607 if self.confirmcommit():
1603 1608 return True
1604 1609 elif keypressed in ["r"]:
1605 1610 if self.confirmcommit(review=True):
1606 1611 return True
1607 1612 elif test and keypressed in ['X']:
1608 1613 return True
1609 1614 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1610 1615 self.toggleapply()
1611 1616 elif keypressed in ['A']:
1612 1617 self.toggleall()
1613 1618 elif keypressed in ['e']:
1614 1619 self.toggleedit(test=test)
1615 1620 elif keypressed in ["f"]:
1616 1621 self.togglefolded()
1617 1622 elif keypressed in ["F"]:
1618 1623 self.togglefolded(foldparent=True)
1619 1624 elif keypressed in ["?"]:
1620 1625 self.helpwindow()
1621 1626 self.stdscr.clear()
1622 1627 self.stdscr.refresh()
1623 1628
1624 1629 def main(self, stdscr):
1625 1630 """
1626 1631 method to be wrapped by curses.wrapper() for selecting chunks.
1627 1632 """
1628 1633
1629 1634 signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1630 1635 self.stdscr = stdscr
1631 1636 # error during initialization, cannot be printed in the curses
1632 1637 # interface, it should be printed by the calling code
1633 1638 self.initerr = None
1634 1639 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1635 1640
1636 1641 curses.start_color()
1637 1642 curses.use_default_colors()
1638 1643
1639 1644 # available colors: black, blue, cyan, green, magenta, white, yellow
1640 1645 # init_pair(color_id, foreground_color, background_color)
1641 1646 self.initcolorpair(None, None, name="normal")
1642 1647 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1643 1648 name="selected")
1644 1649 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1645 1650 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1646 1651 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1647 1652 # newwin([height, width,] begin_y, begin_x)
1648 1653 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1649 1654 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1650 1655
1651 1656 # figure out how much space to allocate for the chunk-pad which is
1652 1657 # used for displaying the patch
1653 1658
1654 1659 # stupid hack to prevent getnumlinesdisplayed from failing
1655 1660 self.chunkpad = curses.newpad(1, self.xscreensize)
1656 1661
1657 1662 # add 1 so to account for last line text reaching end of line
1658 1663 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1659 1664
1660 1665 try:
1661 1666 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1662 1667 except curses.error:
1663 1668 self.initerr = _('this diff is too large to be displayed')
1664 1669 return
1665 1670 # initialize selecteitemendline (initial start-line is 0)
1666 1671 self.selecteditemendline = self.getnumlinesdisplayed(
1667 1672 self.currentselecteditem, recursechildren=False)
1668 1673
1669 1674 while True:
1670 1675 self.updatescreen()
1671 1676 try:
1672 1677 keypressed = self.statuswin.getkey()
1673 1678 if self.errorstr is not None:
1674 1679 self.errorstr = None
1675 1680 continue
1676 1681 except curses.error:
1677 1682 keypressed = "foobar"
1678 1683 if self.handlekeypressed(keypressed):
1679 1684 break
General Comments 0
You need to be logged in to leave comments. Login now