Source code for termtools.terminal

#!/usr/bin/env python
"""
This module exposes classes to control displaying text and progress bars at any place of the screen.

The two main classes are TerminalController and ProgressBarController
"""
import math
import sys
import time
import typing as t


[docs]class TerminalController(object): """A class for controlling where to print on a screen and the attributes of text to be printed.""" def __init__(self): """Constructor.""" self.x = None self.y = None pass
[docs] def clear(self): """ clears the screen :rtype: TerminalController """ return self.home(erase_screen=True)
[docs] def home(self, erase_screen=False): """ gets the cursor to the top of the screen :rtype: TerminalController """ if erase_screen: for n in range(0, 64, 1): print("\r\n") print("\033[2J", end='') print("\033[0;0H", end='') return self
[docs] def goto(self, x=0, y=0): """ goes to the specified x-y coordingates on the screen :param x: x coordinate from the left to right :param y: y coordinate from the top to bottom :rtype: TerminalController """ x = int(x) y = int(y) if x >= 255: x = 255 if y >= 255: y = 255 if x <= 0: x = 0 if y <= 0: y = 0 HORIZ = str(x) VERT = str(y) # Plot the user_string at the starting at position HORIZ, VERT... print("\033[" + VERT + ";" + HORIZ + "f", end='') return self
[docs] def printat(self, txt, x=0, y=0): """ goes to the specified x-y coordingates on the screen and prints the text. :param x: x coordinate from the left to right :param y: y coordinate from the top to bottom :rtype: TerminalController """ x = int(x) y = int(y) if x >= 255: x = 255 if y >= 255: y = 255 if x <= 0: x = 0 if y <= 0: y = 0 HORIZ = str(x) VERT = str(y) # Plot the user_string at the starting at position HORIZ, VERT... print("\033[" + VERT + ";" + HORIZ + "f" + txt) return self
[docs] def up(self, n=1): """ goes up the specified number of rows :param n: The number of rows to go up (a negative number goes down) :rtype: TerminalController """ n = int(n) if n < 0: return self.down(-n) print("\033[" + str(n) + "A", end='') return self
[docs] def down(self, n=1): """ goes down the specified number of rows :param n: The number of rows to go down (a negative number goes up) :rtype: TerminalController """ n = int(n) if n < 0: return self.up(-n) print("\033[" + str(n) + "B", end='') return self
[docs] def left(self, n=1): """ goes left the specified number of rows :param n: The number of rows to go up (a negative number goes right) :rtype: TerminalController """ n = int(n) if n < 0: return self.right(-n) print("\033[" + str(n) + "D", end='') return self
[docs] def right(self, n=1): """ goes right the specified number of rows :param n: The number of rows to go up (a negative number goes left) :rtype: TerminalController """ n = int(n) if n < 0: return self.left(-n) print("\033[" + str(n) + "C", end='') return self
[docs] def bookmark(self): """ saves current cursor position :rtype: TerminalController """ print("\033[s", end='') return self
[docs] def goto_bookmark(self): """ goes to current bookmarked position (must use bookmark() before it) :rtype: TerminalController """ print("\033[u", end='') return self
[docs] def eraseToEOL(self): """ erases all text from currnt location to the end of the line :rtype: TerminalController """ print("\033[K", end='') return self
[docs] def eraseToBOL(self): """ erases all text from currnt location to the beginning of the line :rtype: TerminalController """ print("\033[1K", end='') return self
[docs] def eraseLine(self): """ erases all text from currnt line :rtype: TerminalController """ print("\033[2K", end='') return self
[docs] def eraseUp(self): """ erases all text from currnt line to the beginning of the screen :rtype: TerminalController """ print("\033[1J", end='') return self
[docs] def eraseDown(self): """ erases all text from currnt line to the end of the screen :rtype: TerminalController """ print("\033[J", end='') return self
# TODO Add Tab control support (http://www.termsys.demon.co.uk/vtansi.htm)
[docs] def set_attributes(self, color="keep", background="keep", attrib="keep"): """ sets the text attributes to be used by new prints. a value of "keep" keeps the current set :param color: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :param background: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :param attrib: one of ['bright','dim','underscore','blink','reverse','hidden'] :rtype: TerminalController """ color = color.lower() background = background.lower() attrib = attrib.lower() att = [] clrlst = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'] attlst = ['bright', 'dim', 'underscore', 'blink', 'reverse', 'hidden'] if color == "keep": pass else: clr = clrlst.index(color) if clr > 0: clr = clr + 30 att.append(clr) if background == "keep": pass else: clr = clrlst.index(background) if clr > 0: clr = clr + 40 att.append(clr) if attrib == "keep": pass else: clr = attlst.index(background) if clr > 0: clr = clr + 1 att.append(clr) to_print = "\033[" nothingAdded = len(att) < 1 for a in att: to_print = to_print + str(a) + ';' if nothingAdded: return self to_print = to_print[:-1] to_print = to_print + "m" print(to_print, end='') return self
[docs] def color(self, color="keep"): """ sets the text foreground color :param color: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :rtype: TerminalController """ return self.set_attributes(color=color)
[docs] def set_foreground(self, color="keep"): """ sets the text foreground color :param color: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :rtype: TerminalController """ return self.color(color=color)
[docs] def background(self, background='keep'): """ sets the text background color :param background: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :rtype: TerminalController """ return self.set_attributes(background=background)
[docs] def set_background(self, background='keep'): """ sets the text background color :param background: one of ['black','red','green','yellow','blue','magenta','cyan','white'] :rtype: TerminalController """ return self.background(background=background)
[docs] def attrib(self, attrib='keep'): """ sets the text attributes :param attrib: one of ['bright','dim','underscore','blink','reverse','hidden'] :rtype: TerminalController """ return self.set_attributes(attrib=attrib)
[docs] def set_attribute(self, attrib='keep'): """ sets the text attributes :param attrib: one of ['bright','dim','underscore','blink','reverse','hidden'] :rtype: TerminalController """ return self.attrib(attrib=attrib)
[docs] def reset_attributes(self): """ resets all text attributes to their defaults :rtype: TerminalController """ print("\033[0m", end='')
[docs] def print(self, *args, **kwargs): print(*args, **kwargs) return self
[docs]def humanize_time(secs, align=False, always_show_all_units=False): ''' Prints time that is given as seconds in human readable form. Useful only for times >=1sec. :param secs float: number of seconds :param align bool, optional: whether to align outputs so that they all take the same size (not implemented) :param always_show_all_units bool, optional: Whether to always show days, hours, and minutes even when they are zeros. default False :return: str formated string with the humanized form ''' units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)] parts = [] for unit, mul in units: if secs / mul >= 1 or mul == 1 or always_show_all_units: if mul > 1: n = int(math.floor(secs / mul)) secs -= n * mul else: n = secs if secs != int(secs) else int(secs) if align: parts.append("%2d%s%s" % (n, unit, "")) else: parts.append("%2d%s%s" % (n, unit, "")) return ":".join(parts)
[docs]class ProgressBarController(object): ''' A set of progress bars. A set of progress bars with custom pre and post text. It is mostly useful when you have several running threads or suprocesses and each needs its own bar. It allows prefixes and postfixes that can be changed using the following tags: <name> bar name <remaining> remaining time to completion estimate <activity> An indicator that the process represented by the bar is active ''' _terminal = TerminalController() barNames = None current = None begTime = None activityChars = ['. ', ' . ', ' . ', ' .', ' . ', ' . ', '. '] last_name_updated = None last_i = None last_n = None running_color = 'yellow' running_background = 'black' completed_color = 'green' completed_background = 'black' over_complete_color = 'red' over_complete_background = 'black' under_complete_color = 'red' under_complete_background = 'black' def __init__(self, barNames=None, barLength=50, align_bars=True): """ Creates the set of progress bars. :param barNames: array_like: list of bar names :param barLength: int: Length of bar in spaces on screen :param align_bars: bool: whether or not to align all bars to start together and end together. Default True """ if barNames is None: barNames = [] self.barNames = barNames self.current = [0.0] * len(barNames) self.start_timing(None) self.barLength = barLength self.count = [-1] * len(barNames) self.activity = [0] * len(barNames) self.align_bars = align_bars self.last_skip_rows = None pass def _indexOfBar(self, name): try: b = self.barNames.index(name) except: return -1 return b
[docs] def add_bar(self, name: str, *, i: float = 0, n: float = 0, start_timing: bool = True): """Adds a bar Can control the name, starting progress (i) and total progress Args: name (str): name of bar Kwargs: i (int): current progress n (int): total progress start_timing (bool or None): Starts a timer for this bar, otherwise it uses the begTime member which is common to all bars Returns: self Remarks: """ self.barNames.append(name) if n > 0: self.current.append(float(i) / float(n)) else: self.current.append(None) self.count.append(n) self.activity.append(0) self.begTime.append(time.perf_counter() if start_timing else (self.begTime[0] if len(self.begTime) > 0 else 0)) return self
[docs] def remove_bar(self, name): """Removes a bar given its name Args: name (str): bar name Returns: self """ i = self._indexOfBar(name) if i >= 0: self.barNames.pop(i) self.current.pop(i) self.begTime.pop(i) self.count.pop(i) self.activity.pop(i) return self
[docs] def start_timing(self, forNames=None): """Starts timing for ETA calculation for the given bar names Args: forNames (str or None): The list of bar names. If None (Default), the beginning time of all bars is set to now Returns: self """ begTime = time.perf_counter() if forNames is None: self.begTime = [begTime] * len(self.barNames) else: for name in forNames: i = self._indexOfBar(name) if i >= 0: self.begTime[i] = begTime return self
[docs] def set_progress(self, name: str, i: int, n: int = None): """Sets progress for a specific bar, optionally setting its limit as well Args: name (str): bar name i (int): current progress n (int or None): Limit (optional) Returns: self """ b = self._indexOfBar(name) if b >= 0: if n is None: n = self.count[b] if n > 0: self.current[b] = float(i) / float(n) else: self.current[b] = 1.0 self.last_name_updated = name self.last_i = i self.last_n = n return self
[docs] def get_remaining_time(self, name): """Gets the time remaining till the end of execution (only an estimate) Args: name: bar name Returns: int: remaining time (ETA) """ b = self._indexOfBar(name) if b < 0: return -1 current = self.current[b] if current is None: return -1 if current > 1.0: return 0 remaining = (time.perf_counter() - self.begTime[b]) * (1 - current + 0.0005) / (current + 0.0005) return remaining
def _get_max_name_length(self): n = len(max(self.barNames, key=len)) if n == 0: return 10 return n def _replace_tags(self, input, name): remaining = self.get_remaining_time(name) if remaining < 0: remaining = 'ETA: unknown' else: remaining = 'ETA: {}'.format(humanize_time(remaining, align=True, always_show_all_units=True)) fmt = "{:<" + str(self._get_max_name_length()) + "}" input = input.replace('<name>', fmt.format(name)) input = input.replace('<remaining>', remaining) b = self._indexOfBar(name) if input.find('<activity>') > -1: activity = 0 if b >= 0: activity = self.activity[b] self.activity[b] = (self.activity[b] + 1) % len(self.activityChars) input = input.replace('<activity>', self.activityChars[activity]) return input
[docs] def activate(self, name: str = None): """Indicate that the given bar is active. If name is None, all bars are indicated to be active by progressing Args: name (str): bar name. If None is given then all bars are set to be active Returns: self """ if name is None: for name in self.barNames: self.activate(name) b = self._indexOfBar(name) if b >= 0: self.activity[b] = (self.activity[b] + 1) % len(self.activityChars) return self
[docs] def show(self, name=None, prefix='<name>', suffix='<remaining>', bar_length=-1, from_line_beginning=True): """Shows a specific bar just in the current place on the screen. We should have a new line before it Args: name (str or None): name of the bar prefix (str): prefix to write before the bar (see the class doc string for possible tag values) suffix (str): postfix (see prefix) bar_length (int): Bar length, if less than zero then the current length set during creation of the bar or latest setting of its progess will be used. from_line_beginning (bool): If true, a '\r' and flushing will be outputed to set the bar to the beginning of the line Returns: self """ try: if name is None: return self.show_all(barname=None, prefix=prefix, suffix=suffix, bar_length=bar_length) b = self._indexOfBar(name) if b < 0: return self if prefix is not None: prefix = self._replace_tags(prefix, name) if suffix is not None: suffix = self._replace_tags(suffix, name) if from_line_beginning: sys.stdout.write('\r') sys.stdout.flush() c = self.current[b] if c is None: c = 0.0 if c < 0: self._terminal.color(self.under_complete_color) self._terminal.background(self.under_complete_background) elif c > 1.00001: self._terminal.color(self.over_complete_color) self._terminal.background(self.over_complete_background) elif c > 0.99999999999: self._terminal.color(self.completed_color) self._terminal.background(self.completed_background) else: self._terminal.color(self.running_color) self._terminal.background(self.running_background) print_progress(self.current[b], 1, prefix=prefix, suffix=suffix , barLength=bar_length if bar_length > 0 else self.barLength) self._terminal.reset_attributes() except: pass return self
[docs] def show_all(self, barname=None, i=None, n=None, clean_screen=True, prefix='<name><activity>', suffix='<remaining>', bar_length=-1, skip_rows=0): """Shows a specific bar or all bars, possibly cleaning the screen Args: barname (str or None): name of the bar i (int or None): optional progress value n (int or None): optional maximum value for the bar clean_screen (bool): Whether or not to clean the screen before drawing. Default is true prefix (str): prefix to write before the bar (see the class doc string for possible tag values) suffix (str): postfix (see prefix) bar_length (int): Bar length, if less than zero then the current length set during creation of the bar or latest setting of its progess will be used. skip_rows (int): the number of lines to skip between bars from_line_beginning (bool): If true, a \\r and flushing will be outputed to set the bar to the beginning of the line Returns: self Remarks: """ try: if clean_screen: self._terminal.clear().home().left() if barname is not None: self.set_progress(barname, i, n) self.last_skip_rows = skip_rows for name in self.barNames: self.show(name, prefix=prefix, suffix=suffix, bar_length=bar_length) sys.stdout.write('\n') for r in range(skip_rows): sys.stdout.write('\n') sys.stdout.flush() except: pass return self
def _indicateError(self): pass
[docs] def terminal(self) -> TerminalController: """Gets the built-in `TerminalController` object Returns: TerminalController """ return self._terminal
[docs] def update(self, name: str or None = None, i: t.Optional[int] = None, n: t.Optional[int] = None , prefix: str = '<name><activity>', suffix: str = '<remaining>', bar_length: int = -1): """ Updates the progress value of a given bar and shows all the bars in their relative positions Args: name (str): bar name i (int or None): optional progress value n (int or None): optional maximum value for the bar clean_screen (bool): Whether or not to clean the screen before drawing. Default is true prefix (str): prefix to write before the bar (see the class doc string for possible tag values) suffix (str): postfix (see prefix) bar_length (int): Bar length, if less than zero then the current length set during creation of the bar or latest setting of its progess will be used. Returns: self """ try: if self.last_skip_rows is None: self.show_all(name, i, n, prefix=prefix, suffix=suffix, bar_length=bar_length) return self if name is None: if self.last_name_updated is None: self._indicateError() return self name = self.last_name_updated if i is None: i = self.last_i if n is None: n = self.last_n b = self._indexOfBar(name) if b < 0: self._indicateError() return self if i is not None and n is not None and name is not None: self.set_progress(name, i, n) loc = 1 + (self.last_skip_rows + 1) * b self._terminal.goto(0, loc) self.show(name, prefix=prefix, suffix=suffix, bar_length=bar_length) self._terminal.goto(0, 1 + (self.last_skip_rows + 1) * len(self.barNames)) except: pass return self
[docs] def show_in_position(self, name=None, prefix='<name><activity>', suffix='<remaining>', bar_length=-1): """ Shows the named bar in its appropriate position without touching anything else in the screen Args: name (str): bar name clean_screen (bool): Whether or not to clean the screen before drawing. Default is true prefix (str): prefix to write before the bar (see the class doc string for possible tag values) suffix (str): postfix (see prefix) bar_length (int): Bar length, if less than zero then the current length set during creation of the bar or latest setting of its progress will be used. Returns: self """ try: if self.last_skip_rows is None: self._indicateError() return self if name is None: if self.last_name_updated is None: self._indicateError() return self name = self.last_name_updated b = self._indexOfBar(name) if b < 0: self._indicateError() return self loc = 1 + (self.last_skip_rows + 1) * b self._terminal.goto(0, loc) self.show(name, prefix=prefix, suffix=suffix, bar_length=bar_length) self._terminal.goto(0, 1 + (self.last_skip_rows + 1) * len(self.barNames)) except: pass return self
if __name__ == '__main__': import pytest pytest.main()