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