From 3a0f77ab9696f8b3e539a3d4974a8b901e7c1ee3 Mon Sep 17 00:00:00 2001 From: Martinez Date: Sun, 21 Feb 2016 13:15:29 +0100 Subject: [PATCH] initial working implementation of ProgressBar2 --- core/text.py | 289 ++++++++++++++++++++++++++++----------------------- 1 file changed, 157 insertions(+), 132 deletions(-) diff --git a/core/text.py b/core/text.py index d002690..3f80532 100644 --- a/core/text.py +++ b/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): @@ -141,6 +179,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): """ @@ -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() + + 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) - # display literal §s + # 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) @@ -178,6 +286,20 @@ class ProgressBar2: self.render() 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: ''' @@ -202,6 +324,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: @@ -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) - pb.render() + 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)