initial working implementation of ProgressBar2
This commit is contained in:
parent
b691bb9b25
commit
3a0f77ab96
1 changed files with 157 additions and 132 deletions
287
core/text.py
287
core/text.py
|
@ -42,13 +42,21 @@ def lexical_join(words, oxford=False):
|
|||
# --------------------------------------------------
|
||||
|
||||
class _ProgressBarChannel:
|
||||
def __init__(self, unit, top, prefix_base2, precision):
|
||||
def __init__(self, unit, top, precision, use_prefixes=True, prefix_base2=False, min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7):
|
||||
self.unit = unit
|
||||
self.top = top
|
||||
self.dynamic_top = top is None
|
||||
self.prefix_base2 = prefix_base2
|
||||
self.precision = precision
|
||||
self.use_prefixes = use_prefixes
|
||||
self.min_width = {
|
||||
"raw": min_width_raw,
|
||||
"rate": min_width_rate,
|
||||
"percent": min_width_percent,
|
||||
"time": min_width_time
|
||||
}
|
||||
|
||||
self.value = 0
|
||||
self._value = 0
|
||||
self.set()
|
||||
|
||||
if prefix_base2:
|
||||
|
@ -56,11 +64,42 @@ class _ProgressBarChannel:
|
|||
|
||||
def set(self, value=None):
|
||||
if value is not None:
|
||||
self.value = value
|
||||
self._value = value
|
||||
|
||||
self.ratio = self.value / self.top
|
||||
s = Unit(self.value, self.unit, format="%.{:d}f pU".format(self.precision))
|
||||
self.text = str(s).split(" ", 1)
|
||||
self.ratio = self._value / self.top if self.top else None
|
||||
|
||||
def _render(self, value, unit=None, just=None):
|
||||
if unit is None:
|
||||
unit = self.unit
|
||||
|
||||
u = Unit(value, unit, format="%.{:d}f pU".format(self.precision))
|
||||
|
||||
if not self.use_prefixes or just == "percent":
|
||||
u._prefixes = (('', 0),) # Unit needs fixin'
|
||||
|
||||
s = str(u)
|
||||
|
||||
if just:
|
||||
s = s.rjust(self.min_width[just])
|
||||
|
||||
return s
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._render(self._value, just="raw")
|
||||
|
||||
@property
|
||||
def inverse(self):
|
||||
return self._render(self.top - self._value, just="raw")
|
||||
|
||||
def divide(self, amount):
|
||||
"""
|
||||
Divides the amount into amount * ratio, amount * (1 - ratio) and rounds to integers.
|
||||
"""
|
||||
|
||||
A = round(amount * self.ratio)
|
||||
|
||||
return (A, amount - A)
|
||||
|
||||
class ProgressBar2:
|
||||
"""
|
||||
|
@ -68,7 +107,7 @@ class ProgressBar2:
|
|||
|
||||
Each atom consists of three characters: a literal §, an operator and a channel.
|
||||
An operator is one of:
|
||||
% - a percentage (value from 0 to 100, padded to three characters)
|
||||
% - a percentage (value from 0 to 100, padded with spaces to three characters)
|
||||
r - displays the raw value of the channel
|
||||
z - displays the maximum (total) value
|
||||
s - displays the rate of change in units/s
|
||||
|
@ -80,7 +119,7 @@ class ProgressBar2:
|
|||
at 100% it fills the entirety of the remaining space
|
||||
|
||||
A channel ID as a lowercase letter is the channel's actual value. An uppercase letter
|
||||
is that channel's complement.
|
||||
is that channel's complement. Operations z, s, m and h cannot have a complement.
|
||||
|
||||
Use §§ to display a literal §.
|
||||
|
||||
|
@ -93,7 +132,7 @@ class ProgressBar2:
|
|||
Output: 53 % (21.0/39.6 kf) [---------------------> ] 27.1 f/s [233/439 Mio] (ETA 00:11:26)
|
||||
"""
|
||||
|
||||
def __init__(self, format, channels, width=None, space_before_unit=True):
|
||||
def __init__(self, format, channels, width=None):
|
||||
"""
|
||||
Initialize the ProgressBar.
|
||||
|
||||
|
@ -130,8 +169,7 @@ class ProgressBar2:
|
|||
|
||||
self.format = format
|
||||
self.channels = {id: _ProgressBarChannel(**conf) for id, conf in channels.items()}
|
||||
self.space_before_unit = space_before_unit
|
||||
self.time_start = time.time()
|
||||
self.time_start = None
|
||||
self.width = width
|
||||
|
||||
def set(self, channel_id, value):
|
||||
|
@ -142,6 +180,11 @@ class ProgressBar2:
|
|||
c = self.channels[channel_id]
|
||||
c.set(value)
|
||||
|
||||
if c.ratio is not None:
|
||||
for channel in self.channels.values():
|
||||
if channel.dynamic_top:
|
||||
channel.top = channel._value / c.ratio
|
||||
|
||||
def render(self, clear=True, stream=sys.stderr):
|
||||
"""
|
||||
Renders the progressbar into a stream. If clear is True, clears the previous content first.
|
||||
|
@ -149,18 +192,83 @@ class ProgressBar2:
|
|||
|
||||
width = self.width or get_terminal_size()[1]
|
||||
raw = self.format
|
||||
t = time.time()
|
||||
|
||||
for tag in re.findall("§.[a-zA-Z]", raw):
|
||||
if not self.time_start:
|
||||
self.time_start = t
|
||||
|
||||
for tag in re.findall("§[%rzsmhtT][a-zA-Z]", raw):
|
||||
op = tag[1]
|
||||
ch = tag[2]
|
||||
channel = self.channels[ch.lower()]
|
||||
inverse = ch.isupper()
|
||||
|
||||
# display literal §s
|
||||
if op == "%":
|
||||
ratio = 1 - channel.ratio if inverse else channel.ratio
|
||||
|
||||
raw = raw.replace(tag, channel._render(ratio * 100, "%", "percent"), 1)
|
||||
|
||||
elif op == "r": # display the current value
|
||||
raw = raw.replace(tag, channel.inverse if inverse else channel.value, 1)
|
||||
|
||||
elif op == "z": # display the top value
|
||||
if inverse:
|
||||
raise ValueError("inverse of the top value is not defined: {:s}".format(tag))
|
||||
|
||||
if channel.top:
|
||||
s = channel._render(channel.top, just="raw")
|
||||
else:
|
||||
s = "- {:s}".format(channel.unit).rjust(channel.min_width["raw"])
|
||||
|
||||
raw = raw.replace(tag, s, 1)
|
||||
|
||||
elif op.lower() == "t":
|
||||
t_elapsed = t - self.time_start
|
||||
t_to_end = (t_elapsed * (channel.top - channel._value) / channel._value) if channel._value > 0 else None
|
||||
|
||||
raw = raw.replace(tag, self._render_time(t_to_end if inverse else t_elapsed, op.isupper(), channel.min_width["time"]), 1)
|
||||
|
||||
elif op in "smh":
|
||||
if inverse:
|
||||
raise ValueError("the inverse of rates is not defined: {:s}".format(tag))
|
||||
|
||||
t_elapsed = t - self.time_start
|
||||
|
||||
if op == "s":
|
||||
t_unit = "s"
|
||||
t_div = 1
|
||||
elif op == "m":
|
||||
t_unit = "min"
|
||||
t_div = 60
|
||||
elif op == "h":
|
||||
t_unit = "h"
|
||||
t_div = 3600
|
||||
|
||||
if t_elapsed > 0:
|
||||
rate = t_div * channel._value / t_elapsed
|
||||
s = channel._render(rate, channel.unit + "/" + t_unit, "rate")
|
||||
else:
|
||||
s = "- {:s}/{:s}".format(channel.unit, t_unit).rjust(channel.min_width["rate"])
|
||||
|
||||
raw = raw.replace(tag, s, 1)
|
||||
|
||||
# all that remains are bars
|
||||
free_space = width - len(re.sub("§[^%rzsmhtT][a-zA-Z]", "", raw))
|
||||
|
||||
for tag in re.findall("§[^%rzsmhtT][a-zA-Z]", raw):
|
||||
op = tag[1]
|
||||
ch = tag[2]
|
||||
channel = self.channels[ch.lower()]
|
||||
lengths = channel.divide(free_space)
|
||||
|
||||
raw = raw.replace(tag, op * lengths[1 if ch.isupper() else 0], 1)
|
||||
|
||||
# render literal §s
|
||||
raw = raw.replace("§§", "§")
|
||||
|
||||
# pave over previous render and draw the new one
|
||||
if clear:
|
||||
stream.write("\r" + "_" * width)
|
||||
stream.write("\r" + " " * width)
|
||||
|
||||
stream.write("\r" + raw)
|
||||
|
||||
|
@ -179,6 +287,20 @@ class ProgressBar2:
|
|||
|
||||
print()
|
||||
|
||||
def _render_time(self, seconds, hhmmss=False, just=0):
|
||||
if hhmmss:
|
||||
if seconds is None:
|
||||
output = "--:--:--"
|
||||
else:
|
||||
mm, ss = divmod(int(seconds), 60)
|
||||
hh, mm = divmod(mm, 60)
|
||||
output = "{:02d}:{:02d}:{:02d}".format(hh, mm, ss)
|
||||
|
||||
else:
|
||||
output = "- s" if seconds is None else "{:d} s".format(int(seconds))
|
||||
|
||||
return output.rjust(just)
|
||||
|
||||
class ProgressBar:
|
||||
'''
|
||||
An animated progress bar.
|
||||
|
@ -203,6 +325,8 @@ class ProgressBar:
|
|||
self.old_len = 0
|
||||
self.t_start = None
|
||||
|
||||
_print("over.core.text.ProgressBar is deprecated and will be replaced by ProgressBar2 soon", prefix.warn)
|
||||
|
||||
def draw(self):
|
||||
if not self.t_start:
|
||||
self.t_start = time.time()
|
||||
|
@ -401,116 +525,6 @@ def paragraph(text, width=0, indent=0, prefix=None, stamp=None):
|
|||
|
||||
return '\n'.join(lines_tmp) # put lines together
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
# def strtrim(text, length, mode=0, dots=True, colors=True):
|
||||
# '''
|
||||
# str text text to trim
|
||||
# int length desired length, >1
|
||||
# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right
|
||||
# bool dots add an ellipsis to the point of cutting
|
||||
# bool colors dont count color tags into length; also turns the ellipsis blue
|
||||
# '''
|
||||
|
||||
# if len(text) <= length:
|
||||
# return text
|
||||
|
||||
# if length <= 3:
|
||||
# dots = False
|
||||
|
||||
# if dots:
|
||||
# length -= 3
|
||||
|
||||
# if mode == -1:
|
||||
# if dots:
|
||||
# return '...' + cut(text, length, 1, colors)
|
||||
# else:
|
||||
# return cut(text, length, 1, colors)
|
||||
|
||||
# elif mode == 0:
|
||||
# if length%2 == 1:
|
||||
# part1 = cut(text, length/2+1, 0, colors)
|
||||
# else:
|
||||
# part1 = cut(text, length/2, 0, colors)
|
||||
|
||||
# part2 = cut(text, length/2, 1, colors)
|
||||
|
||||
# if dots:
|
||||
# part1 += '...'
|
||||
|
||||
# return part1 + part2
|
||||
|
||||
# else:
|
||||
# if dots:
|
||||
# return cut(text, length, 0, colors) + '...'
|
||||
# else:
|
||||
# return cut(text, length, 0, colors)
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
# def textfilter(text, set=None):
|
||||
# '''
|
||||
# str text text to filter
|
||||
# dict set {to_replace: replacement}
|
||||
|
||||
# Text filter that replaces occurences of to_replace keys with their respective values.
|
||||
|
||||
# Defaults to filtering of 'bad' characters if no translational dictionary is provided.
|
||||
# '''
|
||||
|
||||
# if not set:
|
||||
# set = badchars
|
||||
|
||||
# for to_replace in set.keys():
|
||||
# text = text.replace(to_replace, set[to_replace])
|
||||
|
||||
# return text
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
# def cut(text, size, end=0, colors=True):
|
||||
# '''
|
||||
# str text text to cut
|
||||
# int size how many chars to return, >=0
|
||||
# int end return chars from 0 = left, 1 = right
|
||||
# bool colors skip color tags
|
||||
# '''
|
||||
|
||||
# out = ''
|
||||
|
||||
# strlen = len(text)
|
||||
|
||||
# if end == 0:
|
||||
# ptr = 0 # go from left
|
||||
# else:
|
||||
# ptr = strlen - 1
|
||||
|
||||
# while size:
|
||||
# if end == 0:
|
||||
# if colors and text[ptr] == '<' and strlen - ptr >= 4 and text[ptr+1] == 'C' and text[ptr+3] == '>': # we have a tag
|
||||
# out += text[ptr:ptr+4]
|
||||
# ptr += 4
|
||||
# else:
|
||||
# out += text[ptr]
|
||||
# ptr += 1
|
||||
# size -= 1
|
||||
# else:
|
||||
# if colors and text[ptr] == '>' and ptr >= 4 and text[ptr-2] == 'C' and text[ptr-3] == '<': # we have a tag
|
||||
# out = text[ptr-3:ptr+1] + out
|
||||
# ptr -= 4
|
||||
# else:
|
||||
# out = text[ptr] + out
|
||||
# ptr -= 1
|
||||
# size -= 1
|
||||
|
||||
# # reached end ?
|
||||
# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1):
|
||||
# break
|
||||
|
||||
# return out
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
class prefix:
|
||||
info = (' ', 'INFO')
|
||||
debug = (' §b?§/', '§bDEBG§/')
|
||||
|
@ -659,25 +673,36 @@ _print = Output('over.core.text', stream=sys.stderr)
|
|||
|
||||
if __name__ == "__main__":
|
||||
pb = ProgressBar2(
|
||||
"§%a (§ra/§ma) [§-a>§ A] §sa [§rb/§rB] (ETA §tA)",
|
||||
"§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)",
|
||||
{
|
||||
"a": {
|
||||
"unit": "f",
|
||||
"prefix_base2": False,
|
||||
"top": 39600,
|
||||
"precision": 1
|
||||
"precision": 1,
|
||||
"min_width_raw": 10,
|
||||
"min_width_rate": 10,
|
||||
# "min_width_percent": 7, # 7 is already the default
|
||||
"min_width_time": 0
|
||||
},
|
||||
"b": {
|
||||
"unit": "o",
|
||||
"prefix_base2": True,
|
||||
"top": 3200,
|
||||
"precision": 0
|
||||
"precision": 0,
|
||||
"min_width_raw": 10,
|
||||
"min_width_rate": 10,
|
||||
"min_width_time": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
80
|
||||
)
|
||||
|
||||
pb.set("a", 12000)
|
||||
pb.set("b", 1500)
|
||||
ratios = [0, 0.1, 0.25, 0.4, 0.6, 0.8, 0.95, 1.0]
|
||||
|
||||
for ratio in ratios:
|
||||
pb.set("a", ratio * pb.channels["a"].top)
|
||||
pb.set("b", ratio * pb.channels["b"].top)
|
||||
pb.render()
|
||||
time.sleep(0.25)
|
||||
|
||||
pb.end(not True)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue