Changed init, added log_level config option

pull/27/head
aristocratos 2020-07-12 00:54:52 +02:00
parent d5e63f56c1
commit fa32357d6a
1 changed files with 306 additions and 128 deletions

422
bpytop.py
View File

@ -24,6 +24,7 @@ from select import select
from distutils.util import strtobool from distutils.util import strtobool
from string import Template from string import Template
from math import ceil from math import ceil
from random import randint
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable
errors: List[str] = [] errors: List[str] = []
@ -53,16 +54,48 @@ if errors:
#? Variables -------------------------------------------------------------------------------------> #? Variables ------------------------------------------------------------------------------------->
BANNER_SRC: Dict[str, str] = { BANNER_SRC: List[Tuple[str, str, str]] = [
"#E62525" : "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗", ("#ffa50a", "#0fd7ff", "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗"),
"#CD2121" : "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗", ("#f09800", "#00bfe6", "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗"),
"#B31D1D" : "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝", ("#db8b00", "#00a6c7", "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝"),
"#9A1919" : "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ ", ("#c27b00", "#008ca8", "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ "),
"#801414" : "██████╔╝██║ ██║ ██║ ╚██████╔╝██║", ("#a86b00", "#006e85", "██████╔╝██║ ██║ ██║ ╚██████╔╝██║"),
"#000000" : "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝", ("#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 #*?This is the template used to create the config file
DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + ''' 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, looks for a .theme file in "~/.config/bpytop/themes" and "~/.config/bpytop/user_themes", "Default" for builtin default theme
color_theme="$color_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 update_ms=$update_ms
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive" #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive",
#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly #* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly.
proc_sorting="$proc_sorting" proc_sorting="$proc_sorting"
#* Reverse sorting order, True or False #* Reverse sorting order, True or False.
proc_reversed=$proc_reversed proc_reversed=$proc_reversed
#* Show processes as a tree #* Show processes as a tree
proc_tree=$proc_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 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" 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 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" 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 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 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" 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 update_check=$update_check
#* Enable graphs with double the horizontal resolution, increases cpu usage #* Set loglevel for "~/.config/bpytop/error.log" levels are: "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG".
hires_graphs=$hires_graphs #* 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' 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!') print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!')
quit(1) quit(1)
errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}')
#! Timers, remove -----------------------------------------------------------------------> #! Timers, remove ----------------------------------------------------------------------->
class Timer: class Timer:
@ -229,7 +261,7 @@ def timerd(func):
class Config: class Config:
'''Holds all config variables and functions for loading from and saving to disk''' '''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]] = {} conf_dict: Dict[str, Union[str, int, bool]] = {}
color_theme: str = "Default" color_theme: str = "Default"
update_ms: int = 2500 update_ms: int = 2500
@ -244,7 +276,12 @@ class Config:
proc_per_core: bool = False proc_per_core: bool = False
disks_filter: str = "" disks_filter: str = ""
update_check: bool = True 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 changed: bool = False
recreate: bool = False recreate: bool = False
@ -257,10 +294,10 @@ class Config:
conf: Dict[str, Union[str, int, bool]] = self.load_config() conf: Dict[str, Union[str, int, bool]] = self.load_config()
if not "version" in conf.keys(): if not "version" in conf.keys():
self.recreate = True 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: elif conf["version"] != VERSION:
self.recreate = True 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: for key in self.keys:
if key in conf.keys() and conf[key] != "_error_": if key in conf.keys() and conf[key] != "_error_":
setattr(self, key, conf[key]) setattr(self, key, conf[key])
@ -295,19 +332,22 @@ class Config:
try: try:
new_config[key] = int(line) new_config[key] = int(line)
except ValueError: 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: if type(getattr(self, key)) == bool:
try: try:
new_config[key] = bool(strtobool(line)) new_config[key] = bool(strtobool(line))
except ValueError: 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: if type(getattr(self, key)) == str:
new_config[key] = str(line) new_config[key] = str(line)
except Exception as e: except Exception as e:
errlog.exception(str(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_" 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 return new_config
def save_config(self): def save_config(self):
@ -321,6 +361,13 @@ class Config:
try: try:
CONFIG: Config = Config(CONFIG_FILE) 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: except Exception as e:
errlog.exception(f'{e}') errlog.exception(f'{e}')
quit(1) quit(1)
@ -365,7 +412,7 @@ class Term:
cls._w, cls._h = os.get_terminal_size() cls._w, cls._h = os.get_terminal_size()
cls._resizing = False cls._resizing = False
Box.calc_sizes() Box.calc_sizes()
Box.draw_bg() Box.draw_bg(now=True if not Init.running else False)
@staticmethod @staticmethod
@ -478,6 +525,25 @@ class Key:
except: except:
pass 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 @classmethod
def input_wait(cls, sec: float = 0.0) -> bool: def input_wait(cls, sec: float = 0.0) -> bool:
'''Returns True if key is detected else waits out timer and returns False''' '''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 with Nonblocking(sys.stdin): #* Set non blocking to prevent read stall if less than 5 characters
input_key += sys.stdin.read(5) input_key += sys.stdin.read(5)
if input_key == "\033": clean_key = "escape" #* Key is escape if only containing \033 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: else:
for code in escape.keys(): #* Go trough dict of escape codes to get the cleaned key name for code in escape.keys(): #* Go trough dict of escape codes to get the cleaned key name
if input_key.lstrip("\033").startswith(code): 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 else: #* If not found in escape dict and length of key is 1, assume regular character
if len(input_key) == 1: if len(input_key) == 1:
clean_key = input_key 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: 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 = "" clean_key = ""
cls.new.set() #* Set threading event to interrupt main thread sleep cls.new.set() #* Set threading event to interrupt main thread sleep
input_key = "" input_key = ""
@ -557,7 +624,7 @@ class Key:
class Draw: class Draw:
'''Holds the draw buffer and manages IO blocking queue '''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 "+" 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 * - 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 * .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 * .clear(*names) : Clear named buffers, all if no argument
''' '''
strings: Dict[str, str] = {} strings: Dict[str, str] = {}
z_order: Dict[str, int] = {}
last_screen: str = "" last_screen: str = ""
idle = threading.Event() idle = threading.Event()
idle.set() idle.set()
@ -584,7 +652,7 @@ class Draw:
cls.idle.set() cls.idle.set()
@classmethod @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 = "" string: str = ""
if name.startswith("+"): if name.startswith("+"):
name = name.lstrip("+") name = name.lstrip("+")
@ -592,29 +660,47 @@ class Draw:
if name.endswith("!"): if name.endswith("!"):
name = name.rstrip("!") name = name.rstrip("!")
now = True 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 args: string = "".join(args)
if name not in cls.strings or not append: cls.strings[name] = "" if name not in cls.strings or not append: cls.strings[name] = ""
cls.strings[name] += string cls.strings[name] += string
if now: cls.now(string) if now: cls.now(string)
@classmethod @classmethod
def out(cls, clear = False): 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()) cls.last_screen = "".join(cls.strings.values())
if clear: cls.strings = {} if clear: cls.strings = {}
cls.now(cls.last_screen) 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 @classmethod
def clear(cls, *names): def clear(cls, *names):
if names: if names:
for name in names: for name in names:
if name in cls.strings: if name in cls.strings:
del cls.strings["name"] del cls.strings[name]
del cls.z_order[name]
else: else:
cls.strings = {} cls.strings = {}
class Color: class Color:
'''Holds representations for a 24-bit color '''Holds representations for a 24-bit color value
__init__(color, depth="fg", default=False) __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. -- 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" -- 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. __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 * 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): def __init__(self, color: str, depth: str = "fg", default: bool = False):
self.depth = depth self.depth = depth
@ -748,7 +834,9 @@ class Theme:
tdict = self._load_file(self.themes[theme]) tdict = self._load_file(self.themes[theme])
self.cached[theme] = tdict self.cached[theme] = tdict
else: else:
errlog.warning(f'No theme named "{theme}" found!')
theme = "Default" theme = "Default"
CONFIG.color_theme = theme
tdict = DEFAULT_THEME tdict = DEFAULT_THEME
self.current = theme self.current = theme
#if CONFIG.color_theme != theme: CONFIG.color_theme = theme #if CONFIG.color_theme != theme: CONFIG.color_theme = theme
@ -817,21 +905,25 @@ class Banner:
c_color: str = "" c_color: str = ""
length: int = 0 length: int = 0
if not out: 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) if len(line) > length: length = len(line)
out += [""] out_var = ""
line_color = Color.fg(color) line_color = Color.fg(color)
line_color2 = Color.fg(color2)
line_dark = Color.fg(f'#{80 - num * 6}') 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: if letter == "" and c_color != line_color:
c_color = line_color if n > 5 and n < 25: c_color = line_color2
out[num] += line_color else: c_color = line_color
out_var += c_color
elif letter == " ": elif letter == " ":
letter = f'{Mv.r(1)}' letter = f'{Mv.r(1)}'
c_color = ""
elif letter != "" and c_color != line_dark: elif letter != "" and c_color != line_dark:
c_color = line_dark c_color = line_dark
out[num] += line_dark out_var += line_dark
out[num] += letter out_var += letter
out.append(out_var)
@classmethod @classmethod
def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False): 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")}' fail: str = f'{Color.fg("#ff3050")}!{Color.fg("#cc")}'
class Graph: 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 out: str
width: int width: int
height: int height: int
@ -910,9 +1006,10 @@ class Graph:
value_width = ceil(len(data) / 2) 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 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) filler = " " * (width - value_width)
if len(data) % 2 == 1: data.insert(0, 0)
for _ in range(height): for _ in range(height):
for b in [True, False]: for b in [True, False]:
self.graphs[b].append(filler if filler else "") self.graphs[b].append(filler)
self._create(data, new=True) self._create(data, new=True)
def _create(self, data: List[int], new: bool = False): def _create(self, data: List[int], new: bool = False):
@ -970,7 +1067,7 @@ class Graphs:
net: Graph net: Graph
detailed_cpu: Graph detailed_cpu: Graph
detailed_mem: Graph detailed_mem: Graph
pid_cpu: List[Graph] = [] pid_cpu: Dict[int, Graph] = {}
class Meter: class Meter:
'''Creates a percentage meter '''Creates a percentage meter
@ -1065,8 +1162,8 @@ class SubBox:
class CpuBox(Box, SubBox): class CpuBox(Box, SubBox):
name = "cpu" name = "cpu"
x = 0 x = 1
y = 0 y = 1
height_p = 32 height_p = 32
width_p = 100 width_p = 100
@ -1102,8 +1199,8 @@ class MemBox(Box):
name = "mem" name = "mem"
height_p = 40 height_p = 40
width_p = 45 width_p = 45
x = 0 x = 1
y = 0 y = 1
divider: int = 0 divider: int = 0
mem_width: int = 0 mem_width: int = 0
disks_width: int = 0 disks_width: int = 0
@ -1131,8 +1228,8 @@ class NetBox(Box, SubBox):
name = "net" name = "net"
height_p = 28 height_p = 28
width_p = 45 width_p = 45
x = 0 x = 1
y = 0 y = 1
@classmethod @classmethod
def _calc_size(cls): def _calc_size(cls):
@ -1154,8 +1251,8 @@ class ProcBox(Box):
name = "proc" name = "proc"
height_p = 68 height_p = 68
width_p = 55 width_p = 55
x = 0 x = 1
y = 0 y = 1
detailed: bool = False detailed: bool = False
detailed_x: int = 0 detailed_x: int = 0
detailed_y: 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') #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 -------------------------------------------------------------------------------------> #? Functions ------------------------------------------------------------------------------------->
@ -1400,8 +1507,8 @@ def clean_quit(errcode: int = 0, errmsg: str = ""):
Key.stop() Key.stop()
Collector.stop() Collector.stop()
if not errcode: CONFIG.save_config() if not errcode: CONFIG.save_config()
#Draw.now(Term.clear, Term.normal_screen, Term.show_cursor) #! Enable if not testing: Draw.now(Term.clear, Term.normal_screen, Term.show_cursor) #! Enable
Draw.now(Term.show_cursor) #! Remove else: Draw.now(Term.show_cursor) #! Remove
Term.echo(True) Term.echo(True)
if errcode == 0: if errcode == 0:
errlog.info(f'Exiting. Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n') 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() CPU_NAME: str = get_cpu_name()
testing = True #! Remove testing = False #! Remove
#! For testing -------------------------------------------------------------------------------> #! For testing ------------------------------------------------------------------------------->
@ -1566,136 +1673,207 @@ def testing_keyinput():
Draw.clear() Draw.clear()
def testing_graphs(): def testing_graphs():
from random import randint
my_data = [x for x in range(0, 101)] my_data = [x for x in range(0, 101)]
my_data += [x for x in range(100, -1, -1)] 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_data100 = [100 for _ in range(200)]
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)
Draw.now(f'{Mv.to(0, 0)}{my_graph}') my_data100 = [randint(0, 100) for _ in range(Term.width * 2)]
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}') my_data2 = my_data[-90:]
Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 4, 0)}{my_graph4}') 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): my_graph = Graph(Term.width, Term.height // 2, my_colors, my_data100, invert=True)
sleep(0.1) 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) x = randint(0, 100)
Draw.now(f'{Mv.to(0, 0)}{my_graph.add(x)}') # x += 1 if t == 1 else -1
Draw.now(f'{Mv.to(Term.height // 3 + 1, 0)}{my_graph2.add(x)}') # if x == 100: t = 0
Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 2, 0)}{my_graph3.add(x)}') # if x == 0: t = 1
Draw.now(f'{Mv.to(Term.height - (Term.height // 3) + 4, 0)}{my_graph4.add(x)}') 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)) Draw.now(Mv.to(Term.height -4, 0))
# if not Key.reader.is_alive():
# clean_quit(1)
# Key.new.wait(1.0)
#! Remove ------------------------------------------------------------------------------------< #! Remove ------------------------------------------------------------------------------------<
if __name__ == "__main__": if __name__ == "__main__":
#? Init --------------------------------------------------------------------------------------> #? Init -------------------------------------------------------------------------------------->
Timer.start("Init") Timer.start("Init")
#? Temporary functions for init
def _fail(err): class Init:
Draw.now(f'{Symbol.fail}') 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}') errlog.exception(f'{err}')
sleep(2) sleep(2)
clean_quit(1, errmsg=f'Error during init! See {CONFIG_DIR}/error.log for more information.') 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 @classmethod
Draw.now(f'{Symbol.ok}\n{Mv.r(Term.width // 2 - 19)}') 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 #? Switch to alternate screen, clear screen, hide cursor and disable input echo
Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor) #! Enable if not testing: Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor) #! Enable
#Draw.now(Term.clear, Term.hide_cursor) #! Disable else: Draw.now(Term.clear, Term.hide_cursor) #! Disable
Term.echo(False) Term.echo(False)
#? Draw banner and init status #? Draw banner and init status
Draw.now(Banner.draw(Term.height // 2 - 10, center=True), "\n") if not testing: Init.success(start=True)
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)}')
#? Load theme #? 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: try:
THEME: Theme = Theme(CONFIG.color_theme) THEME: Theme = Theme(CONFIG.color_theme)
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
#? Setup boxes #? 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: try:
Box.calc_sizes() Box.calc_sizes()
Box.draw_bg(now=False) Box.draw_bg(now=False)
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
#? Setup signal handlers for SIGSTP, SIGCONT, SIGINT and SIGWINCH #? 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: try:
signal.signal(signal.SIGTSTP, now_sleeping) #* Ctrl-Z signal.signal(signal.SIGTSTP, now_sleeping) #* Ctrl-Z
signal.signal(signal.SIGCONT, now_awake) #* Resume signal.signal(signal.SIGCONT, now_awake) #* Resume
signal.signal(signal.SIGINT, quit_sigint) #* Ctrl-C signal.signal(signal.SIGINT, quit_sigint) #* Ctrl-C
signal.signal(signal.SIGWINCH, Term.refresh) #* Terminal resized signal.signal(signal.SIGWINCH, Term.refresh) #* Terminal resized
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
#? Start a separate thread for reading keyboard input #? 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: try:
Key.start() Key.start()
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
#? Start a separate thread for data collection and drawing #? 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: try:
Collector.start() Collector.start()
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
#? Collect data and draw to buffer #? 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: try:
#Collector.collect(draw_now=False) #Collector.collect(draw_now=False)
pass pass
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: 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 to screen
Draw.now("Finishing up...") Draw.buffer("+init!", f'{Mv.restore()}{Fx.trans("Finishing up... ")}{Mv.save()}')
try: try:
#Collector.collect_done.wait() #Collector.collect_done.wait()
pass pass
except Exception as e: except Exception as e:
_fail(e) Init.fail(e)
else: else:
_success() Init.success()
if not testing: sleep(1) #! Remove if if not testing: Init.done() #! Remove if
del _fail, _success
if not testing: Draw.out(clear=True) #! Remove if if not testing: Draw.out(clear=True) #! Remove if
else: Draw.clear(); Draw.now(Term.clear) #! Remove else: Draw.clear(); Draw.now(Term.clear) #! Remove
Timer.stop("Init") Timer.stop("Init")
@ -1704,12 +1882,12 @@ if __name__ == "__main__":
#! For testing -------------------------------------------------------------------------------> #! For testing ------------------------------------------------------------------------------->
if testing: if testing:
try: try:
testing_graphs() #testing_graphs()
#testing_collectors() #testing_collectors()
#testing_humanizer() #testing_humanizer()
# waitone(1) # waitone(1)
#testing_keyinput() #testing_keyinput()
#testing_banner() testing_banner()
# waitone(1) # waitone(1)
#testing_colors() #testing_colors()
# waitone(1) # waitone(1)