Added load_config(), save_config(), get_themes() and changed DEFAULT_CONF to a string template

pull/27/head
aristocratos 2020-07-02 02:40:53 +02:00
parent e2de8e7401
commit bea8bf084a
1 changed files with 262 additions and 159 deletions

421
bpytop
View File

@ -19,6 +19,7 @@ import os, sys, time, threading, signal, re, subprocess, logging, logging.handle
from select import select
from pathlib import Path
from distutils.util import strtobool
from string import Template
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable
errors: List[str] = []
@ -49,7 +50,7 @@ from functools import partial
print: partial = partial(print, sep="", end="", flush=True) #* Setup print function to default to empty seperator and no new line
#? Variables ------------------------------------------------------------------------------------->
#? Constants ------------------------------------------------------------------------------------->
BANNER_SRC: Dict[str, str] = {
"#E62525" : "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗",
@ -62,63 +63,65 @@ BANNER_SRC: Dict[str, str] = {
VERSION: str = "0.0.1"
DEFAULT_CONF: str = f'#? Config file for bpytop v. {VERSION}\n'
DEFAULT_CONF += '''
#* This is the template used to create a new config file
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 = "Default"
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_ms = 2500
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
proc_sorting = "cpu lazy"
proc_sorting="$proc_sorting"
#* Reverse sorting order, True or False
proc_reversed = False
proc_reversed=$proc_reversed
#* Show processes as a tree
proc_tree = False
proc_tree=$proc_tree
#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available
check_temp = True
check_temp=$check_temp
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable
draw_clock = "%X"
draw_clock="$draw_clock"
#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort
background_update = True
background_update=$background_update
#* Custom cpu model name, empty string to disable
custom_cpu_name = ""
custom_cpu_name="$custom_cpu_name"
#* Show color gradient in process list, True or False
proc_gradient = True
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
proc_per_core = False
proc_per_core=$proc_per_core
#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space
disks_filter = ""
disks_filter="$disks_filter"
#* Enable check for new version from github.com/aristocratos/bpytop at start
update_check = True
update_check=$update_check
#* Enable graphs with double the horizontal resolution, increases cpu usage
hires_graphs = False
hires_graphs=$hires_graphs
''')
'''
conf: Path = Path(f'{Path.home()}/.config/bpytop')
if not conf.is_dir():
CONFIG_DIR: str = f'{Path.home()}/.config/bpytop'
if not os.path.isdir(CONFIG_DIR):
try:
conf.mkdir(mode=0o777, parents=True)
os.makedirs(CONFIG_DIR)
os.mkdir(f'{CONFIG_DIR}/themes')
os.mkdir(f'{CONFIG_DIR}/user_themes')
except PermissionError:
print(f'ERROR!\nNo permission to write to "{conf}" directory!')
print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!')
quit(1)
CONFIG_DIR: str = str(conf)
del conf
CONFIG_FILE: str = f'{CONFIG_DIR}/bpytop.conf'
THEME_DIR: str = f'{CONFIG_DIR}/themes'
USER_THEME_DIR: str = f'{CONFIG_DIR}/user_themes'
CORES: int = psutil.cpu_count(logical=False) or 1
THREADS: int = psutil.cpu_count(logical=True) or 1
@ -386,24 +389,24 @@ class Theme:
class Draw:
'''Holds the draw buffer\n
Add to buffer: .buffer(name, *args, now=False, clear=False)\n
Print buffer: .out()\n
Add to buffer: .buffer(name, *args, append=False, now=False)\n
Print buffer: .out(clear=False)\n
'''
strings: Dict[str, str] = {}
last_screen: str = ""
@classmethod
def buffer(cls, name: str, *args, now: bool = False, clear: bool = False):
def buffer(cls, name: str, *args, append: bool = False, now: bool = False):
string: str = ""
if args: string = "".join(map(str, args))
if name not in cls.strings or clear: cls.strings[name] = ""
if name not in cls.strings or not append: cls.strings[name] = ""
cls.strings[name] += string
if now: print(string)
@classmethod
def out(cls):
def out(cls, clear = False):
cls.last_screen = "".join(cls.strings.values())
#cls.strings = {}
if clear: cls.strings = {}
print(cls.last_screen)
class Symbol:
@ -432,6 +435,101 @@ class Symbol:
4.0 : "⡇", 4.1 : "⡏", 4.2 : "⡟", 4.3 : "⡿", 4.4 : "⣿"
}
class Graphs:
'''Holds all graph objects and dicts for dynamically created graphs'''
cpu: object = None
cores: Dict[int, object] = {}
temps: Dict[int, object] = {}
net: object = None
detailed_cpu: object = None
detailed_mem: object = None
pid_cpu: Dict[int, object] = {}
class Meters:
'''Holds created meters to reuse instead of recreating meters of past values'''
cpu: Dict[int, str] = {}
mem_used: Dict[int, str] = {}
mem_available: Dict[int, str] = {}
mem_cached: Dict[int, str] = {}
mem_free: Dict[int, str] = {}
swap_used: Dict[int, str] = {}
swap_free: Dict[int, str] = {}
disks_used: Dict[int, str] = {}
disks_free: Dict[int, str] = {}
class Box:
'''Box object with all needed attributes for create_box() function'''
def __init__(self, name: str, height_p: int, width_p: int):
self.name: str = name
self.height_p: int = height_p
self.width_p: int = width_p
self.x: int = 0
self.y: int = 0
self.width: int = 0
self.height: int = 0
self.out: str = ""
if name == "proc":
self.detailed: bool = False
self.detailed_x: int = 0
self.detailed_y: int = 0
self.detailed_width: int = 0
self.detailed_height: int = 8
if name == "mem":
self.divider: int = 0
self.mem_width: int = 0
self.disks_width: int = 0
if name in ("cpu", "net"):
self.box_x: int = 0
self.box_y: int = 0
self.box_width: int = 0
self.box_height: int = 0
self.box_columns: int = 0
class Config:
'''Holds all config variables'''
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"]
conf_dict: Dict[str, Union[str, int, bool]] = {}
color_theme: str = "Default"
update_ms: int = 2500
proc_sorting: str = "cpu lazy"
proc_reversed: bool = False
proc_tree: bool = False
check_temp: bool = True
draw_clock: str = "%X"
background_update: bool = True
custom_cpu_name: str = ""
proc_gradient: bool = True
proc_per_core: bool = False
disks_filter: str = ""
update_check: bool = True
hires_graphs: bool = False
changed: bool = False
recreate: bool = False
__initialized: bool = False
def __init__(self, conf: Dict[str, Union[str, int, bool]]):
if not isinstance(conf, dict):
conf = {}
if not "version" in conf.keys() or conf["version"] != VERSION:
self.recreate = True
for key in self.keys:
if key in conf.keys():
setattr(self, key, conf[key])
else:
self.recreate = True
self.conf_dict[key] = getattr(self, key)
self.__initialized = True
def __setattr__(self, name, value):
if self.__initialized:
object.__setattr__(self, "changed", True)
object.__setattr__(self, name, value)
if name not in ["_Config__initialized", "recreate", "changed"]:
self.conf_dict[name] = value
#? Functions ------------------------------------------------------------------------------------->
@ -469,9 +567,24 @@ def get_cpu_name():
return name
def load_theme(path: str) -> Dict[str, str]:
'''Load a bashtop formatted theme file'''
def get_themes() -> Dict[str, str]:
'''Returns a dict with names and paths to all found themes'''
found: Dict[str, str] = { "Default" : "Default" }
try:
for d in (THEME_DIR, USER_THEME_DIR):
for f in os.listdir(d):
if f.endswith(".theme"):
found[f'{f[:-6] if d == THEME_DIR else f[:-6] + "*"}'] = f'{d}/{f}'
except Exception as e:
errlog.exception(str(e))
return found
def load_theme(name: str) -> Dict[str, str]:
'''Load a bashtop formatted theme file and return a dict'''
new_theme: Dict[str, str] = {}
all_themes: Dict[str, str] = get_themes()
if name == "Default" or name not in all_themes.keys(): return DEFAULT_THEME
path: str = all_themes[name]
try:
with open(path) as f:
for line in f:
@ -485,6 +598,49 @@ def load_theme(path: str) -> Dict[str, str]:
return new_theme
def load_config(path: str) -> Dict[str, Union[str, int, bool]]:
'''Load config from file, set correct types for values and return a dict'''
new_config: Dict[str,Union[str, int, bool]] = {}
if not os.path.isfile(path): return new_config
try:
with open(path, "r") as f:
for line in f:
line = line.rstrip()
if line.startswith("#? Config"):
new_config["version"] = line[line.find("v. ") + 3:]
for key in Config.keys:
if line.startswith(key):
l = line.lstrip(key + "=")
if l.startswith('"'):
l = l.lstrip('"').rstrip('"')
if type(getattr(Config, key)) == type(int()):
try:
new_config[key] = int(l)
except Exception as e:
errlog.exception(str(e))
new_config[key] = ""
if type(getattr(Config, key)) == type(bool()):
try:
new_config[key] = bool(strtobool(l))
except Exception as e:
errlog.exception(str(e))
new_config[key] = ""
if type(getattr(Config, key)) == type(str()):
new_config[key] = str(l)
except Exception as e:
errlog.exception(str(e))
return new_config
def save_config(path: str, conf: Config):
'''Save current config to config file if difference in values or version, creates a new file if not found'''
if not conf.changed and not conf.recreate: return
try:
with open(path, "w" if os.path.isfile(path) else "x") as f:
f.write(DEFAULT_CONF.substitute(conf.conf_dict))
except Exception as e:
errlog.exception(str(e))
def fg(h_r: Union[str, int], g: int = 0, b: int = 0) -> str:
"""Returns escape sequence to set foreground color, accepts either 6 digit hexadecimal: "#RRGGBB", 2 digit hexadecimal: "#FF" or decimal RGB: 0-255, 0-255, 0-255"""
color: str = ""
@ -567,6 +723,7 @@ def quit_sigint(signum, frame):
def clean_quit(errcode: int = 0):
"""Reset terminal settings, save settings to config and stop background input read before quitting"""
Key.stop()
save_config(CONFIG_FILE, config)
print(Term.clear, Term.normal_screen, Term.show_cursor)
raise SystemExit(errcode)
@ -597,7 +754,7 @@ def calc_sizes():
elif THREADS > cpu.height - 5 and cpu.width > 100: cpu.box_width = 24 * 2; cpu.box_height = round(THREADS / 2) + 4; cpu.box_columns = 2
else: cpu.box_width = 24; cpu.box_height = THREADS + 4; cpu.box_columns = 1
if Config.check_temp: cpu.box_width += 13 * cpu.box_columns
if config.check_temp: cpu.box_width += 13 * cpu.box_columns
if cpu.box_height > cpu.height - 3: cpu.box_height = cpu.height - 3
cpu.box_x = (cpu.width - 2) - cpu.box_width
cpu.box_y = cpu.y + ((cpu.height - 2) // 2) - round(cpu.box_height / 2) + 2
@ -659,37 +816,33 @@ def create_box(x: int = 0, y: int = 0, width: int = 0, height: int = 0, title: s
return out
#? Main function --------------------------------------------------------------------------------->
def draw_bg(now: bool = True):
'''Draw all boxes to buffer and print to screen if now=True'''
def main():
line: str = ""
this_key: str = ""
count: int = 0
while True:
count += 1
print(f'{Mv.to(1,1)}{Fx.b}{blue("Count:")} {count} {lime("Time:")} {time.strftime("%H:%M:%S", time.localtime())}')
print(f'{fg("#ff")} Width: {Term.width} Height: {Term.height} Resized: {Term.resized}')
while Key.list:
Key.new.clear()
this_key = Key.list.pop()
print(f'{Mv.to(2,1)}{fg("#ff9050")}{Fx.b}Last key= {Term.fg}{Fx.ub}{repr(this_key)}{" " * 40}')
if this_key == "backspace":
line = line[:-1]
elif this_key == "enter":
line += "\n"
else:
line += this_key
print(f'{Mv.to(3,1)}{fg("#90ff50")}{Fx.b}Full line= {Term.fg}{Fx.ub}{line}{Fx.bl}| {Fx.ubl}')
if this_key == "q":
clean_quit()
if this_key == "R":
raise Exception("Test ERROR")
if not Key.reader.is_alive():
clean_quit(1)
Key.new.wait(1.0)
#* Draw cpu box and cpu sub box
cpu_box = f'{create_box(box_object=cpu, line_color=theme.cpu_box, fill=True)}\
{Mv.to(cpu.y, cpu.x + 10)}{theme.cpu_box(Symbol.title_left)}{Fx.b}{theme.hi_fg("m")}{theme.title("enu")}{Fx.ub}{theme.cpu_box(Symbol.title_right)}\
{create_box(x=cpu.box_x, y=cpu.box_y, width=cpu.box_width, height=cpu.box_height, line_color=theme.div_line, title=CPU_NAME[:18 if config.check_temp else 9])}'
#* Draw mem/disk box and divider
mem_box = f'{create_box(box_object=mem, line_color=theme.mem_box, fill=True)}\
{Mv.to(mem.y, mem.divider + 2)}{theme.mem_box(Symbol.title_left)}{Fx.b}{theme.title("disks")}{Fx.ub}{theme.mem_box(Symbol.title_right)}\
{Mv.to(mem.y, mem.divider)}{theme.mem_box(Symbol.div_up)}\
{Mv.to(mem.y + mem.height, mem.divider)}{theme.mem_box(Symbol.div_down)}{theme.div_line}'
for i in range(1, mem.height):
mem_box += f'{Mv.to(mem.y + i, mem.divider)}{Symbol.v_line}'
#? Init Classes ---------------------------------------------------------------------------------->
#* Draw net box and net sub box
net_box = f'{create_box(box_object=net, line_color=theme.net_box, fill=True)}\
{create_box(x=net.box_x, y=net.box_y, width=net.box_width, height=net.box_height, line_color=theme.div_line, title="Download")}\
{Mv.to(net.box_y + net.box_height, net.box_x + 1)}{theme.div_line(Symbol.title_left)}{Fx.b}{theme.title("Upload")}{Fx.ub}{theme.div_line(Symbol.title_right)}'
#* Draw proc box
proc_box = create_box(box_object=proc, line_color=theme.proc_box, fill=True)
Draw.buffer("bg", cpu_box, mem_box, net_box, proc_box)
#? Function dependent classes -------------------------------------------------------------------->
class Banner:
out: List[str] = []
@ -722,68 +875,47 @@ class Banner:
if now: print(out)
else: return out
class Config:
'''Holds all config variables'''
check_temp: bool = True
#? Main function --------------------------------------------------------------------------------->
class Graphs:
'''Holds all graph objects and dicts for dynamically created graphs'''
cpu: object = None
cores: Dict[int, object] = {}
temps: Dict[int, object] = {}
net: object = None
detailed_cpu: object = None
detailed_mem: object = None
pid_cpu: Dict[int, object] = {}
class Meters:
'''Holds created meters to reuse instead of recreating meters of past values'''
cpu: Dict[int, str] = {}
mem_used: Dict[int, str] = {}
mem_available: Dict[int, str] = {}
mem_cached: Dict[int, str] = {}
mem_free: Dict[int, str] = {}
swap_used: Dict[int, str] = {}
swap_free: Dict[int, str] = {}
disks_used: Dict[int, str] = {}
disks_free: Dict[int, str] = {}
class Box:
'''Box object with all needed attributes for create_box() function'''
def __init__(self, name: str, height_p: int, width_p: int):
self.name: str = name
self.height_p: int = height_p
self.width_p: int = width_p
self.x: int = 0
self.y: int = 0
self.width: int = 0
self.height: int = 0
self.out: str = ""
if name == "proc":
self.detailed: bool = False
self.detailed_x: int = 0
self.detailed_y: int = 0
self.detailed_width: int = 0
self.detailed_height: int = 8
if name == "mem":
self.divider: int = 0
self.mem_width: int = 0
self.disks_width: int = 0
if name in ("cpu", "net"):
self.box_x: int = 0
self.box_y: int = 0
self.box_width: int = 0
self.box_height: int = 0
self.box_columns: int = 0
def main():
line: str = ""
this_key: str = ""
count: int = 0
while True:
count += 1
print(f'{Mv.to(1,1)}{Fx.b}{blue("Count:")} {count} {lime("Time:")} {time.strftime("%H:%M:%S", time.localtime())}')
print(f'{fg("#ff")} Width: {Term.width} Height: {Term.height} Resized: {Term.resized}')
while Key.list:
Key.new.clear()
this_key = Key.list.pop()
print(f'{Mv.to(2,1)}{fg("#ff9050")}{Fx.b}Last key= {Term.fg}{Fx.ub}{repr(this_key)}{" " * 40}')
if this_key == "backspace":
line = line[:-1]
elif this_key == "enter":
line += "\n"
else:
line += this_key
print(f'{Mv.to(3,1)}{fg("#90ff50")}{Fx.b}Full line= {Term.fg}{Fx.ub}{line}{Fx.bl}| {Fx.ubl}')
if this_key == "q":
clean_quit()
if this_key == "R":
raise Exception("Test ERROR")
if not Key.reader.is_alive():
clean_quit(1)
Key.new.wait(1.0)
#? Init variables -------------------------------------------------------------------------------->
#? Init ------------------------------------------------------------------------------------------>
#Key.start()
CPU_NAME: str = get_cpu_name()
config: Config = Config(load_config(CONFIG_FILE))
#theme = Theme(load_theme("/home/gnm/.config/bashtop/themes/monokai.theme"))
theme = Theme(DEFAULT_THEME)
config.proc_per_core = True
theme: Theme = Theme(load_theme(config.color_theme))
cpu = Box("cpu", height_p=32, width_p=100)
mem = Box("mem", height_p=40, width_p=45)
@ -798,39 +930,11 @@ orange = theme.available_end
green = theme.cpu_start
dfg = theme.main_fg
def draw_bg(now: bool = True):
'''Draw all boxes to buffer and print to screen if now=True'''
Draw.buffer("bg", clear=True)
#* Draw cpu box and cpu sub box
cpu_box = f'{create_box(box_object=cpu, line_color=theme.cpu_box, fill=True)}\
{Mv.to(cpu.y, cpu.x + 10)}{theme.cpu_box(Symbol.title_left)}{Fx.b}{theme.hi_fg("m")}{theme.title("enu")}{Fx.ub}{theme.cpu_box(Symbol.title_right)}\
{create_box(x=cpu.box_x, y=cpu.box_y, width=cpu.box_width, height=cpu.box_height, line_color=theme.div_line, title=CPU_NAME[:18 if Config.check_temp else 9])}'
#* Draw mem/disk box and divider
mem_box = f'{create_box(box_object=mem, line_color=theme.mem_box, fill=True)}\
{Mv.to(mem.y, mem.divider + 2)}{theme.mem_box(Symbol.title_left)}{Fx.b}{theme.title("disks")}{Fx.ub}{theme.mem_box(Symbol.title_right)}\
{Mv.to(mem.y, mem.divider)}{theme.mem_box(Symbol.div_up)}\
{Mv.to(mem.y + mem.height, mem.divider)}{theme.mem_box(Symbol.div_down)}{theme.div_line}'
for i in range(1, mem.height):
mem_box += f'{Mv.to(mem.y + i, mem.divider)}{Symbol.v_line}'
#* Draw net box and net sub box
net_box = f'{create_box(box_object=net, line_color=theme.net_box, fill=True)}\
{create_box(x=net.box_x, y=net.box_y, width=net.box_width, height=net.box_height, line_color=theme.div_line, title="Download")}\
{Mv.to(net.box_y + net.box_height, net.box_x + 1)}{theme.div_line(Symbol.title_left)}{Fx.b}{theme.title("Upload")}{Fx.ub}{theme.div_line(Symbol.title_right)}'
#* Draw proc box
proc_box = create_box(box_object=proc, line_color=theme.proc_box, fill=True)
Draw.buffer("bg", cpu_box, mem_box, net_box, proc_box)
def testing_colors():
for item, _ in DEFAULT_THEME.items():
print(Fx.b, getattr(theme, item)(f'{item:<20}'), Fx.ub, f'{"hex=" + getattr(theme, item).hex:<20} dec={getattr(theme, item).dec}', end="\n")
Draw.buffer("testing", Fx.b, getattr(theme, item)(f'{item:<20}'), Fx.ub, f'{"hex=" + getattr(theme, item).hex:<20} dec={getattr(theme, item).dec}\n')
Draw.out()
print()
print(theme.temp_start, "Hej!\n")
print(Term.fg, "\nHEJ\n")
@ -840,23 +944,21 @@ def testing_colors():
def testing_banner():
print(Term.normal_screen, Term.alt_screen)
Key.start()
#try:
#sad
#except Exception as e:
# errlog.exception(f'{e}')
#Key.start()
#eprint("Test")
calc_sizes()
draw_bg()
Draw.buffer("banner", Banner.draw(18, 45), clear=True)
Draw.buffer("banner", Banner.draw(18, 45))
Draw.out()
print(Mv.to(35, 1), repr(Term.fg), " ", repr(Term.bg), "\n")
print(Mv.to(35, 1))
quit()
# quit()
@ -882,7 +984,7 @@ def testing_banner():
# total_h += getattr(box, "height")
# total_w += getattr(box, "width")
# print(f'\nTotal Height={cpu.height + net.height + mem.height} Width={net.width + proc.width}')
Key.stop()
#Key.stop()
quit()
@ -892,6 +994,7 @@ def testing_banner():
try:
testing_banner()
#testing_colors()
except Exception as e:
errlog.exception(f'{e}')
clean_quit(1)