##// END OF EJS Templates
record: add message when starting record's curses interface...
Laurent Charignon -
r24779:23727465 default
parent child Browse files
Show More
@@ -1,1596 +1,1597 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 ui.write(_('starting interactive selection\n'))
480 481 chunkselector = curseschunkselector(headerlist, ui)
481 482 curses.wrapper(chunkselector.main)
482 483
483 484 def testdecorator(testfn, f):
484 485 def u(*args, **kwargs):
485 486 return f(testfn, *args, **kwargs)
486 487 return u
487 488
488 489 def testchunkselector(testfn, ui, headerlist):
489 490 """
490 491 test interface to get selection of chunks, and mark the applied flags
491 492 of the chosen chunks.
492 493
493 494 """
494 495 chunkselector = curseschunkselector(headerlist, ui)
495 496 if testfn and os.path.exists(testfn):
496 497 testf = open(testfn)
497 498 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
498 499 testf.close()
499 500 while True:
500 501 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
501 502 break
502 503
503 504 class curseschunkselector(object):
504 505 def __init__(self, headerlist, ui):
505 506 # put the headers into a patch object
506 507 self.headerlist = patch(headerlist)
507 508
508 509 self.ui = ui
509 510
510 511 # list of all chunks
511 512 self.chunklist = []
512 513 for h in headerlist:
513 514 self.chunklist.append(h)
514 515 self.chunklist.extend(h.hunks)
515 516
516 517 # dictionary mapping (fgcolor, bgcolor) pairs to the
517 518 # corresponding curses color-pair value.
518 519 self.colorpairs = {}
519 520 # maps custom nicknames of color-pairs to curses color-pair values
520 521 self.colorpairnames = {}
521 522
522 523 # the currently selected header, hunk, or hunk-line
523 524 self.currentselecteditem = self.headerlist[0]
524 525
525 526 # updated when printing out patch-display -- the 'lines' here are the
526 527 # line positions *in the pad*, not on the screen.
527 528 self.selecteditemstartline = 0
528 529 self.selecteditemendline = None
529 530
530 531 # define indentation levels
531 532 self.headerindentnumchars = 0
532 533 self.hunkindentnumchars = 3
533 534 self.hunklineindentnumchars = 6
534 535
535 536 # the first line of the pad to print to the screen
536 537 self.firstlineofpadtoprint = 0
537 538
538 539 # keeps track of the number of lines in the pad
539 540 self.numpadlines = None
540 541
541 542 self.numstatuslines = 2
542 543
543 544 # keep a running count of the number of lines printed to the pad
544 545 # (used for determining when the selected item begins/ends)
545 546 self.linesprintedtopadsofar = 0
546 547
547 548 # the first line of the pad which is visible on the screen
548 549 self.firstlineofpadtoprint = 0
549 550
550 551 # stores optional text for a commit comment provided by the user
551 552 self.commenttext = ""
552 553
553 554 # if the last 'toggle all' command caused all changes to be applied
554 555 self.waslasttoggleallapplied = True
555 556
556 557 def uparrowevent(self):
557 558 """
558 559 try to select the previous item to the current item that has the
559 560 most-indented level. for example, if a hunk is selected, try to select
560 561 the last hunkline of the hunk prior to the selected hunk. or, if
561 562 the first hunkline of a hunk is currently selected, then select the
562 563 hunk itself.
563 564
564 565 if the currently selected item is already at the top of the screen,
565 566 scroll the screen down to show the new-selected item.
566 567
567 568 """
568 569 currentitem = self.currentselecteditem
569 570
570 571 nextitem = currentitem.previtem(constrainlevel=False)
571 572
572 573 if nextitem is None:
573 574 # if no parent item (i.e. currentitem is the first header), then
574 575 # no change...
575 576 nextitem = currentitem
576 577
577 578 self.currentselecteditem = nextitem
578 579
579 580 def uparrowshiftevent(self):
580 581 """
581 582 select (if possible) the previous item on the same level as the
582 583 currently selected item. otherwise, select (if possible) the
583 584 parent-item of the currently selected item.
584 585
585 586 if the currently selected item is already at the top of the screen,
586 587 scroll the screen down to show the new-selected item.
587 588
588 589 """
589 590 currentitem = self.currentselecteditem
590 591 nextitem = currentitem.previtem()
591 592 # if there's no previous item on this level, try choosing the parent
592 593 if nextitem is None:
593 594 nextitem = currentitem.parentitem()
594 595 if nextitem is None:
595 596 # if no parent item (i.e. currentitem is the first header), then
596 597 # no change...
597 598 nextitem = currentitem
598 599
599 600 self.currentselecteditem = nextitem
600 601
601 602 def downarrowevent(self):
602 603 """
603 604 try to select the next item to the current item that has the
604 605 most-indented level. for example, if a hunk is selected, select
605 606 the first hunkline of the selected hunk. or, if the last hunkline of
606 607 a hunk is currently selected, then select the next hunk, if one exists,
607 608 or if not, the next header if one exists.
608 609
609 610 if the currently selected item is already at the bottom of the screen,
610 611 scroll the screen up to show the new-selected item.
611 612
612 613 """
613 614 #self.startprintline += 1 #debug
614 615 currentitem = self.currentselecteditem
615 616
616 617 nextitem = currentitem.nextitem(constrainlevel=False)
617 618 # if there's no next item, keep the selection as-is
618 619 if nextitem is None:
619 620 nextitem = currentitem
620 621
621 622 self.currentselecteditem = nextitem
622 623
623 624 def downarrowshiftevent(self):
624 625 """
625 626 if the cursor is already at the bottom chunk, scroll the screen up and
626 627 move the cursor-position to the subsequent chunk. otherwise, only move
627 628 the cursor position down one chunk.
628 629
629 630 """
630 631 # todo: update docstring
631 632
632 633 currentitem = self.currentselecteditem
633 634 nextitem = currentitem.nextitem()
634 635 # if there's no previous item on this level, try choosing the parent's
635 636 # nextitem.
636 637 if nextitem is None:
637 638 try:
638 639 nextitem = currentitem.parentitem().nextitem()
639 640 except AttributeError:
640 641 # parentitem returned None, so nextitem() can't be called
641 642 nextitem = None
642 643 if nextitem is None:
643 644 # if no next item on parent-level, then no change...
644 645 nextitem = currentitem
645 646
646 647 self.currentselecteditem = nextitem
647 648
648 649 def rightarrowevent(self):
649 650 """
650 651 select (if possible) the first of this item's child-items.
651 652
652 653 """
653 654 currentitem = self.currentselecteditem
654 655 nextitem = currentitem.firstchild()
655 656
656 657 # turn off folding if we want to show a child-item
657 658 if currentitem.folded:
658 659 self.togglefolded(currentitem)
659 660
660 661 if nextitem is None:
661 662 # if no next item on parent-level, then no change...
662 663 nextitem = currentitem
663 664
664 665 self.currentselecteditem = nextitem
665 666
666 667 def leftarrowevent(self):
667 668 """
668 669 if the current item can be folded (i.e. it is an unfolded header or
669 670 hunk), then fold it. otherwise try select (if possible) the parent
670 671 of this item.
671 672
672 673 """
673 674 currentitem = self.currentselecteditem
674 675
675 676 # try to fold the item
676 677 if not isinstance(currentitem, uihunkline):
677 678 if not currentitem.folded:
678 679 self.togglefolded(item=currentitem)
679 680 return
680 681
681 682 # if it can't be folded, try to select the parent item
682 683 nextitem = currentitem.parentitem()
683 684
684 685 if nextitem is None:
685 686 # if no item on parent-level, then no change...
686 687 nextitem = currentitem
687 688 if not nextitem.folded:
688 689 self.togglefolded(item=nextitem)
689 690
690 691 self.currentselecteditem = nextitem
691 692
692 693 def leftarrowshiftevent(self):
693 694 """
694 695 select the header of the current item (or fold current item if the
695 696 current item is already a header).
696 697
697 698 """
698 699 currentitem = self.currentselecteditem
699 700
700 701 if isinstance(currentitem, uiheader):
701 702 if not currentitem.folded:
702 703 self.togglefolded(item=currentitem)
703 704 return
704 705
705 706 # select the parent item recursively until we're at a header
706 707 while True:
707 708 nextitem = currentitem.parentitem()
708 709 if nextitem is None:
709 710 break
710 711 else:
711 712 currentitem = nextitem
712 713
713 714 self.currentselecteditem = currentitem
714 715
715 716 def updatescroll(self):
716 717 "scroll the screen to fully show the currently-selected"
717 718 selstart = self.selecteditemstartline
718 719 selend = self.selecteditemendline
719 720 #selnumlines = selend - selstart
720 721 padstart = self.firstlineofpadtoprint
721 722 padend = padstart + self.yscreensize - self.numstatuslines - 1
722 723 # 'buffered' pad start/end values which scroll with a certain
723 724 # top/bottom context margin
724 725 padstartbuffered = padstart + 3
725 726 padendbuffered = padend - 3
726 727
727 728 if selend > padendbuffered:
728 729 self.scrolllines(selend - padendbuffered)
729 730 elif selstart < padstartbuffered:
730 731 # negative values scroll in pgup direction
731 732 self.scrolllines(selstart - padstartbuffered)
732 733
733 734
734 735 def scrolllines(self, numlines):
735 736 "scroll the screen up (down) by numlines when numlines >0 (<0)."
736 737 self.firstlineofpadtoprint += numlines
737 738 if self.firstlineofpadtoprint < 0:
738 739 self.firstlineofpadtoprint = 0
739 740 if self.firstlineofpadtoprint > self.numpadlines - 1:
740 741 self.firstlineofpadtoprint = self.numpadlines - 1
741 742
742 743 def toggleapply(self, item=None):
743 744 """
744 745 toggle the applied flag of the specified item. if no item is specified,
745 746 toggle the flag of the currently selected item.
746 747
747 748 """
748 749 if item is None:
749 750 item = self.currentselecteditem
750 751
751 752 item.applied = not item.applied
752 753
753 754 if isinstance(item, uiheader):
754 755 item.partial = False
755 756 if item.applied:
756 757 # apply all its hunks
757 758 for hnk in item.hunks:
758 759 hnk.applied = True
759 760 # apply all their hunklines
760 761 for hunkline in hnk.changedlines:
761 762 hunkline.applied = True
762 763 else:
763 764 # un-apply all its hunks
764 765 for hnk in item.hunks:
765 766 hnk.applied = False
766 767 hnk.partial = False
767 768 # un-apply all their hunklines
768 769 for hunkline in hnk.changedlines:
769 770 hunkline.applied = False
770 771 elif isinstance(item, uihunk):
771 772 item.partial = False
772 773 # apply all it's hunklines
773 774 for hunkline in item.changedlines:
774 775 hunkline.applied = item.applied
775 776
776 777 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
777 778 allsiblingsapplied = not (False in siblingappliedstatus)
778 779 nosiblingsapplied = not (True in siblingappliedstatus)
779 780
780 781 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
781 782 somesiblingspartial = (True in siblingspartialstatus)
782 783
783 784 #cases where applied or partial should be removed from header
784 785
785 786 # if no 'sibling' hunks are applied (including this hunk)
786 787 if nosiblingsapplied:
787 788 if not item.header.special():
788 789 item.header.applied = False
789 790 item.header.partial = False
790 791 else: # some/all parent siblings are applied
791 792 item.header.applied = True
792 793 item.header.partial = (somesiblingspartial or
793 794 not allsiblingsapplied)
794 795
795 796 elif isinstance(item, uihunkline):
796 797 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
797 798 allsiblingsapplied = not (False in siblingappliedstatus)
798 799 nosiblingsapplied = not (True in siblingappliedstatus)
799 800
800 801 # if no 'sibling' lines are applied
801 802 if nosiblingsapplied:
802 803 item.hunk.applied = False
803 804 item.hunk.partial = False
804 805 elif allsiblingsapplied:
805 806 item.hunk.applied = True
806 807 item.hunk.partial = False
807 808 else: # some siblings applied
808 809 item.hunk.applied = True
809 810 item.hunk.partial = True
810 811
811 812 parentsiblingsapplied = [hnk.applied for hnk
812 813 in item.hunk.header.hunks]
813 814 noparentsiblingsapplied = not (True in parentsiblingsapplied)
814 815 allparentsiblingsapplied = not (False in parentsiblingsapplied)
815 816
816 817 parentsiblingspartial = [hnk.partial for hnk
817 818 in item.hunk.header.hunks]
818 819 someparentsiblingspartial = (True in parentsiblingspartial)
819 820
820 821 # if all parent hunks are not applied, un-apply header
821 822 if noparentsiblingsapplied:
822 823 if not item.hunk.header.special():
823 824 item.hunk.header.applied = False
824 825 item.hunk.header.partial = False
825 826 # set the applied and partial status of the header if needed
826 827 else: # some/all parent siblings are applied
827 828 item.hunk.header.applied = True
828 829 item.hunk.header.partial = (someparentsiblingspartial or
829 830 not allparentsiblingsapplied)
830 831
831 832 def toggleall(self):
832 833 "toggle the applied flag of all items."
833 834 if self.waslasttoggleallapplied: # then unapply them this time
834 835 for item in self.headerlist:
835 836 if item.applied:
836 837 self.toggleapply(item)
837 838 else:
838 839 for item in self.headerlist:
839 840 if not item.applied:
840 841 self.toggleapply(item)
841 842 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
842 843
843 844 def togglefolded(self, item=None, foldparent=False):
844 845 "toggle folded flag of specified item (defaults to currently selected)"
845 846 if item is None:
846 847 item = self.currentselecteditem
847 848 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
848 849 if not isinstance(item, uiheader):
849 850 # we need to select the parent item in this case
850 851 self.currentselecteditem = item = item.parentitem()
851 852 elif item.neverunfolded:
852 853 item.neverunfolded = False
853 854
854 855 # also fold any foldable children of the parent/current item
855 856 if isinstance(item, uiheader): # the original or 'new' item
856 857 for child in item.allchildren():
857 858 child.folded = not item.folded
858 859
859 860 if isinstance(item, (uiheader, uihunk)):
860 861 item.folded = not item.folded
861 862
862 863
863 864 def alignstring(self, instr, window):
864 865 """
865 866 add whitespace to the end of a string in order to make it fill
866 867 the screen in the x direction. the current cursor position is
867 868 taken into account when making this calculation. the string can span
868 869 multiple lines.
869 870
870 871 """
871 872 y, xstart = window.getyx()
872 873 width = self.xscreensize
873 874 # turn tabs into spaces
874 875 instr = instr.expandtabs(4)
875 876 strwidth = encoding.colwidth(instr)
876 877 numspaces = (width - ((strwidth + xstart) % width) - 1)
877 878 return instr + " " * numspaces + "\n"
878 879
879 880 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
880 881 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
881 882 """
882 883 print the string, text, with the specified colors and attributes, to
883 884 the specified curses window object.
884 885
885 886 the foreground and background colors are of the form
886 887 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
887 888 magenta, red, white, yellow]. if pairname is provided, a color
888 889 pair will be looked up in the self.colorpairnames dictionary.
889 890
890 891 attrlist is a list containing text attributes in the form of
891 892 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
892 893 underline].
893 894
894 895 if align == True, whitespace is added to the printed string such that
895 896 the string stretches to the right border of the window.
896 897
897 898 if showwhtspc == True, trailing whitespace of a string is highlighted.
898 899
899 900 """
900 901 # preprocess the text, converting tabs to spaces
901 902 text = text.expandtabs(4)
902 903 # strip \n, and convert control characters to ^[char] representation
903 904 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
904 905 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
905 906
906 907 if pair is not None:
907 908 colorpair = pair
908 909 elif pairname is not None:
909 910 colorpair = self.colorpairnames[pairname]
910 911 else:
911 912 if fgcolor is None:
912 913 fgcolor = -1
913 914 if bgcolor is None:
914 915 bgcolor = -1
915 916 if (fgcolor, bgcolor) in self.colorpairs:
916 917 colorpair = self.colorpairs[(fgcolor, bgcolor)]
917 918 else:
918 919 colorpair = self.getcolorpair(fgcolor, bgcolor)
919 920 # add attributes if possible
920 921 if attrlist is None:
921 922 attrlist = []
922 923 if colorpair < 256:
923 924 # then it is safe to apply all attributes
924 925 for textattr in attrlist:
925 926 colorpair |= textattr
926 927 else:
927 928 # just apply a select few (safe?) attributes
928 929 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
929 930 if textattr in attrlist:
930 931 colorpair |= textattr
931 932
932 933 y, xstart = self.chunkpad.getyx()
933 934 t = "" # variable for counting lines printed
934 935 # if requested, show trailing whitespace
935 936 if showwhtspc:
936 937 origlen = len(text)
937 938 text = text.rstrip(' \n') # tabs have already been expanded
938 939 strippedlen = len(text)
939 940 numtrailingspaces = origlen - strippedlen
940 941
941 942 if towin:
942 943 window.addstr(text, colorpair)
943 944 t += text
944 945
945 946 if showwhtspc:
946 947 wscolorpair = colorpair | curses.A_REVERSE
947 948 if towin:
948 949 for i in range(numtrailingspaces):
949 950 window.addch(curses.ACS_CKBOARD, wscolorpair)
950 951 t += " " * numtrailingspaces
951 952
952 953 if align:
953 954 if towin:
954 955 extrawhitespace = self.alignstring("", window)
955 956 window.addstr(extrawhitespace, colorpair)
956 957 else:
957 958 # need to use t, since the x position hasn't incremented
958 959 extrawhitespace = self.alignstring(t, window)
959 960 t += extrawhitespace
960 961
961 962 # is reset to 0 at the beginning of printitem()
962 963
963 964 linesprinted = (xstart + len(t)) / self.xscreensize
964 965 self.linesprintedtopadsofar += linesprinted
965 966 return t
966 967
967 968 def updatescreen(self):
968 969 self.statuswin.erase()
969 970 self.chunkpad.erase()
970 971
971 972 printstring = self.printstring
972 973
973 974 # print out the status lines at the top
974 975 try:
975 976 printstring(self.statuswin,
976 977 "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
977 978 "(space/A) toggle hunk/all; (e)dit hunk;",
978 979 pairname="legend")
979 980 printstring(self.statuswin,
980 981 " (f)old/unfold; (c)ommit applied; (q)uit; (?) help "
981 982 "| [X]=hunk applied **=folded",
982 983 pairname="legend")
983 984 except curses.error:
984 985 pass
985 986
986 987 # print out the patch in the remaining part of the window
987 988 try:
988 989 self.printitem()
989 990 self.updatescroll()
990 991 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
991 992 self.numstatuslines, 0,
992 993 self.yscreensize + 1 - self.numstatuslines,
993 994 self.xscreensize)
994 995 except curses.error:
995 996 pass
996 997
997 998 # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
998 999 self.statuswin.refresh()
999 1000
1000 1001 def getstatusprefixstring(self, item):
1001 1002 """
1002 1003 create a string to prefix a line with which indicates whether 'item'
1003 1004 is applied and/or folded.
1004 1005
1005 1006 """
1006 1007 # create checkbox string
1007 1008 if item.applied:
1008 1009 if not isinstance(item, uihunkline) and item.partial:
1009 1010 checkbox = "[~]"
1010 1011 else:
1011 1012 checkbox = "[x]"
1012 1013 else:
1013 1014 checkbox = "[ ]"
1014 1015
1015 1016 try:
1016 1017 if item.folded:
1017 1018 checkbox += "**"
1018 1019 if isinstance(item, uiheader):
1019 1020 # one of "m", "a", or "d" (modified, added, deleted)
1020 1021 filestatus = item.changetype
1021 1022
1022 1023 checkbox += filestatus + " "
1023 1024 else:
1024 1025 checkbox += " "
1025 1026 if isinstance(item, uiheader):
1026 1027 # add two more spaces for headers
1027 1028 checkbox += " "
1028 1029 except AttributeError: # not foldable
1029 1030 checkbox += " "
1030 1031
1031 1032 return checkbox
1032 1033
1033 1034 def printheader(self, header, selected=False, towin=True,
1034 1035 ignorefolding=False):
1035 1036 """
1036 1037 print the header to the pad. if countlines is True, don't print
1037 1038 anything, but just count the number of lines which would be printed.
1038 1039
1039 1040 """
1040 1041 outstr = ""
1041 1042 text = header.prettystr()
1042 1043 chunkindex = self.chunklist.index(header)
1043 1044
1044 1045 if chunkindex != 0 and not header.folded:
1045 1046 # add separating line before headers
1046 1047 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1047 1048 towin=towin, align=False)
1048 1049 # select color-pair based on if the header is selected
1049 1050 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1050 1051 attrlist=[curses.A_BOLD])
1051 1052
1052 1053 # print out each line of the chunk, expanding it to screen width
1053 1054
1054 1055 # number of characters to indent lines on this level by
1055 1056 indentnumchars = 0
1056 1057 checkbox = self.getstatusprefixstring(header)
1057 1058 if not header.folded or ignorefolding:
1058 1059 textlist = text.split("\n")
1059 1060 linestr = checkbox + textlist[0]
1060 1061 else:
1061 1062 linestr = checkbox + header.filename()
1062 1063 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1063 1064 towin=towin)
1064 1065 if not header.folded or ignorefolding:
1065 1066 if len(textlist) > 1:
1066 1067 for line in textlist[1:]:
1067 1068 linestr = " "*(indentnumchars + len(checkbox)) + line
1068 1069 outstr += self.printstring(self.chunkpad, linestr,
1069 1070 pair=colorpair, towin=towin)
1070 1071
1071 1072 return outstr
1072 1073
1073 1074 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1074 1075 ignorefolding=False):
1075 1076 "includes start/end line indicator"
1076 1077 outstr = ""
1077 1078 # where hunk is in list of siblings
1078 1079 hunkindex = hunk.header.hunks.index(hunk)
1079 1080
1080 1081 if hunkindex != 0:
1081 1082 # add separating line before headers
1082 1083 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1083 1084 towin=towin, align=False)
1084 1085
1085 1086 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1086 1087 attrlist=[curses.A_BOLD])
1087 1088
1088 1089 # print out from-to line with checkbox
1089 1090 checkbox = self.getstatusprefixstring(hunk)
1090 1091
1091 1092 lineprefix = " "*self.hunkindentnumchars + checkbox
1092 1093 frtoline = " " + hunk.getfromtoline().strip("\n")
1093 1094
1094 1095
1095 1096 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1096 1097 align=False) # add uncolored checkbox/indent
1097 1098 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1098 1099 towin=towin)
1099 1100
1100 1101 if hunk.folded and not ignorefolding:
1101 1102 # skip remainder of output
1102 1103 return outstr
1103 1104
1104 1105 # print out lines of the chunk preceeding changed-lines
1105 1106 for line in hunk.before:
1106 1107 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1107 1108 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1108 1109
1109 1110 return outstr
1110 1111
1111 1112 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1112 1113 outstr = ""
1113 1114 if hunk.folded and not ignorefolding:
1114 1115 return outstr
1115 1116
1116 1117 # a bit superfluous, but to avoid hard-coding indent amount
1117 1118 checkbox = self.getstatusprefixstring(hunk)
1118 1119 for line in hunk.after:
1119 1120 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1120 1121 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1121 1122
1122 1123 return outstr
1123 1124
1124 1125 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1125 1126 outstr = ""
1126 1127 checkbox = self.getstatusprefixstring(hunkline)
1127 1128
1128 1129 linestr = hunkline.prettystr().strip("\n")
1129 1130
1130 1131 # select color-pair based on whether line is an addition/removal
1131 1132 if selected:
1132 1133 colorpair = self.getcolorpair(name="selected")
1133 1134 elif linestr.startswith("+"):
1134 1135 colorpair = self.getcolorpair(name="addition")
1135 1136 elif linestr.startswith("-"):
1136 1137 colorpair = self.getcolorpair(name="deletion")
1137 1138 elif linestr.startswith("\\"):
1138 1139 colorpair = self.getcolorpair(name="normal")
1139 1140
1140 1141 lineprefix = " "*self.hunklineindentnumchars + checkbox
1141 1142 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1142 1143 align=False) # add uncolored checkbox/indent
1143 1144 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1144 1145 towin=towin, showwhtspc=True)
1145 1146 return outstr
1146 1147
1147 1148 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1148 1149 towin=True):
1149 1150 """
1150 1151 use __printitem() to print the the specified item.applied.
1151 1152 if item is not specified, then print the entire patch.
1152 1153 (hiding folded elements, etc. -- see __printitem() docstring)
1153 1154 """
1154 1155 if item is None:
1155 1156 item = self.headerlist
1156 1157 if recursechildren:
1157 1158 self.linesprintedtopadsofar = 0
1158 1159
1159 1160 outstr = []
1160 1161 self.__printitem(item, ignorefolding, recursechildren, outstr,
1161 1162 towin=towin)
1162 1163 return ''.join(outstr)
1163 1164
1164 1165 def outofdisplayedarea(self):
1165 1166 y, _ = self.chunkpad.getyx() # cursor location
1166 1167 # * 2 here works but an optimization would be the max number of
1167 1168 # consecutive non selectable lines
1168 1169 # i.e the max number of context line for any hunk in the patch
1169 1170 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1170 1171 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1171 1172 return y < miny or y > maxy
1172 1173
1173 1174 def handleselection(self, item, recursechildren):
1174 1175 selected = (item is self.currentselecteditem)
1175 1176 if selected and recursechildren:
1176 1177 # assumes line numbering starting from line 0
1177 1178 self.selecteditemstartline = self.linesprintedtopadsofar
1178 1179 selecteditemlines = self.getnumlinesdisplayed(item,
1179 1180 recursechildren=False)
1180 1181 self.selecteditemendline = (self.selecteditemstartline +
1181 1182 selecteditemlines - 1)
1182 1183 return selected
1183 1184
1184 1185 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1185 1186 towin=True):
1186 1187 """
1187 1188 recursive method for printing out patch/header/hunk/hunk-line data to
1188 1189 screen. also returns a string with all of the content of the displayed
1189 1190 patch (not including coloring, etc.).
1190 1191
1191 1192 if ignorefolding is True, then folded items are printed out.
1192 1193
1193 1194 if recursechildren is False, then only print the item without its
1194 1195 child items.
1195 1196
1196 1197 """
1197 1198 if towin and self.outofdisplayedarea():
1198 1199 return
1199 1200
1200 1201 selected = self.handleselection(item, recursechildren)
1201 1202
1202 1203 # patch object is a list of headers
1203 1204 if isinstance(item, patch):
1204 1205 if recursechildren:
1205 1206 for hdr in item:
1206 1207 self.__printitem(hdr, ignorefolding,
1207 1208 recursechildren, outstr, towin)
1208 1209 # todo: eliminate all isinstance() calls
1209 1210 if isinstance(item, uiheader):
1210 1211 outstr.append(self.printheader(item, selected, towin=towin,
1211 1212 ignorefolding=ignorefolding))
1212 1213 if recursechildren:
1213 1214 for hnk in item.hunks:
1214 1215 self.__printitem(hnk, ignorefolding,
1215 1216 recursechildren, outstr, towin)
1216 1217 elif (isinstance(item, uihunk) and
1217 1218 ((not item.header.folded) or ignorefolding)):
1218 1219 # print the hunk data which comes before the changed-lines
1219 1220 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1220 1221 ignorefolding=ignorefolding))
1221 1222 if recursechildren:
1222 1223 for l in item.changedlines:
1223 1224 self.__printitem(l, ignorefolding,
1224 1225 recursechildren, outstr, towin)
1225 1226 outstr.append(self.printhunklinesafter(item, towin=towin,
1226 1227 ignorefolding=ignorefolding))
1227 1228 elif (isinstance(item, uihunkline) and
1228 1229 ((not item.hunk.folded) or ignorefolding)):
1229 1230 outstr.append(self.printhunkchangedline(item, selected,
1230 1231 towin=towin))
1231 1232
1232 1233 return outstr
1233 1234
1234 1235 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1235 1236 recursechildren=True):
1236 1237 """
1237 1238 return the number of lines which would be displayed if the item were
1238 1239 to be printed to the display. the item will not be printed to the
1239 1240 display (pad).
1240 1241 if no item is given, assume the entire patch.
1241 1242 if ignorefolding is True, folded items will be unfolded when counting
1242 1243 the number of lines.
1243 1244
1244 1245 """
1245 1246 # temporarily disable printing to windows by printstring
1246 1247 patchdisplaystring = self.printitem(item, ignorefolding,
1247 1248 recursechildren, towin=False)
1248 1249 numlines = len(patchdisplaystring) / self.xscreensize
1249 1250 return numlines
1250 1251
1251 1252 def sigwinchhandler(self, n, frame):
1252 1253 "handle window resizing"
1253 1254 try:
1254 1255 curses.endwin()
1255 1256 self.yscreensize, self.xscreensize = gethw()
1256 1257 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1257 1258 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1258 1259 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1259 1260 # todo: try to resize commit message window if possible
1260 1261 except curses.error:
1261 1262 pass
1262 1263
1263 1264 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1264 1265 attrlist=None):
1265 1266 """
1266 1267 get a curses color pair, adding it to self.colorpairs if it is not
1267 1268 already defined. an optional string, name, can be passed as a shortcut
1268 1269 for referring to the color-pair. by default, if no arguments are
1269 1270 specified, the white foreground / black background color-pair is
1270 1271 returned.
1271 1272
1272 1273 it is expected that this function will be used exclusively for
1273 1274 initializing color pairs, and not curses.init_pair().
1274 1275
1275 1276 attrlist is used to 'flavor' the returned color-pair. this information
1276 1277 is not stored in self.colorpairs. it contains attribute values like
1277 1278 curses.A_BOLD.
1278 1279
1279 1280 """
1280 1281 if (name is not None) and name in self.colorpairnames:
1281 1282 # then get the associated color pair and return it
1282 1283 colorpair = self.colorpairnames[name]
1283 1284 else:
1284 1285 if fgcolor is None:
1285 1286 fgcolor = -1
1286 1287 if bgcolor is None:
1287 1288 bgcolor = -1
1288 1289 if (fgcolor, bgcolor) in self.colorpairs:
1289 1290 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1290 1291 else:
1291 1292 pairindex = len(self.colorpairs) + 1
1292 1293 curses.init_pair(pairindex, fgcolor, bgcolor)
1293 1294 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1294 1295 curses.color_pair(pairindex))
1295 1296 if name is not None:
1296 1297 self.colorpairnames[name] = curses.color_pair(pairindex)
1297 1298
1298 1299 # add attributes if possible
1299 1300 if attrlist is None:
1300 1301 attrlist = []
1301 1302 if colorpair < 256:
1302 1303 # then it is safe to apply all attributes
1303 1304 for textattr in attrlist:
1304 1305 colorpair |= textattr
1305 1306 else:
1306 1307 # just apply a select few (safe?) attributes
1307 1308 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1308 1309 if textattrib in attrlist:
1309 1310 colorpair |= textattrib
1310 1311 return colorpair
1311 1312
1312 1313 def initcolorpair(self, *args, **kwargs):
1313 1314 "same as getcolorpair."
1314 1315 self.getcolorpair(*args, **kwargs)
1315 1316
1316 1317 def helpwindow(self):
1317 1318 "print a help window to the screen. exit after any keypress."
1318 1319 helptext = """ [press any key to return to the patch-display]
1319 1320
1320 1321 crecord allows you to interactively choose among the changes you have made,
1321 1322 and commit only those changes you select. after committing the selected
1322 1323 changes, the unselected changes are still present in your working copy, so you
1323 1324 can use crecord multiple times to split large changes into smaller changesets.
1324 1325 the following are valid keystrokes:
1325 1326
1326 1327 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1327 1328 a : (un-)select all items
1328 1329 up/down-arrow [k/j] : go to previous/next unfolded item
1329 1330 pgup/pgdn [k/j] : go to previous/next item of same type
1330 1331 right/left-arrow [l/h] : go to child item / parent item
1331 1332 shift-left-arrow [h] : go to parent header / fold selected header
1332 1333 f : fold / unfold item, hiding/revealing its children
1333 1334 f : fold / unfold parent item and all of its ancestors
1334 1335 m : edit / resume editing the commit message
1335 1336 e : edit the currently selected hunk
1336 1337 a : toggle amend mode (hg rev >= 2.2)
1337 1338 c : commit selected changes
1338 1339 r : review/edit and commit selected changes
1339 1340 q : quit without committing (no changes will be made)
1340 1341 ? : help (what you're currently reading)"""
1341 1342
1342 1343 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1343 1344 helplines = helptext.split("\n")
1344 1345 helplines = helplines + [" "]*(
1345 1346 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1346 1347 try:
1347 1348 for line in helplines:
1348 1349 self.printstring(helpwin, line, pairname="legend")
1349 1350 except curses.error:
1350 1351 pass
1351 1352 helpwin.refresh()
1352 1353 try:
1353 1354 helpwin.getkey()
1354 1355 except curses.error:
1355 1356 pass
1356 1357
1357 1358 def confirmationwindow(self, windowtext):
1358 1359 "display an informational window, then wait for and return a keypress."
1359 1360
1360 1361 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1361 1362 try:
1362 1363 lines = windowtext.split("\n")
1363 1364 for line in lines:
1364 1365 self.printstring(confirmwin, line, pairname="selected")
1365 1366 except curses.error:
1366 1367 pass
1367 1368 self.stdscr.refresh()
1368 1369 confirmwin.refresh()
1369 1370 try:
1370 1371 response = chr(self.stdscr.getch())
1371 1372 except ValueError:
1372 1373 response = None
1373 1374
1374 1375 return response
1375 1376
1376 1377 def confirmcommit(self, review=False):
1377 1378 "ask for 'y' to be pressed to confirm commit. return True if confirmed."
1378 1379 if review:
1379 1380 confirmtext = (
1380 1381 """if you answer yes to the following, the your currently chosen patch chunks
1381 1382 will be loaded into an editor. you may modify the patch from the editor, and
1382 1383 save the changes if you wish to change the patch. otherwise, you can just
1383 1384 close the editor without saving to accept the current patch as-is.
1384 1385
1385 1386 note: don't add/remove lines unless you also modify the range information.
1386 1387 failing to follow this rule will result in the commit aborting.
1387 1388
1388 1389 are you sure you want to review/edit and commit the selected changes [yn]? """)
1389 1390 else:
1390 1391 confirmtext = (
1391 1392 "are you sure you want to commit the selected changes [yn]? ")
1392 1393
1393 1394 response = self.confirmationwindow(confirmtext)
1394 1395 if response is None:
1395 1396 response = "n"
1396 1397 if response.lower().startswith("y"):
1397 1398 return True
1398 1399 else:
1399 1400 return False
1400 1401
1401 1402 def recenterdisplayedarea(self):
1402 1403 """
1403 1404 once we scrolled with pg up pg down we can be pointing outside of the
1404 1405 display zone. we print the patch with towin=False to compute the
1405 1406 location of the selected item eventhough it is outside of the displayed
1406 1407 zone and then update the scroll.
1407 1408 """
1408 1409 self.printitem(towin=False)
1409 1410 self.updatescroll()
1410 1411
1411 1412 def toggleedit(self, item=None, test=False):
1412 1413 """
1413 1414 edit the currently chelected chunk
1414 1415 """
1415 1416
1416 1417 def editpatchwitheditor(self, chunk):
1417 1418 if chunk is None:
1418 1419 self.ui.write(_('cannot edit patch for whole file'))
1419 1420 self.ui.write("\n")
1420 1421 return None
1421 1422 if chunk.header.binary():
1422 1423 self.ui.write(_('cannot edit patch for binary file'))
1423 1424 self.ui.write("\n")
1424 1425 return None
1425 1426 # patch comment based on the git one (based on comment at end of
1426 1427 # http://mercurial.selenic.com/wiki/recordextension)
1427 1428 phelp = '---' + _("""
1428 1429 to remove '-' lines, make them ' ' lines (context).
1429 1430 to remove '+' lines, delete them.
1430 1431 lines starting with # will be removed from the patch.
1431 1432
1432 1433 if the patch applies cleanly, the edited hunk will immediately be
1433 1434 added to the record list. if it does not apply cleanly, a rejects
1434 1435 file will be generated: you can use that when you try again. if
1435 1436 all lines of the hunk are removed, then the edit is aborted and
1436 1437 the hunk is left unchanged.
1437 1438 """)
1438 1439 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1439 1440 suffix=".diff", text=True)
1440 1441 ncpatchfp = None
1441 1442 try:
1442 1443 # write the initial patch
1443 1444 f = os.fdopen(patchfd, "w")
1444 1445 chunk.header.write(f)
1445 1446 chunk.write(f)
1446 1447 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1447 1448 f.close()
1448 1449 # start the editor and wait for it to complete
1449 1450 editor = self.ui.geteditor()
1450 1451 self.ui.system("%s \"%s\"" % (editor, patchfn),
1451 1452 environ={'hguser': self.ui.username()},
1452 1453 onerr=util.Abort, errprefix=_("edit failed"))
1453 1454 # remove comment lines
1454 1455 patchfp = open(patchfn)
1455 1456 ncpatchfp = cStringIO.StringIO()
1456 1457 for line in patchfp:
1457 1458 if not line.startswith('#'):
1458 1459 ncpatchfp.write(line)
1459 1460 patchfp.close()
1460 1461 ncpatchfp.seek(0)
1461 1462 newpatches = patchmod.parsepatch(ncpatchfp)
1462 1463 finally:
1463 1464 os.unlink(patchfn)
1464 1465 del ncpatchfp
1465 1466 return newpatches
1466 1467 if item is None:
1467 1468 item = self.currentselecteditem
1468 1469 if isinstance(item, uiheader):
1469 1470 return
1470 1471 if isinstance(item, uihunkline):
1471 1472 item = item.parentitem()
1472 1473 if not isinstance(item, uihunk):
1473 1474 return
1474 1475
1475 1476 beforeadded, beforeremoved = item.added, item.removed
1476 1477 newpatches = editpatchwitheditor(self, item)
1477 1478 header = item.header
1478 1479 editedhunkindex = header.hunks.index(item)
1479 1480 hunksbefore = header.hunks[:editedhunkindex]
1480 1481 hunksafter = header.hunks[editedhunkindex + 1:]
1481 1482 newpatchheader = newpatches[0]
1482 1483 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1483 1484 newadded = sum([h.added for h in newhunks])
1484 1485 newremoved = sum([h.removed for h in newhunks])
1485 1486 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1486 1487
1487 1488 for h in hunksafter:
1488 1489 h.toline += offset
1489 1490 for h in newhunks:
1490 1491 h.folded = False
1491 1492 header.hunks = hunksbefore + newhunks + hunksafter
1492 1493 if self.emptypatch():
1493 1494 header.hunks = hunksbefore + [item] + hunksafter
1494 1495 self.currentselecteditem = header
1495 1496
1496 1497 if not test:
1497 1498 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1498 1499 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1499 1500 self.updatescroll()
1500 1501 self.stdscr.refresh()
1501 1502 self.statuswin.refresh()
1502 1503 self.stdscr.keypad(1)
1503 1504
1504 1505 def emptypatch(self):
1505 1506 item = self.headerlist
1506 1507 if not item:
1507 1508 return True
1508 1509 for header in item:
1509 1510 if header.hunks:
1510 1511 return False
1511 1512 return True
1512 1513
1513 1514 def handlekeypressed(self, keypressed, test=False):
1514 1515 if keypressed in ["k", "KEY_UP"]:
1515 1516 self.uparrowevent()
1516 1517 if keypressed in ["k", "KEY_PPAGE"]:
1517 1518 self.uparrowshiftevent()
1518 1519 elif keypressed in ["j", "KEY_DOWN"]:
1519 1520 self.downarrowevent()
1520 1521 elif keypressed in ["j", "KEY_NPAGE"]:
1521 1522 self.downarrowshiftevent()
1522 1523 elif keypressed in ["l", "KEY_RIGHT"]:
1523 1524 self.rightarrowevent()
1524 1525 elif keypressed in ["h", "KEY_LEFT"]:
1525 1526 self.leftarrowevent()
1526 1527 elif keypressed in ["h", "KEY_SLEFT"]:
1527 1528 self.leftarrowshiftevent()
1528 1529 elif keypressed in ["q"]:
1529 1530 raise util.Abort(_('user quit'))
1530 1531 elif keypressed in ["c"]:
1531 1532 if self.confirmcommit():
1532 1533 return True
1533 1534 elif keypressed in ["r"]:
1534 1535 if self.confirmcommit(review=True):
1535 1536 return True
1536 1537 elif test and keypressed in ['X']:
1537 1538 return True
1538 1539 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1539 1540 self.toggleapply()
1540 1541 elif keypressed in ['A']:
1541 1542 self.toggleall()
1542 1543 elif keypressed in ['e']:
1543 1544 self.toggleedit(test=test)
1544 1545 elif keypressed in ["f"]:
1545 1546 self.togglefolded()
1546 1547 elif keypressed in ["f"]:
1547 1548 self.togglefolded(foldparent=True)
1548 1549 elif keypressed in ["?"]:
1549 1550 self.helpwindow()
1550 1551
1551 1552 def main(self, stdscr):
1552 1553 """
1553 1554 method to be wrapped by curses.wrapper() for selecting chunks.
1554 1555
1555 1556 """
1556 1557 signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1557 1558 self.stdscr = stdscr
1558 1559 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1559 1560
1560 1561 curses.start_color()
1561 1562 curses.use_default_colors()
1562 1563
1563 1564 # available colors: black, blue, cyan, green, magenta, white, yellow
1564 1565 # init_pair(color_id, foreground_color, background_color)
1565 1566 self.initcolorpair(None, None, name="normal")
1566 1567 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1567 1568 name="selected")
1568 1569 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1569 1570 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1570 1571 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1571 1572 # newwin([height, width,] begin_y, begin_x)
1572 1573 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1573 1574 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1574 1575
1575 1576 # figure out how much space to allocate for the chunk-pad which is
1576 1577 # used for displaying the patch
1577 1578
1578 1579 # stupid hack to prevent getnumlinesdisplayed from failing
1579 1580 self.chunkpad = curses.newpad(1, self.xscreensize)
1580 1581
1581 1582 # add 1 so to account for last line text reaching end of line
1582 1583 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1583 1584 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1584 1585
1585 1586 # initialize selecteitemendline (initial start-line is 0)
1586 1587 self.selecteditemendline = self.getnumlinesdisplayed(
1587 1588 self.currentselecteditem, recursechildren=False)
1588 1589
1589 1590 while True:
1590 1591 self.updatescreen()
1591 1592 try:
1592 1593 keypressed = self.statuswin.getkey()
1593 1594 except curses.error:
1594 1595 keypressed = "foobar"
1595 1596 if self.handlekeypressed(keypressed):
1596 1597 break
General Comments 0
You need to be logged in to leave comments. Login now