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