diff --git a/bpytop.py b/bpytop.py index 9052df8..95774d8 100755 --- a/bpytop.py +++ b/bpytop.py @@ -24,6 +24,7 @@ from select import select from distutils.util import strtobool from string import Template from math import ceil +from random import randint from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable errors: List[str] = [] @@ -53,16 +54,48 @@ if errors: #? Variables -------------------------------------------------------------------------------------> -BANNER_SRC: Dict[str, str] = { - "#E62525" : "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗", - "#CD2121" : "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗", - "#B31D1D" : "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝", - "#9A1919" : "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ ", - "#801414" : "██████╔╝██║ ██║ ██║ ╚██████╔╝██║", - "#000000" : "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝", -} +BANNER_SRC: List[Tuple[str, str, str]] = [ + ("#ffa50a", "#0fd7ff", "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗"), + ("#f09800", "#00bfe6", "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗"), + ("#db8b00", "#00a6c7", "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝"), + ("#c27b00", "#008ca8", "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ "), + ("#a86b00", "#006e85", "██████╔╝██║ ██║ ██║ ╚██████╔╝██║"), + ("#000000", "#000000", "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝"), +] -VERSION: str = "0.0.1" +#RED +# "#E62525" +# "#CD2121" +# "#B31D1D" +# "#9A1919" +# "#801414" +# "#000000" + +#GREEN +# "#75e12d" +# "#5bbb1b" +# "#499914" +# "#3b7811" +# "#30620e" +# "#000000" + +#YELLOW +# "#fadd00" +# "#dbc200" +# "#b8a200" +# "#998700" +# "#807100" +# "#000000" + +#BLUE +# "#0084ff" +# "#016cd0" +# "#0157a7" +# "#014484" +# "#003261" +# "#000000" + +VERSION: str = "0.0.3" #*?This is the template used to create the config file DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + ''' @@ -70,45 +103,46 @@ DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + '' #* Color theme, looks for a .theme file in "~/.config/bpytop/themes" and "~/.config/bpytop/user_themes", "Default" for builtin default theme color_theme="$color_theme" -#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs +#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs. update_ms=$update_ms -#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive" -#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive", +#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly. proc_sorting="$proc_sorting" -#* Reverse sorting order, True or False +#* Reverse sorting order, True or False. proc_reversed=$proc_reversed #* Show processes as a tree proc_tree=$proc_tree -#* Check cpu temperature, needs "vcgencmd" on Raspberry Pi and "osx-cpu-temp" on MacOS X +#* Check cpu temperature, needs "vcgencmd" on Raspberry Pi and "osx-cpu-temp" on MacOS X. check_temp=$check_temp -#* Draw a clock at top of screen, formatting according to strftime, empty string to disable +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. draw_clock="$draw_clock" -#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. background_update=$background_update -#* Custom cpu model name, empty string to disable +#* Custom cpu model name, empty string to disable. custom_cpu_name="$custom_cpu_name" -#* Show color gradient in process list, True or False +#* Show color gradient in process list, True or False. proc_gradient=$proc_gradient -#* If process cpu usage should be of the core it's running on or usage of the total available cpu power +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. proc_per_core=$proc_per_core -#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space +#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space. disks_filter="$disks_filter" -#* Enable check for new version from github.com/aristocratos/bpytop at start +#* Enable check for new version from github.com/aristocratos/bpytop at start. update_check=$update_check -#* Enable graphs with double the horizontal resolution, increases cpu usage -hires_graphs=$hires_graphs +#* Set loglevel for "~/.config/bpytop/error.log" levels are: "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level="$log_level" ''') CONFIG_DIR: str = f'{os.path.expanduser("~")}/.config/bpytop' @@ -186,8 +220,6 @@ except PermissionError: print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!') quit(1) -errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}') - #! Timers, remove -----------------------------------------------------------------------> class Timer: @@ -229,7 +261,7 @@ def timerd(func): class Config: '''Holds all config variables and functions for loading from and saving to disk''' - keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_gradient", "proc_per_core", "disks_filter", "update_check", "hires_graphs"] + keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_gradient", "proc_per_core", "disks_filter", "update_check", "log_level"] conf_dict: Dict[str, Union[str, int, bool]] = {} color_theme: str = "Default" update_ms: int = 2500 @@ -244,7 +276,12 @@ class Config: proc_per_core: bool = False disks_filter: str = "" update_check: bool = True - hires_graphs: bool = False + log_level: str = "WARNING" + + warnings: List[str] = [] + + sorting_options: List[str] = ["pid", "program", "arguments", "threads", "user", "memory", "cpu lazy", "cpu responsive"] + log_levels: List[str] = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] changed: bool = False recreate: bool = False @@ -257,10 +294,10 @@ class Config: conf: Dict[str, Union[str, int, bool]] = self.load_config() if not "version" in conf.keys(): self.recreate = True - errlog.warning(f'Config file malformatted or missing, will be recreated on exit!') + self.warnings.append(f'Config file malformatted or missing, will be recreated on exit!') elif conf["version"] != VERSION: self.recreate = True - errlog.warning(f'Config file version and bpytop version missmatch, will be recreated on exit!') + self.warnings.append(f'Config file version and bpytop version missmatch, will be recreated on exit!') for key in self.keys: if key in conf.keys() and conf[key] != "_error_": setattr(self, key, conf[key]) @@ -295,19 +332,22 @@ class Config: try: new_config[key] = int(line) except ValueError: - errlog.warning(f'Config key "{key}" should be an integer!') + self.warnings.append(f'Config key "{key}" should be an integer!') if type(getattr(self, key)) == bool: try: new_config[key] = bool(strtobool(line)) except ValueError: - errlog.warning(f'Config key "{key}" can only be True or False!') + self.warnings.append(f'Config key "{key}" can only be True or False!') if type(getattr(self, key)) == str: new_config[key] = str(line) except Exception as e: errlog.exception(str(e)) - if "proc_sorting" in new_config and not new_config["proc_sorting"] in ["pid", "program", "arguments", "threads", "user", "memory", "cpu lazy", "cpu responsive"]: + if "proc_sorting" in new_config and not new_config["proc_sorting"] in self.sorting_options: new_config["proc_sorting"] = "_error_" - errlog.warning(f'Config key "proc_sorted" didn\'t get an acceptable value!') + self.warnings.append(f'Config key "proc_sorted" didn\'t get an acceptable value!') + if "log_level" in new_config and not new_config["log_level"] in self.log_levels: + new_config["log_level"] = "_error_" + self.warnings.append(f'Config key "log_level" didn\'t get an acceptable value!') return new_config def save_config(self): @@ -321,6 +361,13 @@ class Config: try: CONFIG: Config = Config(CONFIG_FILE) + errlog.setLevel(getattr(logging, CONFIG.log_level)) + errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}') + errlog.debug(f'Loglevel set to {CONFIG.log_level}') + if CONFIG.warnings: + for warning in CONFIG.warnings: + errlog.warning(warning) + CONFIG.warnings = [] except Exception as e: errlog.exception(f'{e}') quit(1) @@ -365,7 +412,7 @@ class Term: cls._w, cls._h = os.get_terminal_size() cls._resizing = False Box.calc_sizes() - Box.draw_bg() + Box.draw_bg(now=True if not Init.running else False) @staticmethod @@ -478,6 +525,25 @@ class Key: except: pass + @classmethod + def last(cls) -> str: + if cls.list: return cls.list.pop() + else: return "" + + @classmethod + def get(cls) -> str: + if cls.list: return cls.list.pop(0) + else: return "" + + @classmethod + def has_key(cls) -> bool: + if cls.list: return True + else: return False + + @classmethod + def clear(cls): + cls.list = [] + @classmethod def input_wait(cls, sec: float = 0.0) -> bool: '''Returns True if key is detected else waits out timer and returns False''' @@ -533,7 +599,7 @@ class Key: with Nonblocking(sys.stdin): #* Set non blocking to prevent read stall if less than 5 characters input_key += sys.stdin.read(5) if input_key == "\033": clean_key = "escape" #* Key is escape if only containing \033 - if input_key == "\\": clean_key = "\\" #* Clean up "\" to not return escaped + elif input_key == "\\": clean_key = "\\" #* Clean up "\" to not return escaped else: for code in escape.keys(): #* Go trough dict of escape codes to get the cleaned key name if input_key.lstrip("\033").startswith(code): @@ -542,9 +608,10 @@ class Key: else: #* If not found in escape dict and length of key is 1, assume regular character if len(input_key) == 1: clean_key = input_key - #if testing: errlog.debug(f'Input key: {repr(input_key)} Clean key: {clean_key}') #! Remove + if testing: errlog.debug(f'Input key: {repr(input_key)} Clean key: {clean_key}') #! Remove if clean_key: - cls.list.append(clean_key) #* Store keys in input queue for later processing + cls.list.append(clean_key) #* Store up to 10 keys in input queue for later processing + if len(cls.list) > 10: del cls.list[0] clean_key = "" cls.new.set() #* Set threading event to interrupt main thread sleep input_key = "" @@ -557,7 +624,7 @@ class Key: class Draw: '''Holds the draw buffer and manages IO blocking queue - * .buffer([+]name[!], *args, append=False, now=False) : Add *args to buffer + * .buffer([+]name[!], *args, append=False, now=False, z=100) : Add *args to buffer * - Adding "+" prefix to name sets append to True and appends to name's current string * - Adding "!" suffix to name sets now to True and print name's current string * .out(clear=False) : Print all strings in buffer, clear=True clear all buffers after @@ -565,6 +632,7 @@ class Draw: * .clear(*names) : Clear named buffers, all if no argument ''' strings: Dict[str, str] = {} + z_order: Dict[str, int] = {} last_screen: str = "" idle = threading.Event() idle.set() @@ -584,7 +652,7 @@ class Draw: cls.idle.set() @classmethod - def buffer(cls, name: str, *args: str, append: bool = False, now: bool = False): + def buffer(cls, name: str, *args: str, append: bool = False, now: bool = False, z: int = 100): string: str = "" if name.startswith("+"): name = name.lstrip("+") @@ -592,29 +660,47 @@ class Draw: if name.endswith("!"): name = name.rstrip("!") now = True - if name == "": name = "_null" + if not name in cls.z_order or z != 100: cls.z_order[name] = z if args: string = "".join(args) if name not in cls.strings or not append: cls.strings[name] = "" cls.strings[name] += string if now: cls.now(string) @classmethod - def out(cls, clear = False): - cls.last_screen = "".join(cls.strings.values()) - if clear: cls.strings = {} - cls.now(cls.last_screen) + def out(cls, *names: str, clear = False, no_order: bool = False): + out: str = "" + if not cls.strings: return + if no_order: + cls.last_screen = "".join(cls.strings.values()) + if clear: cls.strings = {} + cls.now(cls.last_screen) + elif names: + for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True): + if name in names: + out += cls.strings[name] + if clear: + del cls.strings[name] + del cls.z_order[name] + cls.now(out) + else: + for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True): + if name in cls.strings: out += cls.strings[name] + if clear: cls.strings = {} + cls.last_screen = out + cls.now(cls.last_screen) @classmethod def clear(cls, *names): if names: for name in names: if name in cls.strings: - del cls.strings["name"] + del cls.strings[name] + del cls.z_order[name] else: cls.strings = {} class Color: - '''Holds representations for a 24-bit color + '''Holds representations for a 24-bit color value __init__(color, depth="fg", default=False) -- color accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit hexadecimal: string "#FF" or decimal RGB "255 255 255" as a string. -- depth accepts "fg" or "bg" @@ -623,7 +709,7 @@ class Color: __iter__ returns iteration over red, green and blue in integer values of 0-255. * Values: .hexa: str | .dec: Tuple[int, int, int] | .red: int | .green: int | .blue: int | .depth: str | .escape: str ''' - hex: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool + hexa: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool def __init__(self, color: str, depth: str = "fg", default: bool = False): self.depth = depth @@ -748,7 +834,9 @@ class Theme: tdict = self._load_file(self.themes[theme]) self.cached[theme] = tdict else: + errlog.warning(f'No theme named "{theme}" found!') theme = "Default" + CONFIG.color_theme = theme tdict = DEFAULT_THEME self.current = theme #if CONFIG.color_theme != theme: CONFIG.color_theme = theme @@ -817,21 +905,25 @@ class Banner: c_color: str = "" length: int = 0 if not out: - for num, (color, line) in enumerate(BANNER_SRC.items()): + for num, (color, color2, line) in enumerate(BANNER_SRC): if len(line) > length: length = len(line) - out += [""] + out_var = "" line_color = Color.fg(color) + line_color2 = Color.fg(color2) line_dark = Color.fg(f'#{80 - num * 6}') - for letter in line: + for n, letter in enumerate(line): if letter == "█" and c_color != line_color: - c_color = line_color - out[num] += line_color + if n > 5 and n < 25: c_color = line_color2 + else: c_color = line_color + out_var += c_color elif letter == " ": letter = f'{Mv.r(1)}' + c_color = "" elif letter != "█" and c_color != line_dark: c_color = line_dark - out[num] += line_dark - out[num] += letter + out_var += line_dark + out_var += letter + out.append(out_var) @classmethod def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False): @@ -873,6 +965,10 @@ class Symbol: fail: str = f'{Color.fg("#ff3050")}!{Color.fg("#cc")}' class Graph: + '''Class for creating and adding to graphs + * __str__ : returns graph as a string + * add(value: int) : adds a value to graph and returns it as a string + ''' out: str width: int height: int @@ -910,9 +1006,10 @@ class Graph: value_width = ceil(len(data) / 2) elif value_width < width: #* If the size of given data set is smaller then width of graph, fill graph with whitespace filler = " " * (width - value_width) + if len(data) % 2 == 1: data.insert(0, 0) for _ in range(height): for b in [True, False]: - self.graphs[b].append(filler if filler else "") + self.graphs[b].append(filler) self._create(data, new=True) def _create(self, data: List[int], new: bool = False): @@ -970,7 +1067,7 @@ class Graphs: net: Graph detailed_cpu: Graph detailed_mem: Graph - pid_cpu: List[Graph] = [] + pid_cpu: Dict[int, Graph] = {} class Meter: '''Creates a percentage meter @@ -1065,8 +1162,8 @@ class SubBox: class CpuBox(Box, SubBox): name = "cpu" - x = 0 - y = 0 + x = 1 + y = 1 height_p = 32 width_p = 100 @@ -1102,8 +1199,8 @@ class MemBox(Box): name = "mem" height_p = 40 width_p = 45 - x = 0 - y = 0 + x = 1 + y = 1 divider: int = 0 mem_width: int = 0 disks_width: int = 0 @@ -1131,8 +1228,8 @@ class NetBox(Box, SubBox): name = "net" height_p = 28 width_p = 45 - x = 0 - y = 0 + x = 1 + y = 1 @classmethod def _calc_size(cls): @@ -1154,8 +1251,8 @@ class ProcBox(Box): name = "proc" height_p = 68 width_p = 55 - x = 0 - y = 0 + x = 1 + y = 1 detailed: bool = False detailed_x: int = 0 detailed_y: int = 0 @@ -1294,6 +1391,16 @@ def testing_collectors(): #Draw.now(f'Cpu usage: {CpuCollector.cpu_usage}\nCpu freq: {CpuCollector.cpu_freq}\nLoad avg: {CpuCollector.load_avg}\n') +class Menu: + '''Holds the main menu and all submenus + * uncolor(string: str) : removes all color and returns string with THEME.inactive_fg color + ''' + color_re = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m{1}") + + @classmethod + def uncolor(cls, string: str) -> str: + return f'{THEME.inactive_fg}{cls.color_re.sub("", string)}{Term.fg}' + #? Functions -------------------------------------------------------------------------------------> @@ -1400,8 +1507,8 @@ def clean_quit(errcode: int = 0, errmsg: str = ""): Key.stop() Collector.stop() if not errcode: CONFIG.save_config() - #Draw.now(Term.clear, Term.normal_screen, Term.show_cursor) #! Enable - Draw.now(Term.show_cursor) #! Remove + if not testing: Draw.now(Term.clear, Term.normal_screen, Term.show_cursor) #! Enable + else: Draw.now(Term.show_cursor) #! Remove Term.echo(True) if errcode == 0: errlog.info(f'Exiting. Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n') @@ -1457,7 +1564,7 @@ def main(): CPU_NAME: str = get_cpu_name() -testing = True #! Remove +testing = False #! Remove #! For testing -------------------------------------------------------------------------------> @@ -1566,136 +1673,207 @@ def testing_keyinput(): Draw.clear() def testing_graphs(): - - from random import randint - my_data = [x for x in range(0, 101)] my_data += [x for x in range(100, -1, -1)] - my_graph = Graph(Term.width, Term.height // 3, THEME.gradient['cpu'], my_data, invert=False) - my_graph2 = Graph(Term.width, Term.height // 3, THEME.gradient['cpu'], my_data, invert=True) - my_graph3 = Graph(Term.width, 1, THEME.proc_misc, my_data) - my_graph4 = Graph(Term.width, 2, None, my_data) + #my_data100 = [100 for _ in range(200)] - Draw.now(f'{Mv.to(0, 0)}{my_graph}') - Draw.now(f'{Mv.to(Term.height // 3 + 1, 0)}{my_graph2}') - Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 2, 0)}{my_graph3}') - Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 4, 0)}{my_graph4}') + my_data100 = [randint(0, 100) for _ in range(Term.width * 2)] + + my_data2 = my_data[-90:] + my_data3 = my_data[:86] + + my_colors = [] + for i in range(51): + for _ in range(2): my_colors.append(Color.fg(i, i, i)) + #my_colors.reverse() - for _ in range(100): - sleep(0.1) + my_graph = Graph(Term.width, Term.height // 2, my_colors, my_data100, invert=True) + my_graph2 = Graph(Term.width, Term.height // 2, my_colors, my_data100, invert=False) + # my_graph3 = Graph(100 // 3 + 10, 1, THEME.proc_misc, my_data2) + # my_graph4 = Graph(100 // 3 + 10, 1, THEME.proc_misc, my_data3) + # my_graph5 = Graph(100, Term.height // 3, THEME.inactive_fg, my_data) + + #pause = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m{1}") + #repl = "\033[0;37m" + + banner = Banner.draw(Term.height // 3 - 2, center=True) + + Draw.now(f'{Fx.ub}{Mv.to(0, 0)}{my_graph}\ + {Mv.to(Term.height // 2, 0)}{my_graph2}\ + {banner}') + + # {Mv.to(Term.height - (Term.height // 3), Term.width // 2 - 50)}{my_graph5}\ + # {Mv.to(Term.height - (Term.height // 3) - 1, Term.width // 2 - 50)}{my_graph3}\ + # {Mv.to(Term.height - (Term.height // 3) - 1, Term.width // 2 + 7)}{my_graph4}\ + + #t = 1 + x = 0 + for _ in range(200): + sleep(0.05) x = randint(0, 100) - Draw.now(f'{Mv.to(0, 0)}{my_graph.add(x)}') - Draw.now(f'{Mv.to(Term.height // 3 + 1, 0)}{my_graph2.add(x)}') - Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 2, 0)}{my_graph3.add(x)}') - Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 4, 0)}{my_graph4.add(x)}') + # x += 1 if t == 1 else -1 + # if x == 100: t = 0 + # if x == 0: t = 1 + Draw.now(f'{Fx.ub}{Mv.to(0, 0)}{my_graph.add(x)}\ + {Mv.to(Term.height // 2, 0)}{my_graph2.add(x)}\ + {banner}') + + # Draw.now(f'{Mv.to(Term.height - (Term.height // 3), Term.width // 2 - 50)}{my_graph5.add(x)}') + # Draw.now(f'{Mv.to(Term.height - (Term.height // 3) - 1, Term.width // 2 - 50)}{my_graph3.add(x)}') + # Draw.now(f'{Mv.to(Term.height - (Term.height // 3) - 1, Term.width // 2 + 7)}{my_graph4.add(x)}') Draw.now(Mv.to(Term.height -4, 0)) - # if not Key.reader.is_alive(): - # clean_quit(1) - # Key.new.wait(1.0) #! Remove ------------------------------------------------------------------------------------< if __name__ == "__main__": #? Init --------------------------------------------------------------------------------------> Timer.start("Init") - #? Temporary functions for init - def _fail(err): - Draw.now(f'{Symbol.fail}') - errlog.exception(f'{err}') - sleep(2) - clean_quit(1, errmsg=f'Error during init! See {CONFIG_DIR}/error.log for more information.') - def _success(): - if not testing: sleep(0.1) #! Remove if - Draw.now(f'{Symbol.ok}\n{Mv.r(Term.width // 2 - 19)}') + + class Init: + running: bool = True + initbg_colors: List[str] = [] + initbg_data: List[int] + initbg_up: Graph + initbg_down: Graph + + @staticmethod + def fail(err): + Draw.buffer("+init!", f'{Mv.restore()}{Symbol.fail}') + errlog.exception(f'{err}') + sleep(2) + clean_quit(1, errmsg=f'Error during init! See {CONFIG_DIR}/error.log for more information.') + + @classmethod + def success(cls, start: bool = False): + if start: + Draw.buffer("initbg", z=10) + Draw.buffer("init", z=1) + for i in range(51): + for _ in range(2): cls.initbg_colors.append(Color.fg(i, i, i)) + Draw.buffer("banner", f'{Banner.draw(Term.height // 2 - 10, center=True)}{Color.fg("#50")}\n', z=2) + for _i in range(10): + perc = f'{str((_i + 1) * 10) + "%":>5}' + Draw.buffer("+banner", f'{Mv.to(Term.height // 2 - 3 + _i, Term.width // 2 - 28)}{Fx.trans(perc)}{Symbol.v_line}') + Draw.out("banner") + Draw.buffer("+init!", f'{Color.fg("#cc")}{Fx.b}{Mv.to(Term.height // 2 - 3, Term.width // 2 - 21)}{Mv.save()}') + + if start or Term.resized: + cls.initbg_data = [randint(0, 100) for _ in range(Term.width * 2)] + cls.initbg_up = Graph(Term.width, Term.height // 2, cls.initbg_colors, cls.initbg_data, invert=True) + cls.initbg_down = Graph(Term.width, Term.height // 2, cls.initbg_colors, cls.initbg_data, invert=False) + + if start: return + + if not testing: + cls.draw_bg(10) + Draw.buffer("+init!", f'{Mv.restore()}{Symbol.ok}\n{Mv.r(Term.width // 2 - 22)}{Mv.save()}') + + @classmethod + def draw_bg(cls, times: int = 10): + for _ in range(times): + sleep(0.05) + x = randint(0, 100) + Draw.buffer("initbg", f'{Fx.ub}{Mv.to(0, 0)}{cls.initbg_up.add(x)}{Mv.to(Term.height // 2, 0)}{cls.initbg_down.add(x)}') + Draw.out("initbg", "banner", "init") + + @classmethod + def done(cls): + cls.draw_bg(20) + Draw.clear("initbg", "banner", "init") + cls.running = False + del cls.initbg_up, cls.initbg_down, cls.initbg_data, cls.initbg_colors + #? Switch to alternate screen, clear screen, hide cursor and disable input echo - Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor) #! Enable - #Draw.now(Term.clear, Term.hide_cursor) #! Disable + if not testing: Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor) #! Enable + else: Draw.now(Term.clear, Term.hide_cursor) #! Disable Term.echo(False) #? Draw banner and init status - Draw.now(Banner.draw(Term.height // 2 - 10, center=True), "\n") - Draw.now(Color.fg("#50")) - for _i in range(10): - Draw.now(f'{Mv.to(Term.height // 2 - 3 + _i, Term.width // 2 - 25)}{str((_i + 1) * 10) + "%":>5}{Symbol.v_line}') - Draw.now(f'{Color.fg("#cc")}{Fx.b}{Mv.to(Term.height // 2 - 3, Term.width // 2 - 18)}') + if not testing: Init.success(start=True) #? Load theme - Draw.now("Loading theme and creating colors... ") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Loading theme and creating colors... ")}{Mv.save()}') try: THEME: Theme = Theme(CONFIG.color_theme) except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() #? Setup boxes - Draw.now("Doing some maths and drawing... ") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Doing some maths and drawing... ")}{Mv.save()}') try: Box.calc_sizes() Box.draw_bg(now=False) except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() #? Setup signal handlers for SIGSTP, SIGCONT, SIGINT and SIGWINCH - Draw.now("Setting up signal handlers... ") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Setting up signal handlers... ")}{Mv.save()}') try: signal.signal(signal.SIGTSTP, now_sleeping) #* Ctrl-Z signal.signal(signal.SIGCONT, now_awake) #* Resume signal.signal(signal.SIGINT, quit_sigint) #* Ctrl-C signal.signal(signal.SIGWINCH, Term.refresh) #* Terminal resized except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() #? Start a separate thread for reading keyboard input - Draw.now("Starting input reader thread... ") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Starting input reader thread... ")}{Mv.save()}') try: Key.start() except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() #? Start a separate thread for data collection and drawing - Draw.now("Starting data collection and drawer thread... ") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Starting data collection and drawer thread... ")}{Mv.save()}') try: Collector.start() except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() #? Collect data and draw to buffer - Draw.now("Collecting data and drawing...") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Collecting data and drawing... ")}{Mv.save()}') try: #Collector.collect(draw_now=False) pass except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() + + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Collecting nuclear launch codes... ")}{Mv.save()}') + Init.success() + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Launching missiles... ")}{Mv.save()}') + Init.success() + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Alien invasion... ")}{Mv.save()}') + Init.success() #? Draw to screen - Draw.now("Finishing up...") + Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Finishing up... ")}{Mv.save()}') try: #Collector.collect_done.wait() pass except Exception as e: - _fail(e) + Init.fail(e) else: - _success() + Init.success() - if not testing: sleep(1) #! Remove if - del _fail, _success + if not testing: Init.done() #! Remove if if not testing: Draw.out(clear=True) #! Remove if else: Draw.clear(); Draw.now(Term.clear) #! Remove Timer.stop("Init") @@ -1704,12 +1882,12 @@ if __name__ == "__main__": #! For testing -------------------------------------------------------------------------------> if testing: try: - testing_graphs() + #testing_graphs() #testing_collectors() #testing_humanizer() # waitone(1) #testing_keyinput() - #testing_banner() + testing_banner() # waitone(1) #testing_colors() # waitone(1)