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