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