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