comparison mercurial/progress.py @ 25497:93b8b0049932

progress: move most extension code into a 'mercurial.progress' module This initiate the relocation of progress into core.
author Pierre-Yves David <pierre-yves.david@fb.com>
date Sun, 07 Jun 2015 17:19:20 -0700
parents
children 79c75459321e
comparison
equal deleted inserted replaced
25496:38fd17bcc083 25497:93b8b0049932
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