diff --git a/core/text.py b/core/text.py index 4ffdbee..d002690 100644 --- a/core/text.py +++ b/core/text.py @@ -41,6 +41,144 @@ def lexical_join(words, oxford=False): # -------------------------------------------------- +class _ProgressBarChannel: + def __init__(self, unit, top, prefix_base2, precision): + self.unit = unit + self.top = top + self.prefix_base2 = prefix_base2 + self.precision = precision + + self.value = 0 + self.set() + + if prefix_base2: + _print("Unit does not yet support base2 prefixes (e.g. Gi, Mi), using decadic (G, M) instead", prefix.warn) + + def set(self, value=None): + if value is not None: + 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) + +class ProgressBar2: + """ + A configurable text progressbar. + + 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) + r - displays the raw value of the channel + z - displays the maximum (total) value + s - displays the rate of change in units/s + m - displays the rate of change in units/min + h - displays the rate of change in units/h + t - displays the elapsed time in s (use with uppercase channel to get an ETA) + T - displays the elapsed time in hh:mm:ss (use with uppercase channel to get an ETA) + any other character - a bar consisting of these characters filling the line so that + 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. + + Use §§ to display a literal §. + + Examples + ======== + Format: §%a [§=a>§ A] §tA + Output: 42 % [============> ] 27 s + + Format: §%a (§ra/§ma) [§-a>§ A] §sa [§rb/§rB] (ETA §tA) + 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): + """ + Initialize the ProgressBar. + + width is the desired size of the progressbar drawing area in characters (columns) + and defaults to terminal width. + + space_before_unit enables a space character between a value and its unit. + + channels is a dict that for each channel lists its properties: + + { + "a": { + "unit": "f", + "prefix_base2": False, + "top": 39600, + "precision": 1 + }, + "b": { + "unit": "o", + "prefix_base2": True, + "top": measure_file_size_closure("/path/to/file"), + "precision": 0 + } + } + + Channel IDs (the "a", "b") are arbitrary lowercase letters ([a-z]). + + Properties "unit" and "prefix_base2" are passed to over.core.text.Unit. + "top" is the value of the channel that corresponds to 100%. Channels are + allowed to exceed this value. It can either be a number or a callable. If + it's a callable, it will be called without arguments and shall return a number. + "precision" is the displayed floating point precision. Use 0 to force an integer. + """ + + 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.width = width + + def set(self, channel_id, value): + """ + Sets the channel's value. + """ + + c = self.channels[channel_id] + c.set(value) + + def render(self, clear=True, stream=sys.stderr): + """ + Renders the progressbar into a stream. If clear is True, clears the previous content first. + """ + + width = self.width or get_terminal_size()[1] + raw = self.format + + for tag in re.findall("§.[a-zA-Z]", raw): + op = tag[1] + ch = tag[2] + inverse = ch.isupper() + + # display literal §s + raw = raw.replace("§§", "§") + + # pave over previous render and draw the new one + if clear: + stream.write("\r" + "_" * width) + + stream.write("\r" + raw) + + def end(self, finish=False): + """ + Print a newline (heh :-) + + If finish is True, set all channels to 100% and draw the progressbar first. + """ + + if finish: + for c in self.channels.values(): + c.set(c.top) + + self.render() + + print() + class ProgressBar: ''' An animated progress bar. @@ -512,3 +650,34 @@ def get_terminal_size(): return struct.unpack('HHHH', fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) except IOError: return (40, 80) + +# -------------------------------------------------- + +_print = Output('over.core.text', stream=sys.stderr) + +# -------------------------------------------------- + +if __name__ == "__main__": + pb = ProgressBar2( + "§%a (§ra/§ma) [§-a>§ A] §sa [§rb/§rB] (ETA §tA)", + { + "a": { + "unit": "f", + "prefix_base2": False, + "top": 39600, + "precision": 1 + }, + "b": { + "unit": "o", + "prefix_base2": True, + "top": 3200, + "precision": 0 + } + }, + 80 + ) + + pb.set("a", 12000) + pb.set("b", 1500) + pb.render() + pb.end(not True)