##// END OF EJS Templates
progress: move most extension code into a 'mercurial.progress' module...
Pierre-Yves David -
r25497:93b8b004 default
parent child Browse files
Show More
@@ -0,0 +1,252 b''
1 # progress.py progress bars related code
2 #
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 #
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.
7
8 import sys
9 import time
10 import threading
11 from mercurial import encoding
12
13 from mercurial.i18n import _
14
15
16 def spacejoin(*args):
17 return ' '.join(s for s in args if s)
18
19 def shouldprint(ui):
20 return not ui.plain() and (ui._isatty(sys.stderr) or
21 ui.configbool('progress', 'assume-tty'))
22
23 def fmtremaining(seconds):
24 """format a number of remaining seconds in humain readable way
25
26 This will properly display seconds, minutes, hours, days if needed"""
27 if seconds < 60:
28 # i18n: format XX seconds as "XXs"
29 return _("%02ds") % (seconds)
30 minutes = seconds // 60
31 if minutes < 60:
32 seconds -= minutes * 60
33 # i18n: format X minutes and YY seconds as "XmYYs"
34 return _("%dm%02ds") % (minutes, seconds)
35 # we're going to ignore seconds in this case
36 minutes += 1
37 hours = minutes // 60
38 minutes -= hours * 60
39 if hours < 30:
40 # i18n: format X hours and YY minutes as "XhYYm"
41 return _("%dh%02dm") % (hours, minutes)
42 # we're going to ignore minutes in this case
43 hours += 1
44 days = hours // 24
45 hours -= days * 24
46 if days < 15:
47 # i18n: format X days and YY hours as "XdYYh"
48 return _("%dd%02dh") % (days, hours)
49 # we're going to ignore hours in this case
50 days += 1
51 weeks = days // 7
52 days -= weeks * 7
53 if weeks < 55:
54 # i18n: format X weeks and YY days as "XwYYd"
55 return _("%dw%02dd") % (weeks, days)
56 # we're going to ignore days and treat a year as 52 weeks
57 weeks += 1
58 years = weeks // 52
59 weeks -= years * 52
60 # i18n: format X years and YY weeks as "XyYYw"
61 return _("%dy%02dw") % (years, weeks)
62
63 class progbar(object):
64 def __init__(self, ui):
65 self.ui = ui
66 self._refreshlock = threading.Lock()
67 self.resetstate()
68
69 def resetstate(self):
70 self.topics = []
71 self.topicstates = {}
72 self.starttimes = {}
73 self.startvals = {}
74 self.printed = False
75 self.lastprint = time.time() + float(self.ui.config(
76 'progress', 'delay', default=3))
77 self.curtopic = None
78 self.lasttopic = None
79 self.indetcount = 0
80 self.refresh = float(self.ui.config(
81 'progress', 'refresh', default=0.1))
82 self.changedelay = max(3 * self.refresh,
83 float(self.ui.config(
84 'progress', 'changedelay', default=1)))
85 self.order = self.ui.configlist(
86 'progress', 'format',
87 default=['topic', 'bar', 'number', 'estimate'])
88
89 def show(self, now, topic, pos, item, unit, total):
90 if not shouldprint(self.ui):
91 return
92 termwidth = self.width()
93 self.printed = True
94 head = ''
95 needprogress = False
96 tail = ''
97 for indicator in self.order:
98 add = ''
99 if indicator == 'topic':
100 add = topic
101 elif indicator == 'number':
102 if total:
103 add = ('% ' + str(len(str(total))) +
104 's/%s') % (pos, total)
105 else:
106 add = str(pos)
107 elif indicator.startswith('item') and item:
108 slice = 'end'
109 if '-' in indicator:
110 wid = int(indicator.split('-')[1])
111 elif '+' in indicator:
112 slice = 'beginning'
113 wid = int(indicator.split('+')[1])
114 else:
115 wid = 20
116 if slice == 'end':
117 add = encoding.trim(item, wid, leftside=True)
118 else:
119 add = encoding.trim(item, wid)
120 add += (wid - encoding.colwidth(add)) * ' '
121 elif indicator == 'bar':
122 add = ''
123 needprogress = True
124 elif indicator == 'unit' and unit:
125 add = unit
126 elif indicator == 'estimate':
127 add = self.estimate(topic, pos, total, now)
128 elif indicator == 'speed':
129 add = self.speed(topic, pos, unit, now)
130 if not needprogress:
131 head = spacejoin(head, add)
132 else:
133 tail = spacejoin(tail, add)
134 if needprogress:
135 used = 0
136 if head:
137 used += encoding.colwidth(head) + 1
138 if tail:
139 used += encoding.colwidth(tail) + 1
140 progwidth = termwidth - used - 3
141 if total and pos <= total:
142 amt = pos * progwidth // total
143 bar = '=' * (amt - 1)
144 if amt > 0:
145 bar += '>'
146 bar += ' ' * (progwidth - amt)
147 else:
148 progwidth -= 3
149 self.indetcount += 1
150 # mod the count by twice the width so we can make the
151 # cursor bounce between the right and left sides
152 amt = self.indetcount % (2 * progwidth)
153 amt -= progwidth
154 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
155 ' ' * int(abs(amt)))
156 prog = ''.join(('[', bar , ']'))
157 out = spacejoin(head, prog, tail)
158 else:
159 out = spacejoin(head, tail)
160 sys.stderr.write('\r' + encoding.trim(out, termwidth))
161 self.lasttopic = topic
162 sys.stderr.flush()
163
164 def clear(self):
165 if not shouldprint(self.ui):
166 return
167 sys.stderr.write('\r%s\r' % (' ' * self.width()))
168
169 def complete(self):
170 if not shouldprint(self.ui):
171 return
172 if self.ui.configbool('progress', 'clear-complete', default=True):
173 self.clear()
174 else:
175 sys.stderr.write('\n')
176 sys.stderr.flush()
177
178 def width(self):
179 tw = self.ui.termwidth()
180 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
181
182 def estimate(self, topic, pos, total, now):
183 if total is None:
184 return ''
185 initialpos = self.startvals[topic]
186 target = total - initialpos
187 delta = pos - initialpos
188 if delta > 0:
189 elapsed = now - self.starttimes[topic]
190 if elapsed > float(
191 self.ui.config('progress', 'estimate', default=2)):
192 seconds = (elapsed * (target - delta)) // delta + 1
193 return fmtremaining(seconds)
194 return ''
195
196 def speed(self, topic, pos, unit, now):
197 initialpos = self.startvals[topic]
198 delta = pos - initialpos
199 elapsed = now - self.starttimes[topic]
200 if elapsed > float(
201 self.ui.config('progress', 'estimate', default=2)):
202 return _('%d %s/sec') % (delta / elapsed, unit)
203 return ''
204
205 def _oktoprint(self, now):
206 '''Check if conditions are met to print - e.g. changedelay elapsed'''
207 if (self.lasttopic is None # first time we printed
208 # not a topic change
209 or self.curtopic == self.lasttopic
210 # it's been long enough we should print anyway
211 or now - self.lastprint >= self.changedelay):
212 return True
213 else:
214 return False
215
216 def progress(self, topic, pos, item='', unit='', total=None):
217 now = time.time()
218 self._refreshlock.acquire()
219 try:
220 if pos is None:
221 self.starttimes.pop(topic, None)
222 self.startvals.pop(topic, None)
223 self.topicstates.pop(topic, None)
224 # reset the progress bar if this is the outermost topic
225 if self.topics and self.topics[0] == topic and self.printed:
226 self.complete()
227 self.resetstate()
228 # truncate the list of topics assuming all topics within
229 # this one are also closed
230 if topic in self.topics:
231 self.topics = self.topics[:self.topics.index(topic)]
232 # reset the last topic to the one we just unwound to,
233 # so that higher-level topics will be stickier than
234 # lower-level topics
235 if self.topics:
236 self.lasttopic = self.topics[-1]
237 else:
238 self.lasttopic = None
239 else:
240 if topic not in self.topics:
241 self.starttimes[topic] = now
242 self.startvals[topic] = pos
243 self.topics.append(topic)
244 self.topicstates[topic] = pos, item, unit, total
245 self.curtopic = topic
246 if now - self.lastprint >= self.refresh and self.topics:
247 if self._oktoprint(now):
248 self.lastprint = now
249 self.show(now, topic, *self.topicstates[topic])
250 finally:
251 self._refreshlock.release()
252
@@ -1,326 +1,81 b''
1 # progress.py show progress bars for some actions
1 # progress.py show progress bars for some actions
2 #
2 #
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
3 # Copyright (C) 2010 Augie Fackler <durin42@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 """show progress bars for some actions
8 """show progress bars for some actions
9
9
10 This extension uses the progress information logged by hg commands
10 This extension uses the progress information logged by hg commands
11 to draw progress bars that are as informative as possible. Some progress
11 to draw progress bars that are as informative as possible. Some progress
12 bars only offer indeterminate information, while others have a definite
12 bars only offer indeterminate information, while others have a definite
13 end point.
13 end point.
14
14
15 The following settings are available::
15 The following settings are available::
16
16
17 [progress]
17 [progress]
18 delay = 3 # number of seconds (float) before showing the progress bar
18 delay = 3 # number of seconds (float) before showing the progress bar
19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
20 # If set to less than 3 * refresh, that value will
20 # If set to less than 3 * refresh, that value will
21 # be used instead.
21 # be used instead.
22 refresh = 0.1 # time in seconds between refreshes of the progress bar
22 refresh = 0.1 # time in seconds between refreshes of the progress bar
23 format = topic bar number estimate # format of the progress bar
23 format = topic bar number estimate # format of the progress bar
24 width = <none> # if set, the maximum width of the progress information
24 width = <none> # if set, the maximum width of the progress information
25 # (that is, min(width, term width) will be used)
25 # (that is, min(width, term width) will be used)
26 clear-complete = True # clear the progress bar after it's done
26 clear-complete = True # clear the progress bar after it's done
27 disable = False # if true, don't show a progress bar
27 disable = False # if true, don't show a progress bar
28 assume-tty = False # if true, ALWAYS show a progress bar, unless
28 assume-tty = False # if true, ALWAYS show a progress bar, unless
29 # disable is given
29 # disable is given
30
30
31 Valid entries for the format field are topic, bar, number, unit,
31 Valid entries for the format field are topic, bar, number, unit,
32 estimate, speed, and item. item defaults to the last 20 characters of
32 estimate, speed, and item. item defaults to the last 20 characters of
33 the item, but this can be changed by adding either ``-<num>`` which
33 the item, but this can be changed by adding either ``-<num>`` which
34 would take the last num characters, or ``+<num>`` for the first num
34 would take the last num characters, or ``+<num>`` for the first num
35 characters.
35 characters.
36 """
36 """
37
37
38 import sys
38 from mercurial import progress
39 import time
40 import threading
41
42 from mercurial.i18n import _
43 # Note for extension authors: ONLY specify testedwith = 'internal' for
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 # be specifying the version(s) of Mercurial they are tested with, or
46 # leave the attribute unspecified.
47 testedwith = 'internal'
48
49 from mercurial import encoding
50
51 def spacejoin(*args):
52 return ' '.join(s for s in args if s)
53
54 def shouldprint(ui):
55 return not ui.plain() and (ui._isatty(sys.stderr) or
56 ui.configbool('progress', 'assume-tty'))
57
58 def fmtremaining(seconds):
59 if seconds < 60:
60 # i18n: format XX seconds as "XXs"
61 return _("%02ds") % (seconds)
62 minutes = seconds // 60
63 if minutes < 60:
64 seconds -= minutes * 60
65 # i18n: format X minutes and YY seconds as "XmYYs"
66 return _("%dm%02ds") % (minutes, seconds)
67 # we're going to ignore seconds in this case
68 minutes += 1
69 hours = minutes // 60
70 minutes -= hours * 60
71 if hours < 30:
72 # i18n: format X hours and YY minutes as "XhYYm"
73 return _("%dh%02dm") % (hours, minutes)
74 # we're going to ignore minutes in this case
75 hours += 1
76 days = hours // 24
77 hours -= days * 24
78 if days < 15:
79 # i18n: format X days and YY hours as "XdYYh"
80 return _("%dd%02dh") % (days, hours)
81 # we're going to ignore hours in this case
82 days += 1
83 weeks = days // 7
84 days -= weeks * 7
85 if weeks < 55:
86 # i18n: format X weeks and YY days as "XwYYd"
87 return _("%dw%02dd") % (weeks, days)
88 # we're going to ignore days and treat a year as 52 weeks
89 weeks += 1
90 years = weeks // 52
91 weeks -= years * 52
92 # i18n: format X years and YY weeks as "XyYYw"
93 return _("%dy%02dw") % (years, weeks)
94
95 class progbar(object):
96 def __init__(self, ui):
97 self.ui = ui
98 self._refreshlock = threading.Lock()
99 self.resetstate()
100
101 def resetstate(self):
102 self.topics = []
103 self.topicstates = {}
104 self.starttimes = {}
105 self.startvals = {}
106 self.printed = False
107 self.lastprint = time.time() + float(self.ui.config(
108 'progress', 'delay', default=3))
109 self.curtopic = None
110 self.lasttopic = None
111 self.indetcount = 0
112 self.refresh = float(self.ui.config(
113 'progress', 'refresh', default=0.1))
114 self.changedelay = max(3 * self.refresh,
115 float(self.ui.config(
116 'progress', 'changedelay', default=1)))
117 self.order = self.ui.configlist(
118 'progress', 'format',
119 default=['topic', 'bar', 'number', 'estimate'])
120
121 def show(self, now, topic, pos, item, unit, total):
122 if not shouldprint(self.ui):
123 return
124 termwidth = self.width()
125 self.printed = True
126 head = ''
127 needprogress = False
128 tail = ''
129 for indicator in self.order:
130 add = ''
131 if indicator == 'topic':
132 add = topic
133 elif indicator == 'number':
134 if total:
135 add = ('% ' + str(len(str(total))) +
136 's/%s') % (pos, total)
137 else:
138 add = str(pos)
139 elif indicator.startswith('item') and item:
140 slice = 'end'
141 if '-' in indicator:
142 wid = int(indicator.split('-')[1])
143 elif '+' in indicator:
144 slice = 'beginning'
145 wid = int(indicator.split('+')[1])
146 else:
147 wid = 20
148 if slice == 'end':
149 add = encoding.trim(item, wid, leftside=True)
150 else:
151 add = encoding.trim(item, wid)
152 add += (wid - encoding.colwidth(add)) * ' '
153 elif indicator == 'bar':
154 add = ''
155 needprogress = True
156 elif indicator == 'unit' and unit:
157 add = unit
158 elif indicator == 'estimate':
159 add = self.estimate(topic, pos, total, now)
160 elif indicator == 'speed':
161 add = self.speed(topic, pos, unit, now)
162 if not needprogress:
163 head = spacejoin(head, add)
164 else:
165 tail = spacejoin(tail, add)
166 if needprogress:
167 used = 0
168 if head:
169 used += encoding.colwidth(head) + 1
170 if tail:
171 used += encoding.colwidth(tail) + 1
172 progwidth = termwidth - used - 3
173 if total and pos <= total:
174 amt = pos * progwidth // total
175 bar = '=' * (amt - 1)
176 if amt > 0:
177 bar += '>'
178 bar += ' ' * (progwidth - amt)
179 else:
180 progwidth -= 3
181 self.indetcount += 1
182 # mod the count by twice the width so we can make the
183 # cursor bounce between the right and left sides
184 amt = self.indetcount % (2 * progwidth)
185 amt -= progwidth
186 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
187 ' ' * int(abs(amt)))
188 prog = ''.join(('[', bar , ']'))
189 out = spacejoin(head, prog, tail)
190 else:
191 out = spacejoin(head, tail)
192 sys.stderr.write('\r' + encoding.trim(out, termwidth))
193 self.lasttopic = topic
194 sys.stderr.flush()
195
196 def clear(self):
197 if not shouldprint(self.ui):
198 return
199 sys.stderr.write('\r%s\r' % (' ' * self.width()))
200
201 def complete(self):
202 if not shouldprint(self.ui):
203 return
204 if self.ui.configbool('progress', 'clear-complete', default=True):
205 self.clear()
206 else:
207 sys.stderr.write('\n')
208 sys.stderr.flush()
209
210 def width(self):
211 tw = self.ui.termwidth()
212 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
213
214 def estimate(self, topic, pos, total, now):
215 if total is None:
216 return ''
217 initialpos = self.startvals[topic]
218 target = total - initialpos
219 delta = pos - initialpos
220 if delta > 0:
221 elapsed = now - self.starttimes[topic]
222 if elapsed > float(
223 self.ui.config('progress', 'estimate', default=2)):
224 seconds = (elapsed * (target - delta)) // delta + 1
225 return fmtremaining(seconds)
226 return ''
227
228 def speed(self, topic, pos, unit, now):
229 initialpos = self.startvals[topic]
230 delta = pos - initialpos
231 elapsed = now - self.starttimes[topic]
232 if elapsed > float(
233 self.ui.config('progress', 'estimate', default=2)):
234 return _('%d %s/sec') % (delta / elapsed, unit)
235 return ''
236
237 def _oktoprint(self, now):
238 '''Check if conditions are met to print - e.g. changedelay elapsed'''
239 if (self.lasttopic is None # first time we printed
240 # not a topic change
241 or self.curtopic == self.lasttopic
242 # it's been long enough we should print anyway
243 or now - self.lastprint >= self.changedelay):
244 return True
245 else:
246 return False
247
248 def progress(self, topic, pos, item='', unit='', total=None):
249 now = time.time()
250 self._refreshlock.acquire()
251 try:
252 if pos is None:
253 self.starttimes.pop(topic, None)
254 self.startvals.pop(topic, None)
255 self.topicstates.pop(topic, None)
256 # reset the progress bar if this is the outermost topic
257 if self.topics and self.topics[0] == topic and self.printed:
258 self.complete()
259 self.resetstate()
260 # truncate the list of topics assuming all topics within
261 # this one are also closed
262 if topic in self.topics:
263 self.topics = self.topics[:self.topics.index(topic)]
264 # reset the last topic to the one we just unwound to,
265 # so that higher-level topics will be stickier than
266 # lower-level topics
267 if self.topics:
268 self.lasttopic = self.topics[-1]
269 else:
270 self.lasttopic = None
271 else:
272 if topic not in self.topics:
273 self.starttimes[topic] = now
274 self.startvals[topic] = pos
275 self.topics.append(topic)
276 self.topicstates[topic] = pos, item, unit, total
277 self.curtopic = topic
278 if now - self.lastprint >= self.refresh and self.topics:
279 if self._oktoprint(now):
280 self.lastprint = now
281 self.show(now, topic, *self.topicstates[topic])
282 finally:
283 self._refreshlock.release()
284
39
285 _singleton = None
40 _singleton = None
286
41
287 def uisetup(ui):
42 def uisetup(ui):
288 global _singleton
43 global _singleton
289 class progressui(ui.__class__):
44 class progressui(ui.__class__):
290 _progbar = None
45 _progbar = None
291
46
292 def _quiet(self):
47 def _quiet(self):
293 return self.debugflag or self.quiet
48 return self.debugflag or self.quiet
294
49
295 def progress(self, *args, **opts):
50 def progress(self, *args, **opts):
296 if not self._quiet():
51 if not self._quiet():
297 self._progbar.progress(*args, **opts)
52 self._progbar.progress(*args, **opts)
298 return super(progressui, self).progress(*args, **opts)
53 return super(progressui, self).progress(*args, **opts)
299
54
300 def write(self, *args, **opts):
55 def write(self, *args, **opts):
301 if not self._quiet() and self._progbar.printed:
56 if not self._quiet() and self._progbar.printed:
302 self._progbar.clear()
57 self._progbar.clear()
303 return super(progressui, self).write(*args, **opts)
58 return super(progressui, self).write(*args, **opts)
304
59
305 def write_err(self, *args, **opts):
60 def write_err(self, *args, **opts):
306 if not self._quiet() and self._progbar.printed:
61 if not self._quiet() and self._progbar.printed:
307 self._progbar.clear()
62 self._progbar.clear()
308 return super(progressui, self).write_err(*args, **opts)
63 return super(progressui, self).write_err(*args, **opts)
309
64
310 # Apps that derive a class from ui.ui() can use
65 # Apps that derive a class from ui.ui() can use
311 # setconfig('progress', 'disable', 'True') to disable this extension
66 # setconfig('progress', 'disable', 'True') to disable this extension
312 if ui.configbool('progress', 'disable'):
67 if ui.configbool('progress', 'disable'):
313 return
68 return
314 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
69 if progress.shouldprint(ui) and not ui.debugflag and not ui.quiet:
315 dval = object()
70 dval = object()
316 if getattr(ui, '_progbar', dval) is dval:
71 if getattr(ui, '_progbar', dval) is dval:
317 ui.__class__ = progressui
72 ui.__class__ = progressui
318 # we instantiate one globally-shared progress bar to avoid
73 # we instantiate one globally-shared progress bar to avoid
319 # competing progress bars when multiple UI objects get created
74 # competing progress bars when multiple UI objects get created
320 if not progressui._progbar:
75 if not progressui._progbar:
321 if _singleton is None:
76 if _singleton is None:
322 _singleton = progbar(ui)
77 _singleton = progress.progbar(ui)
323 progressui._progbar = _singleton
78 progressui._progbar = _singleton
324
79
325 def reposetup(ui, repo):
80 def reposetup(ui, repo):
326 uisetup(repo.ui)
81 uisetup(repo.ui)
General Comments 0
You need to be logged in to leave comments. Login now