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