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