##// END OF EJS Templates
progress: determine padding width portably...
Augie Fackler -
r36169:d541042f default
parent child Browse files
Show More
@@ -1,303 +1,303
1 # progress.py progress bars related code
1 # progress.py progress bars related code
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import threading
11 import threading
12 import time
12 import time
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import encoding
15 from . import encoding
16
16
17 def spacejoin(*args):
17 def spacejoin(*args):
18 return ' '.join(s for s in args if s)
18 return ' '.join(s for s in args if s)
19
19
20 def shouldprint(ui):
20 def shouldprint(ui):
21 return not (ui.quiet or ui.plain('progress')) and (
21 return not (ui.quiet or ui.plain('progress')) and (
22 ui._isatty(ui.ferr) or ui.configbool('progress', 'assume-tty'))
22 ui._isatty(ui.ferr) or ui.configbool('progress', 'assume-tty'))
23
23
24 def fmtremaining(seconds):
24 def fmtremaining(seconds):
25 """format a number of remaining seconds in human readable way
25 """format a number of remaining seconds in human readable way
26
26
27 This will properly display seconds, minutes, hours, days if needed"""
27 This will properly display seconds, minutes, hours, days if needed"""
28 if seconds < 60:
28 if seconds < 60:
29 # i18n: format XX seconds as "XXs"
29 # i18n: format XX seconds as "XXs"
30 return _("%02ds") % (seconds)
30 return _("%02ds") % (seconds)
31 minutes = seconds // 60
31 minutes = seconds // 60
32 if minutes < 60:
32 if minutes < 60:
33 seconds -= minutes * 60
33 seconds -= minutes * 60
34 # i18n: format X minutes and YY seconds as "XmYYs"
34 # i18n: format X minutes and YY seconds as "XmYYs"
35 return _("%dm%02ds") % (minutes, seconds)
35 return _("%dm%02ds") % (minutes, seconds)
36 # we're going to ignore seconds in this case
36 # we're going to ignore seconds in this case
37 minutes += 1
37 minutes += 1
38 hours = minutes // 60
38 hours = minutes // 60
39 minutes -= hours * 60
39 minutes -= hours * 60
40 if hours < 30:
40 if hours < 30:
41 # i18n: format X hours and YY minutes as "XhYYm"
41 # i18n: format X hours and YY minutes as "XhYYm"
42 return _("%dh%02dm") % (hours, minutes)
42 return _("%dh%02dm") % (hours, minutes)
43 # we're going to ignore minutes in this case
43 # we're going to ignore minutes in this case
44 hours += 1
44 hours += 1
45 days = hours // 24
45 days = hours // 24
46 hours -= days * 24
46 hours -= days * 24
47 if days < 15:
47 if days < 15:
48 # i18n: format X days and YY hours as "XdYYh"
48 # i18n: format X days and YY hours as "XdYYh"
49 return _("%dd%02dh") % (days, hours)
49 return _("%dd%02dh") % (days, hours)
50 # we're going to ignore hours in this case
50 # we're going to ignore hours in this case
51 days += 1
51 days += 1
52 weeks = days // 7
52 weeks = days // 7
53 days -= weeks * 7
53 days -= weeks * 7
54 if weeks < 55:
54 if weeks < 55:
55 # i18n: format X weeks and YY days as "XwYYd"
55 # i18n: format X weeks and YY days as "XwYYd"
56 return _("%dw%02dd") % (weeks, days)
56 return _("%dw%02dd") % (weeks, days)
57 # we're going to ignore days and treat a year as 52 weeks
57 # we're going to ignore days and treat a year as 52 weeks
58 weeks += 1
58 weeks += 1
59 years = weeks // 52
59 years = weeks // 52
60 weeks -= years * 52
60 weeks -= years * 52
61 # i18n: format X years and YY weeks as "XyYYw"
61 # i18n: format X years and YY weeks as "XyYYw"
62 return _("%dy%02dw") % (years, weeks)
62 return _("%dy%02dw") % (years, weeks)
63
63
64 # file_write() and file_flush() of Python 2 do not restart on EINTR if
64 # file_write() and file_flush() of Python 2 do not restart on EINTR if
65 # the file is attached to a "slow" device (e.g. a terminal) and raise
65 # the file is attached to a "slow" device (e.g. a terminal) and raise
66 # IOError. We cannot know how many bytes would be written by file_write(),
66 # IOError. We cannot know how many bytes would be written by file_write(),
67 # but a progress text is known to be short enough to be written by a
67 # but a progress text is known to be short enough to be written by a
68 # single write() syscall, so we can just retry file_write() with the whole
68 # single write() syscall, so we can just retry file_write() with the whole
69 # text. (issue5532)
69 # text. (issue5532)
70 #
70 #
71 # This should be a short-term workaround. We'll need to fix every occurrence
71 # This should be a short-term workaround. We'll need to fix every occurrence
72 # of write() to a terminal or pipe.
72 # of write() to a terminal or pipe.
73 def _eintrretry(func, *args):
73 def _eintrretry(func, *args):
74 while True:
74 while True:
75 try:
75 try:
76 return func(*args)
76 return func(*args)
77 except IOError as err:
77 except IOError as err:
78 if err.errno == errno.EINTR:
78 if err.errno == errno.EINTR:
79 continue
79 continue
80 raise
80 raise
81
81
82 class progbar(object):
82 class progbar(object):
83 def __init__(self, ui):
83 def __init__(self, ui):
84 self.ui = ui
84 self.ui = ui
85 self._refreshlock = threading.Lock()
85 self._refreshlock = threading.Lock()
86 self.resetstate()
86 self.resetstate()
87
87
88 def resetstate(self):
88 def resetstate(self):
89 self.topics = []
89 self.topics = []
90 self.topicstates = {}
90 self.topicstates = {}
91 self.starttimes = {}
91 self.starttimes = {}
92 self.startvals = {}
92 self.startvals = {}
93 self.printed = False
93 self.printed = False
94 self.lastprint = time.time() + float(self.ui.config(
94 self.lastprint = time.time() + float(self.ui.config(
95 'progress', 'delay'))
95 'progress', 'delay'))
96 self.curtopic = None
96 self.curtopic = None
97 self.lasttopic = None
97 self.lasttopic = None
98 self.indetcount = 0
98 self.indetcount = 0
99 self.refresh = float(self.ui.config(
99 self.refresh = float(self.ui.config(
100 'progress', 'refresh'))
100 'progress', 'refresh'))
101 self.changedelay = max(3 * self.refresh,
101 self.changedelay = max(3 * self.refresh,
102 float(self.ui.config(
102 float(self.ui.config(
103 'progress', 'changedelay')))
103 'progress', 'changedelay')))
104 self.order = self.ui.configlist('progress', 'format')
104 self.order = self.ui.configlist('progress', 'format')
105 self.estimateinterval = self.ui.configwith(
105 self.estimateinterval = self.ui.configwith(
106 float, 'progress', 'estimateinterval')
106 float, 'progress', 'estimateinterval')
107
107
108 def show(self, now, topic, pos, item, unit, total):
108 def show(self, now, topic, pos, item, unit, total):
109 if not shouldprint(self.ui):
109 if not shouldprint(self.ui):
110 return
110 return
111 termwidth = self.width()
111 termwidth = self.width()
112 self.printed = True
112 self.printed = True
113 head = ''
113 head = ''
114 needprogress = False
114 needprogress = False
115 tail = ''
115 tail = ''
116 for indicator in self.order:
116 for indicator in self.order:
117 add = ''
117 add = ''
118 if indicator == 'topic':
118 if indicator == 'topic':
119 add = topic
119 add = topic
120 elif indicator == 'number':
120 elif indicator == 'number':
121 if total:
121 if total:
122 add = ('% ' + str(len(str(total))) +
122 padamount = '%d' % len(str(total))
123 's/%s') % (pos, total)
123 add = ('% '+ padamount + 's/%s') % (pos, total)
124 else:
124 else:
125 add = str(pos)
125 add = str(pos)
126 elif indicator.startswith('item') and item:
126 elif indicator.startswith('item') and item:
127 slice = 'end'
127 slice = 'end'
128 if '-' in indicator:
128 if '-' in indicator:
129 wid = int(indicator.split('-')[1])
129 wid = int(indicator.split('-')[1])
130 elif '+' in indicator:
130 elif '+' in indicator:
131 slice = 'beginning'
131 slice = 'beginning'
132 wid = int(indicator.split('+')[1])
132 wid = int(indicator.split('+')[1])
133 else:
133 else:
134 wid = 20
134 wid = 20
135 if slice == 'end':
135 if slice == 'end':
136 add = encoding.trim(item, wid, leftside=True)
136 add = encoding.trim(item, wid, leftside=True)
137 else:
137 else:
138 add = encoding.trim(item, wid)
138 add = encoding.trim(item, wid)
139 add += (wid - encoding.colwidth(add)) * ' '
139 add += (wid - encoding.colwidth(add)) * ' '
140 elif indicator == 'bar':
140 elif indicator == 'bar':
141 add = ''
141 add = ''
142 needprogress = True
142 needprogress = True
143 elif indicator == 'unit' and unit:
143 elif indicator == 'unit' and unit:
144 add = unit
144 add = unit
145 elif indicator == 'estimate':
145 elif indicator == 'estimate':
146 add = self.estimate(topic, pos, total, now)
146 add = self.estimate(topic, pos, total, now)
147 elif indicator == 'speed':
147 elif indicator == 'speed':
148 add = self.speed(topic, pos, unit, now)
148 add = self.speed(topic, pos, unit, now)
149 if not needprogress:
149 if not needprogress:
150 head = spacejoin(head, add)
150 head = spacejoin(head, add)
151 else:
151 else:
152 tail = spacejoin(tail, add)
152 tail = spacejoin(tail, add)
153 if needprogress:
153 if needprogress:
154 used = 0
154 used = 0
155 if head:
155 if head:
156 used += encoding.colwidth(head) + 1
156 used += encoding.colwidth(head) + 1
157 if tail:
157 if tail:
158 used += encoding.colwidth(tail) + 1
158 used += encoding.colwidth(tail) + 1
159 progwidth = termwidth - used - 3
159 progwidth = termwidth - used - 3
160 if total and pos <= total:
160 if total and pos <= total:
161 amt = pos * progwidth // total
161 amt = pos * progwidth // total
162 bar = '=' * (amt - 1)
162 bar = '=' * (amt - 1)
163 if amt > 0:
163 if amt > 0:
164 bar += '>'
164 bar += '>'
165 bar += ' ' * (progwidth - amt)
165 bar += ' ' * (progwidth - amt)
166 else:
166 else:
167 progwidth -= 3
167 progwidth -= 3
168 self.indetcount += 1
168 self.indetcount += 1
169 # mod the count by twice the width so we can make the
169 # mod the count by twice the width so we can make the
170 # cursor bounce between the right and left sides
170 # cursor bounce between the right and left sides
171 amt = self.indetcount % (2 * progwidth)
171 amt = self.indetcount % (2 * progwidth)
172 amt -= progwidth
172 amt -= progwidth
173 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
173 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
174 ' ' * int(abs(amt)))
174 ' ' * int(abs(amt)))
175 prog = ''.join(('[', bar, ']'))
175 prog = ''.join(('[', bar, ']'))
176 out = spacejoin(head, prog, tail)
176 out = spacejoin(head, prog, tail)
177 else:
177 else:
178 out = spacejoin(head, tail)
178 out = spacejoin(head, tail)
179 self._writeerr('\r' + encoding.trim(out, termwidth))
179 self._writeerr('\r' + encoding.trim(out, termwidth))
180 self.lasttopic = topic
180 self.lasttopic = topic
181 self._flusherr()
181 self._flusherr()
182
182
183 def clear(self):
183 def clear(self):
184 if not self.printed or not self.lastprint or not shouldprint(self.ui):
184 if not self.printed or not self.lastprint or not shouldprint(self.ui):
185 return
185 return
186 self._writeerr('\r%s\r' % (' ' * self.width()))
186 self._writeerr('\r%s\r' % (' ' * self.width()))
187 if self.printed:
187 if self.printed:
188 # force immediate re-paint of progress bar
188 # force immediate re-paint of progress bar
189 self.lastprint = 0
189 self.lastprint = 0
190
190
191 def complete(self):
191 def complete(self):
192 if not shouldprint(self.ui):
192 if not shouldprint(self.ui):
193 return
193 return
194 if self.ui.configbool('progress', 'clear-complete'):
194 if self.ui.configbool('progress', 'clear-complete'):
195 self.clear()
195 self.clear()
196 else:
196 else:
197 self._writeerr('\n')
197 self._writeerr('\n')
198 self._flusherr()
198 self._flusherr()
199
199
200 def _flusherr(self):
200 def _flusherr(self):
201 _eintrretry(self.ui.ferr.flush)
201 _eintrretry(self.ui.ferr.flush)
202
202
203 def _writeerr(self, msg):
203 def _writeerr(self, msg):
204 _eintrretry(self.ui.ferr.write, msg)
204 _eintrretry(self.ui.ferr.write, msg)
205
205
206 def width(self):
206 def width(self):
207 tw = self.ui.termwidth()
207 tw = self.ui.termwidth()
208 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
208 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
209
209
210 def estimate(self, topic, pos, total, now):
210 def estimate(self, topic, pos, total, now):
211 if total is None:
211 if total is None:
212 return ''
212 return ''
213 initialpos = self.startvals[topic]
213 initialpos = self.startvals[topic]
214 target = total - initialpos
214 target = total - initialpos
215 delta = pos - initialpos
215 delta = pos - initialpos
216 if delta > 0:
216 if delta > 0:
217 elapsed = now - self.starttimes[topic]
217 elapsed = now - self.starttimes[topic]
218 seconds = (elapsed * (target - delta)) // delta + 1
218 seconds = (elapsed * (target - delta)) // delta + 1
219 return fmtremaining(seconds)
219 return fmtremaining(seconds)
220 return ''
220 return ''
221
221
222 def speed(self, topic, pos, unit, now):
222 def speed(self, topic, pos, unit, now):
223 initialpos = self.startvals[topic]
223 initialpos = self.startvals[topic]
224 delta = pos - initialpos
224 delta = pos - initialpos
225 elapsed = now - self.starttimes[topic]
225 elapsed = now - self.starttimes[topic]
226 if elapsed > 0:
226 if elapsed > 0:
227 return _('%d %s/sec') % (delta / elapsed, unit)
227 return _('%d %s/sec') % (delta / elapsed, unit)
228 return ''
228 return ''
229
229
230 def _oktoprint(self, now):
230 def _oktoprint(self, now):
231 '''Check if conditions are met to print - e.g. changedelay elapsed'''
231 '''Check if conditions are met to print - e.g. changedelay elapsed'''
232 if (self.lasttopic is None # first time we printed
232 if (self.lasttopic is None # first time we printed
233 # not a topic change
233 # not a topic change
234 or self.curtopic == self.lasttopic
234 or self.curtopic == self.lasttopic
235 # it's been long enough we should print anyway
235 # it's been long enough we should print anyway
236 or now - self.lastprint >= self.changedelay):
236 or now - self.lastprint >= self.changedelay):
237 return True
237 return True
238 else:
238 else:
239 return False
239 return False
240
240
241 def _calibrateestimate(self, topic, now, pos):
241 def _calibrateestimate(self, topic, now, pos):
242 '''Adjust starttimes and startvals for topic so ETA works better
242 '''Adjust starttimes and startvals for topic so ETA works better
243
243
244 If progress is non-linear (ex. get much slower in the last minute),
244 If progress is non-linear (ex. get much slower in the last minute),
245 it's more friendly to only use a recent time span for ETA and speed
245 it's more friendly to only use a recent time span for ETA and speed
246 calculation.
246 calculation.
247
247
248 [======================================> ]
248 [======================================> ]
249 ^^^^^^^
249 ^^^^^^^
250 estimateinterval, only use this for estimation
250 estimateinterval, only use this for estimation
251 '''
251 '''
252 interval = self.estimateinterval
252 interval = self.estimateinterval
253 if interval <= 0:
253 if interval <= 0:
254 return
254 return
255 elapsed = now - self.starttimes[topic]
255 elapsed = now - self.starttimes[topic]
256 if elapsed > interval:
256 if elapsed > interval:
257 delta = pos - self.startvals[topic]
257 delta = pos - self.startvals[topic]
258 newdelta = delta * interval / elapsed
258 newdelta = delta * interval / elapsed
259 # If a stall happens temporarily, ETA could change dramatically
259 # If a stall happens temporarily, ETA could change dramatically
260 # frequently. This is to avoid such dramatical change and make ETA
260 # frequently. This is to avoid such dramatical change and make ETA
261 # smoother.
261 # smoother.
262 if newdelta < 0.1:
262 if newdelta < 0.1:
263 return
263 return
264 self.startvals[topic] = pos - newdelta
264 self.startvals[topic] = pos - newdelta
265 self.starttimes[topic] = now - interval
265 self.starttimes[topic] = now - interval
266
266
267 def progress(self, topic, pos, item='', unit='', total=None):
267 def progress(self, topic, pos, item='', unit='', total=None):
268 now = time.time()
268 now = time.time()
269 self._refreshlock.acquire()
269 self._refreshlock.acquire()
270 try:
270 try:
271 if pos is None:
271 if pos is None:
272 self.starttimes.pop(topic, None)
272 self.starttimes.pop(topic, None)
273 self.startvals.pop(topic, None)
273 self.startvals.pop(topic, None)
274 self.topicstates.pop(topic, None)
274 self.topicstates.pop(topic, None)
275 # reset the progress bar if this is the outermost topic
275 # reset the progress bar if this is the outermost topic
276 if self.topics and self.topics[0] == topic and self.printed:
276 if self.topics and self.topics[0] == topic and self.printed:
277 self.complete()
277 self.complete()
278 self.resetstate()
278 self.resetstate()
279 # truncate the list of topics assuming all topics within
279 # truncate the list of topics assuming all topics within
280 # this one are also closed
280 # this one are also closed
281 if topic in self.topics:
281 if topic in self.topics:
282 self.topics = self.topics[:self.topics.index(topic)]
282 self.topics = self.topics[:self.topics.index(topic)]
283 # reset the last topic to the one we just unwound to,
283 # reset the last topic to the one we just unwound to,
284 # so that higher-level topics will be stickier than
284 # so that higher-level topics will be stickier than
285 # lower-level topics
285 # lower-level topics
286 if self.topics:
286 if self.topics:
287 self.lasttopic = self.topics[-1]
287 self.lasttopic = self.topics[-1]
288 else:
288 else:
289 self.lasttopic = None
289 self.lasttopic = None
290 else:
290 else:
291 if topic not in self.topics:
291 if topic not in self.topics:
292 self.starttimes[topic] = now
292 self.starttimes[topic] = now
293 self.startvals[topic] = pos
293 self.startvals[topic] = pos
294 self.topics.append(topic)
294 self.topics.append(topic)
295 self.topicstates[topic] = pos, item, unit, total
295 self.topicstates[topic] = pos, item, unit, total
296 self.curtopic = topic
296 self.curtopic = topic
297 self._calibrateestimate(topic, now, pos)
297 self._calibrateestimate(topic, now, pos)
298 if now - self.lastprint >= self.refresh and self.topics:
298 if now - self.lastprint >= self.refresh and self.topics:
299 if self._oktoprint(now):
299 if self._oktoprint(now):
300 self.lastprint = now
300 self.lastprint = now
301 self.show(now, topic, *self.topicstates[topic])
301 self.show(now, topic, *self.topicstates[topic])
302 finally:
302 finally:
303 self._refreshlock.release()
303 self._refreshlock.release()
General Comments 0
You need to be logged in to leave comments. Login now