##// END OF EJS Templates
progress: use 'encoding.trim' to trim output line correctly...
FUJIWARA Katsunori -
r21859:be4270d2 default
parent child Browse files
Show More
@@ -1,303 +1,305 b''
1 1 # progress.py show progress bars for some actions
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 """show progress bars for some actions
9 9
10 10 This extension uses the progress information logged by hg commands
11 11 to draw progress bars that are as informative as possible. Some progress
12 12 bars only offer indeterminate information, while others have a definite
13 13 end point.
14 14
15 15 The following settings are available::
16 16
17 17 [progress]
18 18 delay = 3 # number of seconds (float) before showing the progress bar
19 19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
20 20 # If set to less than 3 * refresh, that value will
21 21 # be used instead.
22 22 refresh = 0.1 # time in seconds between refreshes of the progress bar
23 23 format = topic bar number estimate # format of the progress bar
24 24 width = <none> # if set, the maximum width of the progress information
25 25 # (that is, min(width, term width) will be used)
26 26 clear-complete = True # clear the progress bar after it's done
27 27 disable = False # if true, don't show a progress bar
28 28 assume-tty = False # if true, ALWAYS show a progress bar, unless
29 29 # disable is given
30 30
31 31 Valid entries for the format field are topic, bar, number, unit,
32 32 estimate, speed, and item. item defaults to the last 20 characters of
33 33 the item, but this can be changed by adding either ``-<num>`` which
34 34 would take the last num characters, or ``+<num>`` for the first num
35 35 characters.
36 36 """
37 37
38 38 import sys
39 39 import time
40 40
41 41 from mercurial.i18n import _
42 42 testedwith = 'internal'
43 43
44 from mercurial import encoding
45
44 46 def spacejoin(*args):
45 47 return ' '.join(s for s in args if s)
46 48
47 49 def shouldprint(ui):
48 50 return not ui.plain() and (ui._isatty(sys.stderr) or
49 51 ui.configbool('progress', 'assume-tty'))
50 52
51 53 def fmtremaining(seconds):
52 54 if seconds < 60:
53 55 # i18n: format XX seconds as "XXs"
54 56 return _("%02ds") % (seconds)
55 57 minutes = seconds // 60
56 58 if minutes < 60:
57 59 seconds -= minutes * 60
58 60 # i18n: format X minutes and YY seconds as "XmYYs"
59 61 return _("%dm%02ds") % (minutes, seconds)
60 62 # we're going to ignore seconds in this case
61 63 minutes += 1
62 64 hours = minutes // 60
63 65 minutes -= hours * 60
64 66 if hours < 30:
65 67 # i18n: format X hours and YY minutes as "XhYYm"
66 68 return _("%dh%02dm") % (hours, minutes)
67 69 # we're going to ignore minutes in this case
68 70 hours += 1
69 71 days = hours // 24
70 72 hours -= days * 24
71 73 if days < 15:
72 74 # i18n: format X days and YY hours as "XdYYh"
73 75 return _("%dd%02dh") % (days, hours)
74 76 # we're going to ignore hours in this case
75 77 days += 1
76 78 weeks = days // 7
77 79 days -= weeks * 7
78 80 if weeks < 55:
79 81 # i18n: format X weeks and YY days as "XwYYd"
80 82 return _("%dw%02dd") % (weeks, days)
81 83 # we're going to ignore days and treat a year as 52 weeks
82 84 weeks += 1
83 85 years = weeks // 52
84 86 weeks -= years * 52
85 87 # i18n: format X years and YY weeks as "XyYYw"
86 88 return _("%dy%02dw") % (years, weeks)
87 89
88 90 class progbar(object):
89 91 def __init__(self, ui):
90 92 self.ui = ui
91 93 self.resetstate()
92 94
93 95 def resetstate(self):
94 96 self.topics = []
95 97 self.topicstates = {}
96 98 self.starttimes = {}
97 99 self.startvals = {}
98 100 self.printed = False
99 101 self.lastprint = time.time() + float(self.ui.config(
100 102 'progress', 'delay', default=3))
101 103 self.lasttopic = None
102 104 self.indetcount = 0
103 105 self.refresh = float(self.ui.config(
104 106 'progress', 'refresh', default=0.1))
105 107 self.changedelay = max(3 * self.refresh,
106 108 float(self.ui.config(
107 109 'progress', 'changedelay', default=1)))
108 110 self.order = self.ui.configlist(
109 111 'progress', 'format',
110 112 default=['topic', 'bar', 'number', 'estimate'])
111 113
112 114 def show(self, now, topic, pos, item, unit, total):
113 115 if not shouldprint(self.ui):
114 116 return
115 117 termwidth = self.width()
116 118 self.printed = True
117 119 head = ''
118 120 needprogress = False
119 121 tail = ''
120 122 for indicator in self.order:
121 123 add = ''
122 124 if indicator == 'topic':
123 125 add = topic
124 126 elif indicator == 'number':
125 127 if total:
126 128 add = ('% ' + str(len(str(total))) +
127 129 's/%s') % (pos, total)
128 130 else:
129 131 add = str(pos)
130 132 elif indicator.startswith('item') and item:
131 133 slice = 'end'
132 134 if '-' in indicator:
133 135 wid = int(indicator.split('-')[1])
134 136 elif '+' in indicator:
135 137 slice = 'beginning'
136 138 wid = int(indicator.split('+')[1])
137 139 else:
138 140 wid = 20
139 141 if slice == 'end':
140 142 add = item[-wid:]
141 143 else:
142 144 add = item[:wid]
143 145 add += (wid - len(add)) * ' '
144 146 elif indicator == 'bar':
145 147 add = ''
146 148 needprogress = True
147 149 elif indicator == 'unit' and unit:
148 150 add = unit
149 151 elif indicator == 'estimate':
150 152 add = self.estimate(topic, pos, total, now)
151 153 elif indicator == 'speed':
152 154 add = self.speed(topic, pos, unit, now)
153 155 if not needprogress:
154 156 head = spacejoin(head, add)
155 157 else:
156 158 tail = spacejoin(tail, add)
157 159 if needprogress:
158 160 used = 0
159 161 if head:
160 162 used += len(head) + 1
161 163 if tail:
162 164 used += len(tail) + 1
163 165 progwidth = termwidth - used - 3
164 166 if total and pos <= total:
165 167 amt = pos * progwidth // total
166 168 bar = '=' * (amt - 1)
167 169 if amt > 0:
168 170 bar += '>'
169 171 bar += ' ' * (progwidth - amt)
170 172 else:
171 173 progwidth -= 3
172 174 self.indetcount += 1
173 175 # mod the count by twice the width so we can make the
174 176 # cursor bounce between the right and left sides
175 177 amt = self.indetcount % (2 * progwidth)
176 178 amt -= progwidth
177 179 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
178 180 ' ' * int(abs(amt)))
179 181 prog = ''.join(('[', bar , ']'))
180 182 out = spacejoin(head, prog, tail)
181 183 else:
182 184 out = spacejoin(head, tail)
183 sys.stderr.write('\r' + out[:termwidth])
185 sys.stderr.write('\r' + encoding.trim(out, termwidth))
184 186 self.lasttopic = topic
185 187 sys.stderr.flush()
186 188
187 189 def clear(self):
188 190 if not shouldprint(self.ui):
189 191 return
190 192 sys.stderr.write('\r%s\r' % (' ' * self.width()))
191 193
192 194 def complete(self):
193 195 if not shouldprint(self.ui):
194 196 return
195 197 if self.ui.configbool('progress', 'clear-complete', default=True):
196 198 self.clear()
197 199 else:
198 200 sys.stderr.write('\n')
199 201 sys.stderr.flush()
200 202
201 203 def width(self):
202 204 tw = self.ui.termwidth()
203 205 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
204 206
205 207 def estimate(self, topic, pos, total, now):
206 208 if total is None:
207 209 return ''
208 210 initialpos = self.startvals[topic]
209 211 target = total - initialpos
210 212 delta = pos - initialpos
211 213 if delta > 0:
212 214 elapsed = now - self.starttimes[topic]
213 215 if elapsed > float(
214 216 self.ui.config('progress', 'estimate', default=2)):
215 217 seconds = (elapsed * (target - delta)) // delta + 1
216 218 return fmtremaining(seconds)
217 219 return ''
218 220
219 221 def speed(self, topic, pos, unit, now):
220 222 initialpos = self.startvals[topic]
221 223 delta = pos - initialpos
222 224 elapsed = now - self.starttimes[topic]
223 225 if elapsed > float(
224 226 self.ui.config('progress', 'estimate', default=2)):
225 227 return _('%d %s/sec') % (delta / elapsed, unit)
226 228 return ''
227 229
228 230 def progress(self, topic, pos, item='', unit='', total=None):
229 231 now = time.time()
230 232 if pos is None:
231 233 self.starttimes.pop(topic, None)
232 234 self.startvals.pop(topic, None)
233 235 self.topicstates.pop(topic, None)
234 236 # reset the progress bar if this is the outermost topic
235 237 if self.topics and self.topics[0] == topic and self.printed:
236 238 self.complete()
237 239 self.resetstate()
238 240 # truncate the list of topics assuming all topics within
239 241 # this one are also closed
240 242 if topic in self.topics:
241 243 self.topics = self.topics[:self.topics.index(topic)]
242 244 # reset the last topic to the one we just unwound to,
243 245 # so that higher-level topics will be stickier than
244 246 # lower-level topics
245 247 if self.topics:
246 248 self.lasttopic = self.topics[-1]
247 249 else:
248 250 self.lasttopic = None
249 251 else:
250 252 if topic not in self.topics:
251 253 self.starttimes[topic] = now
252 254 self.startvals[topic] = pos
253 255 self.topics.append(topic)
254 256 self.topicstates[topic] = pos, item, unit, total
255 257 if now - self.lastprint >= self.refresh and self.topics:
256 258 if (self.lasttopic is None # first time we printed
257 259 # not a topic change
258 260 or topic == self.lasttopic
259 261 # it's been long enough we should print anyway
260 262 or now - self.lastprint >= self.changedelay):
261 263 self.lastprint = now
262 264 self.show(now, topic, *self.topicstates[topic])
263 265
264 266 _singleton = None
265 267
266 268 def uisetup(ui):
267 269 global _singleton
268 270 class progressui(ui.__class__):
269 271 _progbar = None
270 272
271 273 def _quiet(self):
272 274 return self.debugflag or self.quiet
273 275
274 276 def progress(self, *args, **opts):
275 277 if not self._quiet():
276 278 self._progbar.progress(*args, **opts)
277 279 return super(progressui, self).progress(*args, **opts)
278 280
279 281 def write(self, *args, **opts):
280 282 if not self._quiet() and self._progbar.printed:
281 283 self._progbar.clear()
282 284 return super(progressui, self).write(*args, **opts)
283 285
284 286 def write_err(self, *args, **opts):
285 287 if not self._quiet() and self._progbar.printed:
286 288 self._progbar.clear()
287 289 return super(progressui, self).write_err(*args, **opts)
288 290
289 291 # Apps that derive a class from ui.ui() can use
290 292 # setconfig('progress', 'disable', 'True') to disable this extension
291 293 if ui.configbool('progress', 'disable'):
292 294 return
293 295 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
294 296 ui.__class__ = progressui
295 297 # we instantiate one globally shared progress bar to avoid
296 298 # competing progress bars when multiple UI objects get created
297 299 if not progressui._progbar:
298 300 if _singleton is None:
299 301 _singleton = progbar(ui)
300 302 progressui._progbar = _singleton
301 303
302 304 def reposetup(ui, repo):
303 305 uisetup(repo.ui)
@@ -1,240 +1,277 b''
1 1
2 2 $ cat > loop.py <<EOF
3 3 > from mercurial import cmdutil, commands
4 4 > import time
5 5 >
6 6 > cmdtable = {}
7 7 > command = cmdutil.command(cmdtable)
8 8 >
9 9 > class incrementingtime(object):
10 10 > def __init__(self):
11 11 > self._time = 0.0
12 12 > def __call__(self):
13 13 > self._time += 0.25
14 14 > return self._time
15 15 > time.time = incrementingtime()
16 16 >
17 17 > @command('loop',
18 18 > [('', 'total', '', 'override for total'),
19 19 > ('', 'nested', False, 'show nested results'),
20 20 > ('', 'parallel', False, 'show parallel sets of results')],
21 21 > 'hg loop LOOPS',
22 22 > norepo=True)
23 23 > def loop(ui, loops, **opts):
24 24 > loops = int(loops)
25 25 > total = None
26 26 > if loops >= 0:
27 27 > total = loops
28 28 > if opts.get('total', None):
29 29 > total = int(opts.get('total'))
30 30 > nested = False
31 31 > if opts.get('nested', None):
32 32 > nested = True
33 33 > loops = abs(loops)
34 34 >
35 35 > for i in range(loops):
36 > ui.progress('loop', i, 'loop.%d' % i, 'loopnum', total)
36 > ui.progress(topiclabel, i, 'loop.%d' % i, 'loopnum', total)
37 37 > if opts.get('parallel'):
38 38 > ui.progress('other', i, 'other.%d' % i, 'othernum', total)
39 39 > if nested:
40 40 > nested_steps = 2
41 41 > if i and i % 4 == 0:
42 42 > nested_steps = 5
43 43 > for j in range(nested_steps):
44 44 > ui.progress(
45 45 > 'nested', j, 'nested.%d' % j, 'nestnum', nested_steps)
46 46 > ui.progress(
47 47 > 'nested', None, 'nested.done', 'nestnum', nested_steps)
48 > ui.progress('loop', None, 'loop.done', 'loopnum', total)
48 > ui.progress(topiclabel, None, 'loop.done', 'loopnum', total)
49 >
50 > topiclabel = 'loop'
49 51 >
50 52 > EOF
51 53
52 54 $ cp $HGRCPATH $HGRCPATH.orig
53 55 $ echo "[extensions]" >> $HGRCPATH
54 56 $ echo "progress=" >> $HGRCPATH
55 57 $ echo "loop=`pwd`/loop.py" >> $HGRCPATH
56 58 $ echo "[progress]" >> $HGRCPATH
57 59 $ echo "format = topic bar number" >> $HGRCPATH
58 60 $ echo "assume-tty=1" >> $HGRCPATH
59 61 $ echo "width=60" >> $HGRCPATH
60 62
61 63 test default params, display nothing because of delay
62 64
63 65 $ hg -y loop 3
64 66 $ echo "delay=0" >> $HGRCPATH
65 67 $ echo "refresh=0" >> $HGRCPATH
66 68
67 69 test with delay=0, refresh=0
68 70
69 71 $ hg -y loop 3
70 72 \r (no-eol) (esc)
71 73 loop [ ] 0/3\r (no-eol) (esc)
72 74 loop [===============> ] 1/3\r (no-eol) (esc)
73 75 loop [===============================> ] 2/3\r (no-eol) (esc)
74 76 \r (no-eol) (esc)
75 77
76 78
77 79 test nested short-lived topics (which shouldn't display with nestdelay):
78 80
79 81 $ hg -y loop 3 --nested
80 82 \r (no-eol) (esc)
81 83 loop [ ] 0/3\r (no-eol) (esc)
82 84 loop [===============> ] 1/3\r (no-eol) (esc)
83 85 loop [===============================> ] 2/3\r (no-eol) (esc)
84 86 \r (no-eol) (esc)
85 87
86 88 Test nested long-lived topic which has the same name as a short-lived
87 89 peer. We shouldn't get stuck showing the short-lived inner steps, and
88 90 should go back to skipping the inner steps when the slow nested step
89 91 finishes.
90 92
91 93 $ hg -y loop 7 --nested
92 94 \r (no-eol) (esc)
93 95 loop [ ] 0/7\r (no-eol) (esc)
94 96 loop [=====> ] 1/7\r (no-eol) (esc)
95 97 loop [============> ] 2/7\r (no-eol) (esc)
96 98 loop [===================> ] 3/7\r (no-eol) (esc)
97 99 loop [==========================> ] 4/7\r (no-eol) (esc)
98 100 nested [==========================> ] 3/5\r (no-eol) (esc)
99 101 nested [===================================> ] 4/5\r (no-eol) (esc)
100 102 loop [=================================> ] 5/7\r (no-eol) (esc)
101 103 loop [========================================> ] 6/7\r (no-eol) (esc)
102 104 \r (no-eol) (esc)
103 105
104 106
105 107 $ hg --config progress.changedelay=0 -y loop 3 --nested
106 108 \r (no-eol) (esc)
107 109 loop [ ] 0/3\r (no-eol) (esc)
108 110 nested [ ] 0/2\r (no-eol) (esc)
109 111 nested [======================> ] 1/2\r (no-eol) (esc)
110 112 loop [===============> ] 1/3\r (no-eol) (esc)
111 113 nested [ ] 0/2\r (no-eol) (esc)
112 114 nested [======================> ] 1/2\r (no-eol) (esc)
113 115 loop [===============================> ] 2/3\r (no-eol) (esc)
114 116 nested [ ] 0/2\r (no-eol) (esc)
115 117 nested [======================> ] 1/2\r (no-eol) (esc)
116 118 \r (no-eol) (esc)
117 119
118 120
119 121 test two topics being printed in parallel (as when we're doing a local
120 122 --pull clone, where you get the unbundle and bundle progress at the
121 123 same time):
122 124 $ hg loop 3 --parallel
123 125 \r (no-eol) (esc)
124 126 loop [ ] 0/3\r (no-eol) (esc)
125 127 loop [===============> ] 1/3\r (no-eol) (esc)
126 128 loop [===============================> ] 2/3\r (no-eol) (esc)
127 129 \r (no-eol) (esc)
128 130 test refresh is taken in account
129 131
130 132 $ hg -y --config progress.refresh=100 loop 3
131 133
132 134 test format options 1
133 135
134 136 $ hg -y --config 'progress.format=number topic item+2' loop 2
135 137 \r (no-eol) (esc)
136 138 0/2 loop lo\r (no-eol) (esc)
137 139 1/2 loop lo\r (no-eol) (esc)
138 140 \r (no-eol) (esc)
139 141
140 142 test format options 2
141 143
142 144 $ hg -y --config 'progress.format=number item-3 bar' loop 2
143 145 \r (no-eol) (esc)
144 146 0/2 p.0 [ ]\r (no-eol) (esc)
145 147 1/2 p.1 [=======================> ]\r (no-eol) (esc)
146 148 \r (no-eol) (esc)
147 149
148 150 test format options and indeterminate progress
149 151
150 152 $ hg -y --config 'progress.format=number item bar' loop -- -2
151 153 \r (no-eol) (esc)
152 154 0 loop.0 [ <=> ]\r (no-eol) (esc)
153 155 1 loop.1 [ <=> ]\r (no-eol) (esc)
154 156 \r (no-eol) (esc)
155 157
156 158 make sure things don't fall over if count > total
157 159
158 160 $ hg -y loop --total 4 6
159 161 \r (no-eol) (esc)
160 162 loop [ ] 0/4\r (no-eol) (esc)
161 163 loop [===========> ] 1/4\r (no-eol) (esc)
162 164 loop [=======================> ] 2/4\r (no-eol) (esc)
163 165 loop [===================================> ] 3/4\r (no-eol) (esc)
164 166 loop [===============================================>] 4/4\r (no-eol) (esc)
165 167 loop [ <=> ] 5/4\r (no-eol) (esc)
166 168 \r (no-eol) (esc)
167 169
168 170 test immediate progress completion
169 171
170 172 $ hg -y loop 0
171 173
172 174 test delay time estimates
173 175
174 176 $ cat > mocktime.py <<EOF
175 177 > import os
176 178 > import time
177 179 >
178 180 > class mocktime(object):
179 181 > def __init__(self, increment):
180 182 > self.time = 0
181 183 > self.increment = increment
182 184 > def __call__(self):
183 185 > self.time += self.increment
184 186 > return self.time
185 187 >
186 188 > def uisetup(ui):
187 189 > time.time = mocktime(int(os.environ.get('MOCKTIME', '11')))
188 190 > EOF
189 191
190 192 $ cp $HGRCPATH.orig $HGRCPATH
191 193 $ echo "[extensions]" >> $HGRCPATH
192 194 $ echo "mocktime=`pwd`/mocktime.py" >> $HGRCPATH
193 195 $ echo "progress=" >> $HGRCPATH
194 196 $ echo "loop=`pwd`/loop.py" >> $HGRCPATH
195 197 $ echo "[progress]" >> $HGRCPATH
196 198 $ echo "assume-tty=1" >> $HGRCPATH
197 199 $ echo "delay=25" >> $HGRCPATH
198 200 $ echo "width=60" >> $HGRCPATH
199 201
200 202 $ hg -y loop 8
201 203 \r (no-eol) (esc)
202 204 loop [=========> ] 2/8 1m07s\r (no-eol) (esc)
203 205 loop [===============> ] 3/8 56s\r (no-eol) (esc)
204 206 loop [=====================> ] 4/8 45s\r (no-eol) (esc)
205 207 loop [==========================> ] 5/8 34s\r (no-eol) (esc)
206 208 loop [================================> ] 6/8 23s\r (no-eol) (esc)
207 209 loop [=====================================> ] 7/8 12s\r (no-eol) (esc)
208 210 \r (no-eol) (esc)
209 211
210 212 $ MOCKTIME=10000 hg -y loop 4
211 213 \r (no-eol) (esc)
212 214 loop [ ] 0/4\r (no-eol) (esc)
213 215 loop [=========> ] 1/4 8h21m\r (no-eol) (esc)
214 216 loop [====================> ] 2/4 5h34m\r (no-eol) (esc)
215 217 loop [==============================> ] 3/4 2h47m\r (no-eol) (esc)
216 218 \r (no-eol) (esc)
217 219
218 220 $ MOCKTIME=1000000 hg -y loop 4
219 221 \r (no-eol) (esc)
220 222 loop [ ] 0/4\r (no-eol) (esc)
221 223 loop [=========> ] 1/4 5w00d\r (no-eol) (esc)
222 224 loop [====================> ] 2/4 3w03d\r (no-eol) (esc)
223 225 loop [=============================> ] 3/4 11d14h\r (no-eol) (esc)
224 226 \r (no-eol) (esc)
225 227
226 228
227 229 $ MOCKTIME=14000000 hg -y loop 4
228 230 \r (no-eol) (esc)
229 231 loop [ ] 0/4\r (no-eol) (esc)
230 232 loop [=========> ] 1/4 1y18w\r (no-eol) (esc)
231 233 loop [===================> ] 2/4 46w03d\r (no-eol) (esc)
232 234 loop [=============================> ] 3/4 23w02d\r (no-eol) (esc)
233 235 \r (no-eol) (esc)
234 236
235 237 Time estimates should not fail when there's no end point:
236 238 $ hg -y loop -- -4
237 239 \r (no-eol) (esc)
238 240 loop [ <=> ] 2\r (no-eol) (esc)
239 241 loop [ <=> ] 3\r (no-eol) (esc)
240 242 \r (no-eol) (esc)
243
244 test line trimming by '[progress] width', when progress topic contains
245 multi-byte characters, of which length of byte sequence and columns in
246 display are different from each other.
247
248 $ cp $HGRCPATH.orig $HGRCPATH
249 $ cat >> $HGRCPATH <<EOF
250 > [extensions]
251 > progress=
252 > loop=`pwd`/loop.py
253 > [progress]
254 > assume-tty = 1
255 > delay = 0
256 > refresh = 0
257 > EOF
258
259 $ rm -f loop.pyc
260 $ cat >> loop.py <<EOF
261 > # use non-ascii characters as topic label of progress
262 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
263 > topiclabel = u'\u3042\u3044\u3046\u3048'.encode('utf-8')
264 > EOF
265
266 $ cat >> $HGRCPATH <<EOF
267 > [progress]
268 > format = topic number
269 > width= 12
270 > EOF
271
272 $ hg --encoding utf-8 -y loop --total 3 3
273 \r (no-eol) (esc)
274 \xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88 0/3\r (no-eol) (esc)
275 \xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88 1/3\r (no-eol) (esc)
276 \xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88 2/3\r (no-eol) (esc)
277 \r (no-eol) (esc)
General Comments 0
You need to be logged in to leave comments. Login now