bpytop/bpytop.py

3363 lines
119 KiB
Python
Raw Normal View History

2020-07-09 00:56:03 +00:00
#!/usr/bin/env python3
# pylint: disable=not-callable, no-member
# indent = tab
# tab-size = 4
# Copyright 2020 Aristocratos (jakob@qvantnet.com)
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os, sys, threading, signal, re, subprocess, logging, logging.handlers
from time import time, sleep, strftime, localtime
from datetime import timedelta
2020-07-16 00:51:55 +00:00
from _thread import interrupt_main
2020-07-24 01:44:11 +00:00
from collections import defaultdict
2020-07-09 00:56:03 +00:00
from select import select
2020-07-27 01:13:13 +00:00
from itertools import cycle
2020-07-09 00:56:03 +00:00
from distutils.util import strtobool
from string import Template
2020-07-18 01:16:01 +00:00
from math import ceil, floor
from random import randint
from shutil import which
2020-07-27 01:13:13 +00:00
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple
2020-07-09 00:56:03 +00:00
errors: List[str] = []
try: import fcntl, termios, tty
except Exception as e: errors.append(f'{e}')
try: import psutil # type: ignore
except Exception as e: errors.append(f'{e}')
SELF_START = time()
SYSTEM: str
if "linux" in sys.platform: SYSTEM = "Linux"
elif "bsd" in sys.platform: SYSTEM = "BSD"
elif "darwin" in sys.platform: SYSTEM = "MacOS"
else: SYSTEM = "Other"
if errors:
print ("ERROR!")
for error in errors:
print(error)
if SYSTEM == "Other":
print("\nUnsupported platform!\n")
else:
print("\nInstall required modules!\n")
quit(1)
#? Variables ------------------------------------------------------------------------------------->
BANNER_SRC: List[Tuple[str, str, str]] = [
("#ffa50a", "#0fd7ff", "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗"),
("#f09800", "#00bfe6", "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗"),
("#db8b00", "#00a6c7", "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝"),
("#c27b00", "#008ca8", "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ "),
("#a86b00", "#006e85", "██████╔╝██║ ██║ ██║ ╚██████╔╝██║"),
("#000000", "#000000", "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝"),
]
2020-07-24 01:44:11 +00:00
VERSION: str = "0.6.1"
2020-07-09 00:56:03 +00:00
#*?This is the template used to create the config file
DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + '''
2020-07-24 01:44:11 +00:00
#* Color theme, looks for a .theme file in "~/.config/bpytop/themes" and "~/.config/bpytop/user_themes", "Default" for builtin default theme.
#* Themes located in "user_themes" folder should be suffixed by a star "*", i.e. color_theme="monokai*"
2020-07-09 00:56:03 +00:00
color_theme="$color_theme"
2020-07-24 01:44:11 +00:00
#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs.
2020-07-09 00:56:03 +00:00
update_ms=$update_ms
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive",
2020-07-18 01:16:01 +00:00
#* "cpu lazy" updates top process over time, "cpu responsive" updates top process directly.
2020-07-09 00:56:03 +00:00
proc_sorting="$proc_sorting"
#* Reverse sorting order, True or False.
2020-07-09 00:56:03 +00:00
proc_reversed=$proc_reversed
#* Show processes as a tree
proc_tree=$proc_tree
2020-07-24 01:44:11 +00:00
#* Use the cpu graph colors in the process list.
proc_colors=$proc_colors
#* Use a darkening gradient in the process list.
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=$proc_per_core
#* Check cpu temperature, needs "vcgencmd" on Raspberry Pi and "osx-cpu-temp" on MacOS X.
2020-07-09 00:56:03 +00:00
check_temp=$check_temp
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
2020-07-09 00:56:03 +00:00
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.
2020-07-09 00:56:03 +00:00
background_update=$background_update
#* Custom cpu model name, empty string to disable.
2020-07-09 00:56:03 +00:00
custom_cpu_name="$custom_cpu_name"
2020-07-18 01:16:01 +00:00
#* Optional filter for shown disks, should be last folder in path of a mountpoint, "root" replaces "/", separate multiple values with comma.
#* Begin line with "exclude=" to change to exclude filter, oterwise defaults to "most include" filter. Example: disks_filter="exclude=boot, home"
2020-07-09 00:56:03 +00:00
disks_filter="$disks_filter"
2020-07-18 01:16:01 +00:00
#* Show graphs instead of meters for memory values.
mem_graphs=$mem_graphs
#* If swap memory should be shown in memory box.
show_swap=$show_swap
#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
swap_disk=$swap_disk
#* If mem box should be split to also show disks info.
show_disks=$show_disks
#* Show init screen at startup, the init screen is purely cosmetical
show_init=$show_init
#* Enable check for new version from github.com/aristocratos/bpytop at start.
2020-07-09 00:56:03 +00:00
update_check=$update_check
#* Set loglevel for "~/.config/bpytop/error.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
log_level=$log_level
2020-07-09 00:56:03 +00:00
''')
CONFIG_DIR: str = f'{os.path.expanduser("~")}/.config/bpytop'
if not os.path.isdir(CONFIG_DIR):
try:
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 "{CONFIG_DIR}" directory!')
quit(1)
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
2020-07-16 00:51:55 +00:00
THREAD_ERROR: int = 0
if "--debug" in sys.argv:
DEBUG = True
else:
DEBUG = False
2020-07-09 00:56:03 +00:00
DEFAULT_THEME: Dict[str, str] = {
"main_bg" : "",
"main_fg" : "#cc",
"title" : "#ee",
"hi_fg" : "#90",
"selected_bg" : "#7e2626",
"selected_fg" : "#ee",
"inactive_fg" : "#40",
"proc_misc" : "#0de756",
"cpu_box" : "#3d7b46",
"mem_box" : "#8a882e",
"net_box" : "#423ba5",
"proc_box" : "#923535",
"div_line" : "#30",
"temp_start" : "#4897d4",
"temp_mid" : "#5474e8",
"temp_end" : "#ff40b6",
"cpu_start" : "#50f095",
"cpu_mid" : "#f2e266",
"cpu_end" : "#fa1e1e",
"free_start" : "#223014",
"free_mid" : "#b5e685",
"free_end" : "#dcff85",
"cached_start" : "#0b1a29",
"cached_mid" : "#74e6fc",
"cached_end" : "#26c5ff",
"available_start" : "#292107",
"available_mid" : "#ffd77a",
"available_end" : "#ffb814",
"used_start" : "#3b1f1c",
"used_mid" : "#d9626d",
"used_end" : "#ff4769",
"download_start" : "#231a63",
"download_mid" : "#4f43a3",
"download_end" : "#b0a9de",
"upload_start" : "#510554",
"upload_mid" : "#7d4180",
"upload_end" : "#dcafde"
}
2020-07-24 01:44:11 +00:00
MENUS: Dict[str, Dict[str, Tuple[str, ...]]] = {
"options" : {
"normal" : (
"┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐",
"│ │├─┘ │ ││ ││││└─┐",
"└─┘┴ ┴ ┴└─┘┘└┘└─┘"),
"selected" : (
"╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗",
"║ ║╠═╝ ║ ║║ ║║║║╚═╗",
"╚═╝╩ ╩ ╩╚═╝╝╚╝╚═╝") },
"help" : {
"normal" : (
"┬ ┬┌─┐┬ ┌─┐",
"├─┤├┤ │ ├─┘",
"┴ ┴└─┘┴─┘┴ "),
"selected" : (
"╦ ╦╔═╗╦ ╔═╗",
"╠═╣║╣ ║ ╠═╝",
"╩ ╩╚═╝╩═╝╩ ") },
"quit" : {
"normal" : (
"┌─┐ ┬ ┬ ┬┌┬┐",
"│─┼┐│ │ │ │ ",
"└─┘└└─┘ ┴ ┴ "),
"selected" : (
"╔═╗ ╦ ╦ ╦╔╦╗ ",
"║═╬╗║ ║ ║ ║ ",
"╚═╝╚╚═╝ ╩ ╩ ") }
}
MENU_COLORS: Dict[str, Tuple[str, ...]] = {
"normal" : ("#0fd7ff", "#00bfe6", "#00a6c7", "#008ca8"),
"selected" : ("#ffa50a", "#f09800", "#db8b00", "#c27b00")
}
2020-07-09 00:56:03 +00:00
#? Units for floating_humanizer function
UNITS: Dict[str, Tuple[str, ...]] = {
"bit" : ("bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"),
"byte" : ("Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB")
}
#? Setup error logger ---------------------------------------------------------------->
try:
errlog = logging.getLogger("ErrorLogger")
errlog.setLevel(logging.DEBUG)
eh = logging.handlers.RotatingFileHandler(f'{CONFIG_DIR}/error.log', maxBytes=1048576, backupCount=4)
eh.setLevel(logging.DEBUG)
eh.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s: %(message)s", datefmt="%d/%m/%y (%X)"))
errlog.addHandler(eh)
except PermissionError:
print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!')
quit(1)
#? Timers for testing and debugging -------------------------------------------------------------->
2020-07-09 00:56:03 +00:00
class TimeIt:
2020-07-09 00:56:03 +00:00
timers: Dict[str, float] = {}
paused: Dict[str, float] = {}
@classmethod
def start(cls, name):
cls.timers[name] = time()
@classmethod
def pause(cls, name):
if name in cls.timers:
cls.paused[name] = time() - cls.timers[name]
del cls.timers[name]
@classmethod
def stop(cls, name):
if name in cls.timers:
total: float = time() - cls.timers[name]
del cls.timers[name]
if name in cls.paused:
total += cls.paused[name]
del cls.paused[name]
errlog.debug(f'{name} completed in {total:.6f} seconds')
def timeit_decorator(func):
2020-07-09 00:56:03 +00:00
def timed(*args, **kw):
ts = time()
out = func(*args, **kw)
errlog.debug(f'{func.__name__} completed in {time() - ts:.6f} seconds')
2020-07-09 00:56:03 +00:00
return out
return timed
#? Set up config class and load config ----------------------------------------------------------->
2020-07-09 00:56:03 +00:00
class Config:
'''Holds all config variables and functions for loading from and saving to disk'''
2020-07-24 01:44:11 +00:00
keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_colors", "proc_gradient", "proc_per_core", "disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "show_init"]
2020-07-09 00:56:03 +00:00
conf_dict: Dict[str, Union[str, int, bool]] = {}
color_theme: str = "Default"
2020-07-24 01:44:11 +00:00
update_ms: int = 2000
2020-07-09 00:56:03 +00:00
proc_sorting: str = "cpu lazy"
proc_reversed: bool = False
proc_tree: bool = False
2020-07-24 01:44:11 +00:00
proc_colors: bool = True
proc_gradient: bool = True
proc_per_core: bool = False
2020-07-09 00:56:03 +00:00
check_temp: bool = True
draw_clock: str = "%X"
background_update: bool = True
custom_cpu_name: str = ""
disks_filter: str = ""
update_check: bool = True
2020-07-18 01:16:01 +00:00
mem_graphs: bool = True
show_swap: bool = True
2020-07-18 01:16:01 +00:00
swap_disk: bool = True
show_disks: bool = True
show_init: bool = True
log_level: str = "WARNING"
warnings: List[str] = []
info: List[str] = []
sorting_options: List[str] = ["pid", "program", "arguments", "threads", "user", "memory", "cpu lazy", "cpu responsive"]
log_levels: List[str] = ["ERROR", "WARNING", "INFO", "DEBUG"]
2020-07-09 00:56:03 +00:00
changed: bool = False
recreate: bool = False
config_file: str = ""
_initialized: bool = False
def __init__(self, path: str):
self.config_file = path
conf: Dict[str, Union[str, int, bool]] = self.load_config()
if not "version" in conf.keys():
self.recreate = True
self.info.append(f'Config file malformatted or missing, will be recreated on exit!')
2020-07-09 00:56:03 +00:00
elif conf["version"] != VERSION:
self.recreate = True
self.info.append(f'Config file version and bpytop version missmatch, will be recreated on exit!')
2020-07-09 00:56:03 +00:00
for key in self.keys:
if key in conf.keys() and conf[key] != "_error_":
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 ["_initialized", "recreate", "changed"]:
self.conf_dict[name] = value
def load_config(self) -> 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(self.config_file): return new_config
try:
with open(self.config_file, "r") as f:
for line in f:
line = line.strip()
if line.startswith("#? Config"):
new_config["version"] = line[line.find("v. ") + 3:]
for key in self.keys:
if line.startswith(key):
2020-07-24 01:44:11 +00:00
line = line.replace(key + "=", "")
2020-07-09 00:56:03 +00:00
if line.startswith('"'):
line = line.strip('"')
if type(getattr(self, key)) == int:
try:
new_config[key] = int(line)
except ValueError:
self.warnings.append(f'Config key "{key}" should be an integer!')
2020-07-09 00:56:03 +00:00
if type(getattr(self, key)) == bool:
try:
new_config[key] = bool(strtobool(line))
except ValueError:
self.warnings.append(f'Config key "{key}" can only be True or False!')
2020-07-09 00:56:03 +00:00
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 self.sorting_options:
2020-07-09 00:56:03 +00:00
new_config["proc_sorting"] = "_error_"
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!')
if isinstance(new_config["update_ms"], int) and new_config["update_ms"] < 100:
new_config["update_ms"] = 100
self.warnings.append(f'Config key "update_ms" can\'t be lower than 100!')
2020-07-09 00:56:03 +00:00
return new_config
def save_config(self):
'''Save current config to config file if difference in values or version, creates a new file if not found'''
if not self.changed and not self.recreate: return
try:
with open(self.config_file, "w" if os.path.isfile(self.config_file) else "x") as f:
f.write(DEFAULT_CONF.substitute(self.conf_dict))
except Exception as e:
errlog.exception(str(e))
try:
CONFIG: Config = Config(CONFIG_FILE)
if DEBUG:
errlog.setLevel(DEBUG)
else:
errlog.setLevel(getattr(logging, CONFIG.log_level))
if CONFIG.log_level == "DEBUG": DEBUG = True
errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}')
2020-07-24 01:44:11 +00:00
errlog.info(f'Loglevel set to {CONFIG.log_level}')
if CONFIG.info:
for info in CONFIG.info:
errlog.info(info)
CONFIG.info = []
if CONFIG.warnings:
for warning in CONFIG.warnings:
errlog.warning(warning)
CONFIG.warnings = []
2020-07-09 00:56:03 +00:00
except Exception as e:
errlog.exception(f'{e}')
quit(1)
#? Classes --------------------------------------------------------------------------------------->
class Term:
"""Terminal info and commands"""
width: int = os.get_terminal_size().columns #* Current terminal width in columns
height: int = os.get_terminal_size().lines #* Current terminal height in lines
2020-07-18 01:16:01 +00:00
resized: bool = False
2020-07-09 00:56:03 +00:00
_w : int = 0
_h : int = 0
2020-07-24 01:44:11 +00:00
fg: str = "" #* Default foreground color
bg: str = "" #* Default background color
hide_cursor = "\033[?25l" #* Hide terminal cursor
show_cursor = "\033[?25h" #* Show terminal cursor
alt_screen = "\033[?1049h" #* Switch to alternate screen
normal_screen = "\033[?1049l" #* Switch to normal screen
clear = "\033[2J\033[0;0f" #* Clear screen and set cursor to position 0,0
mouse_on = "\033[?1002h\033[?1015h\033[?1006h" #* Enable reporting of mouse position on click and release
mouse_off = "\033[?1002l" #* Disable mouse reporting
mouse_direct_on = "\033[?1003h" #* Enable reporting of mouse position at any movement
mouse_direct_off = "\033[?1003l" #* Disable direct mouse reporting
winch = threading.Event()
2020-07-09 00:56:03 +00:00
@classmethod
def refresh(cls, *args):
"""Update width, height and set resized flag if terminal has been resized"""
if cls.resized: cls.winch.set(); return
2020-07-09 00:56:03 +00:00
cls._w, cls._h = os.get_terminal_size()
2020-07-24 01:44:11 +00:00
if (cls._w, cls._h) == (cls.width, cls.height): return
while (cls._w, cls._h) != (cls.width, cls.height) or (cls._w < 80 or cls._h < 24):
if Init.running: Init.resized = True
2020-07-09 00:56:03 +00:00
cls.resized = True
2020-07-24 01:44:11 +00:00
Collector.collect_interrupt = True
2020-07-09 00:56:03 +00:00
cls.width, cls.height = cls._w, cls._h
Draw.now(Term.clear)
2020-07-24 01:44:11 +00:00
Draw.now(f'{create_box(cls._w // 2 - 25, cls._h // 2 - 2, 50, 3, "resizing", line_color=Colors.green, title_color=Colors.white)}',
f'{Mv.r(12)}{Colors.default}{Colors.black_bg}{Fx.b}Width : {cls._w} Height: {cls._h}{Fx.ub}{Term.bg}{Term.fg}')
2020-07-24 01:44:11 +00:00
if cls._w < 80 or cls._h < 24:
while cls._w < 80 or cls._h < 24:
Draw.now(Term.clear)
Draw.now(f'{create_box(cls._w // 2 - 25, cls._h // 2 - 2, 50, 4, "warning", line_color=Colors.red, title_color=Colors.white)}',
f'{Mv.r(12)}{Colors.default}{Colors.black_bg}{Fx.b}Width: {Colors.red if cls._w < 80 else Colors.green}{cls._w} ',
f'{Colors.default}Height: {Colors.red if cls._h < 24 else Colors.green}{cls._h}{Term.bg}{Term.fg}',
f'{Mv.to(cls._h // 2, cls._w // 2 - 23)}{Colors.default}{Colors.black_bg}Width and Height needs to be at least 80 x 24 !{Fx.ub}{Term.bg}{Term.fg}')
cls.winch.wait(0.3)
cls.winch.clear()
cls._w, cls._h = os.get_terminal_size()
else:
cls.winch.wait(0.3)
cls.winch.clear()
2020-07-09 00:56:03 +00:00
cls._w, cls._h = os.get_terminal_size()
Box.calc_sizes()
if Init.running: cls.resized = False; return
2020-07-24 01:44:11 +00:00
if Menu.active: Menu.resized = True
Box.draw_bg(now=False)
cls.resized = False
Timer.finish()
2020-07-09 00:56:03 +00:00
@staticmethod
def echo(on: bool):
"""Toggle input echo"""
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(sys.stdin.fileno())
if on:
lflag |= termios.ECHO # type: ignore
else:
lflag &= ~termios.ECHO # type: ignore
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_attr)
class Fx:
2020-07-16 00:51:55 +00:00
"""Text effects
2020-07-24 01:44:11 +00:00
* trans(string: str): Replace whitespace with escape move right to not overwrite background behind whitespace.
* uncolor(string: str) : Removes all 24-bit color and returns string ."""
2020-07-09 00:56:03 +00:00
start = "\033[" #* Escape sequence start
sep = ";" #* Escape sequence separator
end = "m" #* Escape sequence end
reset = rs = "\033[0m" #* Reset foreground/background color and text effects
bold = b = "\033[1m" #* Bold on
unbold = ub = "\033[22m" #* Bold off
dark = d = "\033[2m" #* Dark on
undark = ud = "\033[22m" #* Dark off
italic = i = "\033[3m" #* Italic on
unitalic = ui = "\033[23m" #* Italic off
underline = u = "\033[4m" #* Underline on
ununderline = uu = "\033[24m" #* Underline off
blink = bl = "\033[5m" #* Blink on
unblink = ubl = "\033[25m" #* Blink off
strike = s = "\033[9m" #* Strike / crossed-out on
unstrike = us = "\033[29m" #* Strike / crossed-out off
2020-07-24 01:44:11 +00:00
#* Precompiled regex for finding a 24-bit color escape sequence in a string
2020-07-16 00:51:55 +00:00
color_re = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m")
2020-07-09 00:56:03 +00:00
@staticmethod
def trans(string: str):
return string.replace(" ", "\033[1C")
2020-07-16 00:51:55 +00:00
@classmethod
def uncolor(cls, string: str) -> str:
2020-07-24 01:44:11 +00:00
return f'{cls.color_re.sub("", string)}'
2020-07-16 00:51:55 +00:00
2020-07-09 00:56:03 +00:00
class Raw(object):
"""Set raw input mode for device"""
def __init__(self, stream):
self.stream = stream
self.fd = self.stream.fileno()
def __enter__(self):
self.original_stty = termios.tcgetattr(self.stream)
tty.setcbreak(self.stream)
def __exit__(self, type, value, traceback):
termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty)
class Nonblocking(object):
"""Set nonblocking mode for device"""
def __init__(self, stream):
self.stream = stream
self.fd = self.stream.fileno()
def __enter__(self):
self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK)
def __exit__(self, *args):
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl)
class Mv:
"""Class with collection of cursor movement functions: .t[o](line, column) | .r[ight](columns) | .l[eft](columns) | .u[p](lines) | .d[own](lines) | .save() | .restore()"""
@staticmethod
def to(line: int, col: int) -> str:
return f'\033[{line};{col}f' #* Move cursor to line, column
@staticmethod
def right(x: int) -> str: #* Move cursor right x columns
return f'\033[{x}C'
@staticmethod
def left(x: int) -> str: #* Move cursor left x columns
return f'\033[{x}D'
@staticmethod
def up(x: int) -> str: #* Move cursor up x lines
return f'\033[{x}A'
@staticmethod
def down(x: int) -> str: #* Move cursor down x lines
return f'\033[{x}B'
2020-07-18 01:16:01 +00:00
save: str = "\033[s" #* Save cursor position
restore: str = "\033[u" #* Restore saved cursor postion
2020-07-09 00:56:03 +00:00
t = to
r = right
l = left
u = up
d = down
class Key:
2020-07-24 01:44:11 +00:00
"""Handles the threaded input reader for keypresses and mouse events"""
2020-07-09 00:56:03 +00:00
list: List[str] = []
2020-07-24 01:44:11 +00:00
mouse: Dict[str, List[List[int]]] = {}
mouse_pos: Tuple[int, int] = (0, 0)
escape: Dict[Union[str, Tuple[str, str]], str] = {
"\n" : "enter",
("\x7f", "\x08") : "backspace",
("[A", "OA") : "up",
("[B", "OB") : "down",
("[D", "OD") : "left",
("[C", "OC") : "right",
"[2~" : "insert",
"[3~" : "delete",
"[H" : "home",
"[F" : "end",
"[5~" : "page_up",
"[6~" : "page_down",
2020-07-24 01:44:11 +00:00
"\t" : "tab",
"[Z" : "shift_tab",
"OP" : "f1",
"OQ" : "f2",
"OR" : "f3",
"OS" : "f4",
"[15" : "f5",
"[17" : "f6",
"[18" : "f7",
"[19" : "f8",
"[20" : "f9",
"[21" : "f10",
"[23" : "f11",
"[24" : "f12"
}
2020-07-09 00:56:03 +00:00
new = threading.Event()
idle = threading.Event()
2020-07-24 01:44:11 +00:00
mouse_move = threading.Event()
mouse_report: bool = False
2020-07-09 00:56:03 +00:00
idle.set()
stopping: bool = False
started: bool = False
reader: threading.Thread
@classmethod
def start(cls):
cls.stopping = False
cls.reader = threading.Thread(target=cls._get_key)
cls.reader.start()
cls.started = True
@classmethod
def stop(cls):
if cls.started and cls.reader.is_alive():
cls.stopping = True
try:
cls.reader.join()
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 ""
2020-07-24 01:44:11 +00:00
@classmethod
def get_mouse(cls) -> Tuple[int, int]:
if cls.new.is_set():
cls.new.clear()
return cls.mouse_pos
@classmethod
def mouse_moved(cls) -> bool:
if cls.mouse_move.is_set():
cls.mouse_move.clear()
return True
else:
return False
@classmethod
def has_key(cls) -> bool:
if cls.list: return True
else: return False
@classmethod
def clear(cls):
cls.list = []
2020-07-09 00:56:03 +00:00
@classmethod
2020-07-24 01:44:11 +00:00
def input_wait(cls, sec: float = 0.0, mouse: bool = False) -> bool:
2020-07-09 00:56:03 +00:00
'''Returns True if key is detected else waits out timer and returns False'''
2020-07-24 01:44:11 +00:00
if mouse: Draw.now(Term.mouse_direct_on)
2020-07-09 00:56:03 +00:00
cls.new.wait(sec if sec > 0 else 0.0)
2020-07-24 01:44:11 +00:00
if mouse: Draw.now(Term.mouse_direct_off, Term.mouse_on)
2020-07-09 00:56:03 +00:00
if cls.new.is_set():
cls.new.clear()
return True
else:
return False
2020-07-24 01:44:11 +00:00
@classmethod
def break_wait(cls):
cls.list.append("_null")
cls.new.set()
sleep(0.01)
cls.new.clear()
2020-07-09 00:56:03 +00:00
@classmethod
def _get_key(cls):
2020-07-24 01:44:11 +00:00
"""Get a key or escape sequence from stdin, convert to readable format and save to keys list. Meant to be run in it's own thread."""
2020-07-09 00:56:03 +00:00
input_key: str = ""
clean_key: str = ""
try:
while not cls.stopping:
with Raw(sys.stdin):
2020-07-24 01:44:11 +00:00
if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input on stdin then restart loop to check for stop flag
2020-07-09 00:56:03 +00:00
continue
2020-07-24 01:44:11 +00:00
input_key += sys.stdin.read(1) #* Read 1 key safely with blocking on
if input_key == "\033": #* If first character is a escape sequence keep reading
cls.idle.clear() #* Report IO block in progress to prevent Draw functions from getting a IO Block error
Draw.idle.wait() #* Wait for Draw function to finish if busy
with Nonblocking(sys.stdin): #* Set non blocking to prevent read stall
input_key += sys.stdin.read(20)
2020-07-24 01:44:11 +00:00
if input_key.startswith("\033[<35;"):
_ = sys.stdin.read(1000)
cls.idle.set() #* Report IO blocking done
#errlog.debug(f'{repr(input_key)}')
if input_key == "\033": clean_key = "escape" #* Key is "escape" key if only containing \033
elif input_key.startswith(("\033[<0;", "\033[<35;", "\033[<64;", "\033[<65;")): #* Detected mouse event
try:
2020-07-24 01:44:11 +00:00
cls.mouse_pos = (int(input_key.split(";")[1]), int(input_key.split(";")[2].rstrip("mM")))
except:
pass
else:
2020-07-24 01:44:11 +00:00
if input_key.startswith("\033[<35;"): #* Detected mouse move in mouse direct mode
cls.mouse_move.set()
cls.new.set()
elif input_key.startswith("\033[<64;"): #* Detected mouse scroll up
clean_key = "mouse_scroll_up"
elif input_key.startswith("\033[<65;"): #* Detected mouse scroll down
clean_key = "mouse_scroll_down"
elif input_key.startswith("\033[<0;") and input_key.endswith("m"): #* Detected mouse click release
if Menu.active:
clean_key = "mouse_click"
else:
for key_name, positions in cls.mouse.items(): #* Check if mouse position is clickable
if list(cls.mouse_pos) in positions:
clean_key = key_name
break
else:
clean_key = "mouse_click"
elif input_key == "\\": clean_key = "\\" #* Clean up "\" to not return escaped
2020-07-09 00:56:03 +00:00
else:
2020-07-24 01:44:11 +00:00
for code in cls.escape.keys(): #* Go trough dict of escape codes to get the cleaned key name
2020-07-09 00:56:03 +00:00
if input_key.lstrip("\033").startswith(code):
clean_key = cls.escape[code]
2020-07-09 00:56:03 +00:00
break
2020-07-24 01:44:11 +00:00
else: #* If not found in escape dict and length of key is 1, assume regular character
2020-07-09 00:56:03 +00:00
if len(input_key) == 1:
clean_key = input_key
#errlog.debug(f'Input key: {repr(input_key)} Clean key: {clean_key}') #! Remove
2020-07-09 00:56:03 +00:00
if clean_key:
2020-07-24 01:44:11 +00:00
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]
2020-07-09 00:56:03 +00:00
clean_key = ""
2020-07-24 01:44:11 +00:00
cls.new.set() #* Set threading event to interrupt main thread sleep
2020-07-09 00:56:03 +00:00
input_key = ""
2020-07-24 01:44:11 +00:00
2020-07-09 00:56:03 +00:00
except Exception as e:
2020-07-16 00:51:55 +00:00
errlog.exception(f'Input thread failed with exception: {e}')
2020-07-09 00:56:03 +00:00
cls.idle.set()
cls.list.clear()
2020-07-16 00:51:55 +00:00
clean_quit(1, thread=True)
2020-07-09 00:56:03 +00:00
class Draw:
'''Holds the draw buffer and manages IO blocking queue
* .buffer([+]name[!], *args, append=False, now=False, z=100) : Add *args to buffer
2020-07-09 00:56:03 +00:00
* - 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
* .now(*args) : Prints all arguments as a string
* .clear(*names) : Clear named buffers, all if no argument
* .last_screen() : Prints all saved buffers
2020-07-09 00:56:03 +00:00
'''
strings: Dict[str, str] = {}
z_order: Dict[str, int] = {}
2020-07-24 01:44:11 +00:00
saved: Dict[str, str] = {}
save: Dict[str, bool] = {}
once: Dict[str, bool] = {}
2020-07-09 00:56:03 +00:00
idle = threading.Event()
idle.set()
@classmethod
def now(cls, *args):
2020-07-24 01:44:11 +00:00
'''Wait for input reader and self to be idle then print to screen'''
2020-07-09 00:56:03 +00:00
Key.idle.wait()
cls.idle.wait()
cls.idle.clear()
try:
print(*args, sep="", end="", flush=True)
except BlockingIOError:
pass
Key.idle.wait()
print(*args, sep="", end="", flush=True)
cls.idle.set()
@classmethod
2020-07-24 01:44:11 +00:00
def buffer(cls, name: str, *args: str, append: bool = False, now: bool = False, z: int = 100, only_save: bool = False, no_save: bool = False, once: bool = False):
2020-07-09 00:56:03 +00:00
string: str = ""
if name.startswith("+"):
name = name.lstrip("+")
append = True
if name.endswith("!"):
name = name.rstrip("!")
now = True
2020-07-24 01:44:11 +00:00
cls.save[name] = not no_save
cls.once[name] = once
if not name in cls.z_order or z != 100: cls.z_order[name] = z
2020-07-09 00:56:03 +00:00
if args: string = "".join(args)
2020-07-24 01:44:11 +00:00
if only_save:
if name not in cls.saved or not append: cls.saved[name] = ""
cls.saved[name] += string
2020-07-16 00:51:55 +00:00
else:
2020-07-24 01:44:11 +00:00
if name not in cls.strings or not append: cls.strings[name] = ""
2020-07-16 00:51:55 +00:00
cls.strings[name] += string
2020-07-24 01:44:11 +00:00
if now:
cls.out(name)
2020-07-09 00:56:03 +00:00
@classmethod
def out(cls, *names: str, clear = False):
out: str = ""
if not cls.strings: return
if names:
for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True):
2020-07-24 01:44:11 +00:00
if name in names and name in cls.strings:
out += cls.strings[name]
2020-07-24 01:44:11 +00:00
if cls.save[name]:
cls.saved[name] = cls.strings[name]
if clear or cls.once[name]:
cls.clear(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]
2020-07-24 01:44:11 +00:00
if cls.save[name]:
cls.saved[name] = out
if cls.once[name] and not clear:
cls.clear(name)
if clear:
cls.clear()
cls.now(out)
@classmethod
2020-07-24 01:44:11 +00:00
def saved_buffer(cls) -> str:
out: str = ""
for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True):
2020-07-24 01:44:11 +00:00
if name in cls.saved:
out += cls.saved[name]
return out
2020-07-09 00:56:03 +00:00
@classmethod
2020-07-24 01:44:11 +00:00
def clear(cls, *names, saved: bool = False):
2020-07-09 00:56:03 +00:00
if names:
for name in names:
if name in cls.strings:
del cls.strings[name]
2020-07-24 01:44:11 +00:00
if name in cls.save:
del cls.save[name]
if name in cls.once:
del cls.once[name]
if saved:
if name in cls.saved:
del cls.saved[name]
if name in cls.z_order:
del cls.z_order[name]
2020-07-09 00:56:03 +00:00
else:
cls.strings = {}
2020-07-24 01:44:11 +00:00
cls.save = {}
cls.once = {}
if saved:
cls.saved = {}
cls.z_order = {}
2020-07-09 00:56:03 +00:00
class Color:
'''Holds representations for a 24-bit color value
2020-07-09 00:56:03 +00:00
__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"
2020-07-24 01:44:11 +00:00
__call__(*args) joins str arguments to a string and apply color
2020-07-09 00:56:03 +00:00
__str__ returns escape sequence to set 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
'''
hexa: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool
2020-07-09 00:56:03 +00:00
def __init__(self, color: str, depth: str = "fg", default: bool = False):
self.depth = depth
self.default = default
try:
if not color:
self.dec = (-1, -1, -1)
self.hexa = ""
self.red = self.green = self.blue = -1
self.escape = "\033[49m" if depth == "bg" and default else ""
return
elif color.startswith("#"):
self.hexa = color
if len(self.hexa) == 3:
self.hexa += self.hexa[1:3] + self.hexa[1:3]
c = int(self.hexa[1:3], base=16)
self.dec = (c, c, c)
elif len(self.hexa) == 7:
self.dec = (int(self.hexa[1:3], base=16), int(self.hexa[3:5], base=16), int(self.hexa[5:7], base=16))
else:
raise ValueError(f'Incorrectly formatted hexadeciaml rgb string: {self.hexa}')
else:
c_t = tuple(map(int, color.split(" ")))
if len(c_t) == 3:
self.dec = c_t #type: ignore
else:
raise ValueError(f'RGB dec should be "0-255 0-255 0-255"')
ct = self.dec[0] + self.dec[1] + self.dec[2]
if ct > 255*3 or ct < 0:
raise ValueError(f'RGB values out of range: {color}')
except Exception as e:
errlog.exception(str(e))
self.escape = ""
return
if self.dec and not self.hexa: self.hexa = f'{hex(self.dec[0]).lstrip("0x").zfill(2)}{hex(self.dec[1]).lstrip("0x").zfill(2)}{hex(self.dec[2]).lstrip("0x").zfill(2)}'
if self.dec and self.hexa:
self.red, self.green, self.blue = self.dec
self.escape = f'\033[{38 if self.depth == "fg" else 48};2;{";".join(str(c) for c in self.dec)}m'
def __str__(self) -> str:
return self.escape
def __repr__(self) -> str:
return repr(self.escape)
def __iter__(self) -> Iterable:
for c in self.dec: yield c
def __call__(self, *args: str) -> str:
2020-07-24 01:44:11 +00:00
if len(args) < 1: return ""
2020-07-09 00:56:03 +00:00
return f'{self.escape}{"".join(args)}{getattr(Term, self.depth)}'
@staticmethod
def escape_color(hexa: str = "", r: int = 0, g: int = 0, b: int = 0, depth: str = "fg") -> str:
"""Returns escape sequence to set color
* accepts either 6 digit hexadecimal hexa="#RRGGBB", 2 digit hexadecimal: hexa="#FF"
* or decimal RGB: r=0-255, g=0-255, b=0-255
* depth="fg" or "bg"
"""
dint: int = 38 if depth == "fg" else 48
color: str = ""
if hexa:
try:
if len(hexa) == 3:
c = int(hexa[1:], base=16)
color = f'\033[{dint};2;{c};{c};{c}m'
elif len(hexa) == 7:
color = f'\033[{dint};2;{int(hexa[1:3], base=16)};{int(hexa[3:5], base=16)};{int(hexa[5:7], base=16)}m'
except ValueError as e:
errlog.exception(f'{e}')
else:
color = f'\033[{dint};2;{r};{g};{b}m'
return color
@classmethod
def fg(cls, *args) -> str:
2020-07-24 01:44:11 +00:00
if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="fg")
2020-07-09 00:56:03 +00:00
else: return cls.escape_color(hexa=args[0], depth="fg")
@classmethod
def bg(cls, *args) -> str:
2020-07-24 01:44:11 +00:00
if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="bg")
2020-07-09 00:56:03 +00:00
else: return cls.escape_color(hexa=args[0], depth="bg")
class Colors:
2020-07-24 01:44:11 +00:00
'''Standard colors for menus and dialogs'''
default = Color("#cc")
white = Color("#ff")
red = Color("#bf3636")
green = Color("#68bf36")
2020-07-24 01:44:11 +00:00
blue = Color("#0fd7ff")
yellow = Color("#db8b00")
black_bg = Color("#00", depth="bg")
2020-07-24 01:44:11 +00:00
null = Color("")
2020-07-09 00:56:03 +00:00
class Theme:
'''__init__ accepts a dict containing { "color_element" : "color" }'''
themes: Dict[str, str] = {}
cached: Dict[str, Dict[str, str]] = { "Default" : DEFAULT_THEME }
current: str = ""
main_bg = main_fg = title = hi_fg = selected_bg = selected_fg = inactive_fg = proc_misc = cpu_box = mem_box = net_box = proc_box = div_line = temp_start = temp_mid = temp_end = cpu_start = cpu_mid = cpu_end = free_start = free_mid = free_end = cached_start = cached_mid = cached_end = available_start = available_mid = available_end = used_start = used_mid = used_end = download_start = download_mid = download_end = upload_start = upload_mid = upload_end = NotImplemented
gradient: Dict[str, List[str]] = {
"temp" : [],
"cpu" : [],
"free" : [],
"cached" : [],
"available" : [],
"used" : [],
"download" : [],
2020-07-24 01:44:11 +00:00
"upload" : [],
"proc" : [],
"proc_color" : []
2020-07-09 00:56:03 +00:00
}
def __init__(self, theme: str):
self.refresh()
self._load_theme(theme)
def __call__(self, theme: str):
for k in self.gradient.keys(): self.gradient[k] = []
self._load_theme(theme)
def _load_theme(self, theme: str):
tdict: Dict[str, str]
if theme in self.cached:
tdict = self.cached[theme]
elif theme in self.themes:
tdict = self._load_file(self.themes[theme])
self.cached[theme] = tdict
else:
errlog.warning(f'No theme named "{theme}" found!')
2020-07-09 00:56:03 +00:00
theme = "Default"
CONFIG.color_theme = theme
2020-07-09 00:56:03 +00:00
tdict = DEFAULT_THEME
self.current = theme
#if CONFIG.color_theme != theme: CONFIG.color_theme = theme
#* Get key names from DEFAULT_THEME dict to not leave any color unset if missing from theme dict
for item, value in DEFAULT_THEME.items():
default = False if item not in ["main_fg", "main_bg"] else True
depth = "fg" if item not in ["main_bg", "selected_bg"] else "bg"
if item in tdict.keys():
setattr(self, item, Color(tdict[item], depth=depth, default=default))
else:
setattr(self, item, Color(value, depth=depth, default=default))
#* Create color gradients from one, two or three colors, 101 values indexed 0-100
2020-07-24 01:44:11 +00:00
self.proc_start = self.main_fg; self.proc_mid = Colors.null; self.proc_end = self.inactive_fg
self.proc_color_start = self.inactive_fg; self.proc_color_mid = Colors.null; self.proc_color_end = self.cpu_start
2020-07-09 00:56:03 +00:00
rgb: Dict[str, Tuple[int, int, int]]
colors: List[List[int]] = []
2020-07-09 00:56:03 +00:00
for name in self.gradient.keys():
rgb = { "start" : getattr(self, f'{name}_start').dec, "mid" : getattr(self, f'{name}_mid').dec, "end" : getattr(self, f'{name}_end').dec }
colors = [ list(getattr(self, f'{name}_start')) ]
2020-07-09 00:56:03 +00:00
if rgb["end"][0] >= 0:
r = 50 if rgb["mid"][0] >= 0 else 100
for first, second in ["start", "mid" if r == 50 else "end"], ["mid", "end"]:
for i in range(r):
colors += [[rgb[first][n] + i * (rgb[second][n] - rgb[first][n]) // r for n in range(3)]]
2020-07-09 00:56:03 +00:00
if r == 100:
break
self.gradient[name] += [ Color.fg(*color) for color in colors ]
2020-07-09 00:56:03 +00:00
else:
c = Color.fg(*rgb["start"])
for _ in range(100):
self.gradient[name] += [c]
#* Set terminal colors
Term.fg, Term.bg = self.main_fg, self.main_bg
Draw.now(self.main_fg, self.main_bg)
def refresh(self):
'''Sets themes dict with names and paths to all found themes'''
self.themes = { "Default" : "Default" }
try:
for d in (THEME_DIR, USER_THEME_DIR):
for f in os.listdir(d):
if f.endswith(".theme"):
self.themes[f'{f[:-6] if d == THEME_DIR else f[:-6] + "*"}'] = f'{d}/{f}'
except Exception as e:
errlog.exception(str(e))
@staticmethod
def _load_file(path: str) -> Dict[str, str]:
'''Load a bashtop formatted theme file and return a dict'''
new_theme: Dict[str, str] = {}
try:
with open(path) as f:
for line in f:
if not line.startswith("theme["): continue
key = line[6:line.find("]")]
s = line.find('"')
value = line[s + 1:line.find('"', s + 1)]
new_theme[key] = value
except Exception as e:
errlog.exception(str(e))
return new_theme
class Banner:
'''Holds the bpytop banner, .draw(line, [col=0], [center=False], [now=False])'''
out: List[str] = []
c_color: str = ""
length: int = 0
if not out:
for num, (color, color2, line) in enumerate(BANNER_SRC):
2020-07-09 00:56:03 +00:00
if len(line) > length: length = len(line)
out_var = ""
2020-07-09 00:56:03 +00:00
line_color = Color.fg(color)
line_color2 = Color.fg(color2)
2020-07-09 00:56:03 +00:00
line_dark = Color.fg(f'#{80 - num * 6}')
for n, letter in enumerate(line):
2020-07-09 00:56:03 +00:00
if letter == "" and c_color != line_color:
if n > 5 and n < 25: c_color = line_color2
else: c_color = line_color
out_var += c_color
2020-07-09 00:56:03 +00:00
elif letter == " ":
letter = f'{Mv.r(1)}'
c_color = ""
2020-07-09 00:56:03 +00:00
elif letter != "" and c_color != line_dark:
c_color = line_dark
out_var += line_dark
out_var += letter
out.append(out_var)
2020-07-09 00:56:03 +00:00
@classmethod
def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False):
out: str = ""
if center: col = Term.width // 2 - cls.length // 2
for n, o in enumerate(cls.out):
out += f'{Mv.to(line + n, col)}{o}'
out += f'{Term.fg}'
if now: Draw.out(out)
else: return out
class Symbol:
h_line: str = ""
v_line: str = ""
left_up: str = ""
right_up: str = ""
left_down: str = ""
right_down: str = ""
title_left: str = ""
title_right: str = ""
div_up: str = ""
div_down: str = ""
graph_up: Dict[float, str] = {
2020-07-18 01:16:01 +00:00
0.0 : " ", 0.1 : "", 0.2 : "", 0.3 : "", 0.4 : "",
2020-07-09 00:56:03 +00:00
1.0 : "", 1.1 : "", 1.2 : "", 1.3 : "", 1.4 : "",
2.0 : "", 2.1 : "", 2.2 : "", 2.3 : "", 2.4 : "",
3.0 : "", 3.1 : "", 3.2 : "", 3.3 : "", 3.4 : "",
4.0 : "", 4.1 : "", 4.2 : "", 4.3 : "", 4.4 : ""
}
2020-07-16 00:51:55 +00:00
graph_up_small = graph_up.copy()
graph_up_small[0.0] = "\033[1C"
2020-07-09 00:56:03 +00:00
graph_down: Dict[float, str] = {
2020-07-18 01:16:01 +00:00
0.0 : " ", 0.1 : "", 0.2 : "", 0.3 : "", 0.4 : "",
2020-07-09 00:56:03 +00:00
1.0 : "", 1.1 : "", 1.2 : "", 1.3 : "", 1.4 : "",
2.0 : "", 2.1 : "", 2.2 : "", 2.3 : "", 2.4 : "",
3.0 : "", 3.1 : "", 3.2 : "", 3.3 : "", 3.4 : "",
4.0 : "", 4.1 : "", 4.2 : "", 4.3 : "", 4.4 : ""
}
2020-07-16 00:51:55 +00:00
graph_down_small = graph_down.copy()
graph_down_small[0.0] = "\033[1C"
2020-07-09 00:56:03 +00:00
meter: str = ""
ok: str = f'{Color.fg("#30ff50")}{Color.fg("#cc")}'
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
2020-07-18 01:16:01 +00:00
* __call__ : same as add
'''
out: str
width: int
height: int
graphs: Dict[bool, List[str]]
2020-07-09 00:56:03 +00:00
colors: List[str]
invert: bool
max_value: int
2020-07-16 00:51:55 +00:00
offset: int
current: bool
last: int
2020-07-09 00:56:03 +00:00
symbol: Dict[float, str]
2020-07-16 00:51:55 +00:00
def __init__(self, width: int, height: int, color: Union[List[str], Color, None], data: List[int], invert: bool = False, max_value: int = 0, offset: int = 0):
self.graphs: Dict[bool, List[str]] = {False : [], True : []}
self.current: bool = True
self.colors: List[str] = []
2020-07-18 01:16:01 +00:00
if isinstance(color, list) and height > 1:
for i in range(1, height + 1): self.colors.insert(0, color[i * 100 // height]) #* Calculate colors of graph
if invert: self.colors.reverse()
2020-07-18 01:16:01 +00:00
elif isinstance(color, Color) and height > 1:
self.colors = [ f'{color}' for _ in range(height) ]
2020-07-18 01:16:01 +00:00
else:
if isinstance(color, list): self.colors = color
elif isinstance(color, Color): self.colors = [ f'{color}' for _ in range(101) ]
self.width = width
self.height = height
self.invert = invert
2020-07-16 00:51:55 +00:00
self.offset = offset
if not data: data = [0]
2020-07-09 00:56:03 +00:00
if max_value:
self.max_value = max_value
2020-07-16 00:51:55 +00:00
data = [ (v + offset) * 100 // (max_value + offset) if v < max_value else 100 for v in data ] #* Convert values to percentage values of max_value with max_value as ceiling
else:
self.max_value = 0
2020-07-16 00:51:55 +00:00
if self.height == 1:
self.symbol = Symbol.graph_down_small if invert else Symbol.graph_up_small
else:
self.symbol = Symbol.graph_down if invert else Symbol.graph_up
value_width: int = ceil(len(data) / 2)
filler: str = ""
if value_width > width: #* If the size of given data set is bigger then width of graph, shrink data set
data = data[-(width*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
2020-07-16 00:51:55 +00:00
filler = self.symbol[0.0] * (width - value_width)
2020-07-24 01:44:11 +00:00
if len(data) % 2: data.insert(0, 0)
for _ in range(height):
for b in [True, False]:
self.graphs[b].append(filler)
self._create(data, new=True)
def _create(self, data: List[int], new: bool = False):
h_high: int
h_low: int
value: Dict[str, int] = { "left" : 0, "right" : 0 }
val: int
side: str
#* Create the graph
for h in range(self.height):
h_high = round(100 * (self.height - h) / self.height) if self.height > 1 else 100
h_low = round(100 * (self.height - (h + 1)) / self.height) if self.height > 1 else 0
for v in range(len(data)):
if new: self.current = bool(v % 2) #* Switch between True and False graphs
if new and v == 0: self.last = 0
for val, side in [self.last, "left"], [data[v], "right"]: # type: ignore
if val >= h_high:
value[side] = 4
elif val <= h_low:
value[side] = 0
else:
if self.height == 1: value[side] = round(val * 4 / 100 + 0.5)
else: value[side] = round((val - h_low) * 4 / (h_high - h_low) + 0.1)
if new: self.last = data[v]
self.graphs[self.current][h] += self.symbol[float(value["left"] + value["right"] / 10)]
2020-07-18 01:16:01 +00:00
if data: self.last = data[-1]
self.out = ""
2020-07-18 01:16:01 +00:00
if self.height == 1:
self.out += f'{"" if not self.colors else self.colors[self.last]}{self.graphs[self.current][0]}'
elif self.height > 1:
for h in range(self.height):
if h > 0: self.out += f'{Mv.d(1)}{Mv.l(self.width)}'
self.out += f'{"" if not self.colors else self.colors[h]}{self.graphs[self.current][h if not self.invert else (self.height - 1) - h]}'
2020-07-16 00:51:55 +00:00
if self.colors: self.out += f'{Term.fg}'
2020-07-09 00:56:03 +00:00
def __call__(self, value: Union[int, None] = None) -> str:
if not isinstance(value, int): return self.out
2020-07-09 00:56:03 +00:00
self.current = not self.current
2020-07-16 00:51:55 +00:00
if self.height == 1:
if self.graphs[self.current][0].startswith(self.symbol[0.0]):
self.graphs[self.current][0] = self.graphs[self.current][0].replace(self.symbol[0.0], "", 1)
else:
self.graphs[self.current][0] = self.graphs[self.current][0][1:]
else:
for n in range(self.height):
self.graphs[self.current][n] = self.graphs[self.current][n][1:]
if self.max_value: value = (value + self.offset) * 100 // (self.max_value + self.offset) if value < self.max_value else 100
self._create([value])
return self.out
2020-07-09 00:56:03 +00:00
def add(self, value: Union[int, None] = None) -> str:
return self.__call__(value)
2020-07-18 01:16:01 +00:00
2020-07-09 00:56:03 +00:00
def __str__(self):
return self.out
def __repr__(self):
return repr(self.out)
2020-07-09 00:56:03 +00:00
class Graphs:
'''Holds all graphs and lists of graphs for dynamically created graphs'''
2020-07-16 00:51:55 +00:00
cpu: Dict[str, Graph] = {}
cores: List[Graph] = [NotImplemented] * THREADS
temps: List[Graph] = [NotImplemented] * (THREADS + 1)
net: Dict[str, Graph] = {}
2020-07-09 00:56:03 +00:00
detailed_cpu: Graph
detailed_mem: Graph
pid_cpu: Dict[int, Graph] = {}
2020-07-09 00:56:03 +00:00
class Meter:
'''Creates a percentage meter
__init__(value, width, theme, gradient_name) to create new meter
__call__(value) to set value and return meter as a string
__str__ returns last set meter as a string
'''
out: str
2020-07-09 00:56:03 +00:00
color_gradient: List[str]
color_inactive: Color
gradient_name: str
width: int
saved: Dict[int, str]
2020-07-09 00:56:03 +00:00
def __init__(self, value: int, width: int, gradient_name: str):
self.gradient_name = gradient_name
2020-07-09 00:56:03 +00:00
self.color_gradient = THEME.gradient[gradient_name]
self.color_inactive = THEME.inactive_fg
self.width = width
self.saved = {}
2020-07-09 00:56:03 +00:00
self.out = self._create(value)
def __call__(self, value: Union[int, None]) -> str:
if not isinstance(value, int): return self.out
if value > 100: value = 100
elif value < 0: value = 100
if value in self.saved:
2020-07-09 00:56:03 +00:00
self.out = self.saved[value]
else:
self.out = self._create(value)
return self.out
def __str__(self) -> str:
2020-07-09 00:56:03 +00:00
return self.out
def __repr__(self):
return repr(self.out)
def _create(self, value: int) -> str:
2020-07-09 00:56:03 +00:00
if value > 100: value = 100
elif value < 0: value = 100
out: str = ""
for i in range(1, self.width + 1):
if value >= round(i * 100 / self.width):
out += f'{self.color_gradient[round(i * 100 / self.width)]}{Symbol.meter}'
else:
out += self.color_inactive(Symbol.meter * (self.width + 1 - i))
break
else:
out += f'{Term.fg}'
if not value in self.saved:
self.saved[value] = out
2020-07-09 00:56:03 +00:00
return out
class Meters:
cpu: Meter
2020-07-18 01:16:01 +00:00
mem: Dict[str, Union[Meter, Graph]] = {}
swap: Dict[str, Union[Meter, Graph]] = {}
disks_used: Dict[str, Meter] = {}
disks_free: Dict[str, Meter] = {}
2020-07-09 00:56:03 +00:00
class Box:
'''Box class with all needed attributes for create_box() function'''
name: str
height_p: int
width_p: int
x: int
y: int
width: int
height: int
out: str
bg: str
_b_cpu_h: int
_b_mem_h: int
redraw_all: bool
2020-07-16 00:51:55 +00:00
buffers: List[str] = []
2020-07-18 01:16:01 +00:00
resized: bool = False
2020-07-09 00:56:03 +00:00
@classmethod
def calc_sizes(cls):
'''Calculate sizes of boxes'''
for sub in cls.__subclasses__():
sub._calc_size() # type: ignore
2020-07-16 00:51:55 +00:00
sub.resized = True # type: ignore
2020-07-09 00:56:03 +00:00
2020-07-18 01:16:01 +00:00
@classmethod
def draw_update_ms(cls, now: bool = True):
update_string: str = f'{CONFIG.update_ms}ms'
xpos: int = CpuBox.x + CpuBox.width - len(update_string) - 14
2020-07-24 01:44:11 +00:00
Key.mouse["+"] = [[xpos + 7 + i, CpuBox.y] for i in range(3)]
Key.mouse["-"] = [[CpuBox.x + CpuBox.width - 4 + i, CpuBox.y] for i in range(3)]
Draw.buffer("update_ms!" if now and not Menu.active else "update_ms",
f'{Mv.to(CpuBox.y, xpos)}{THEME.cpu_box(Symbol.h_line * 7, Symbol.title_left)}{Fx.b}{THEME.hi_fg("+")} ',
2020-07-24 01:44:11 +00:00
f'{THEME.title(update_string)} {THEME.hi_fg("-")}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}', only_save=Menu.active, once=True)
2020-07-18 01:16:01 +00:00
if now and not Menu.active: Draw.clear("update_ms")
2020-07-09 00:56:03 +00:00
@classmethod
def draw_bg(cls, now: bool = True):
'''Draw all boxes outlines and titles'''
2020-07-24 01:44:11 +00:00
Draw.buffer("bg", "".join(sub._draw_bg() for sub in cls.__subclasses__()), now=now, z=1000, only_save=Menu.active, once=True) # type: ignore
2020-07-18 01:16:01 +00:00
cls.draw_update_ms(now=now)
2020-07-09 00:56:03 +00:00
class SubBox:
box_x: int = 0
box_y: int = 0
box_width: int = 0
box_height: int = 0
box_columns: int = 0
2020-07-16 00:51:55 +00:00
column_size: int = 0
2020-07-09 00:56:03 +00:00
class CpuBox(Box, SubBox):
name = "cpu"
x = 1
y = 1
2020-07-09 00:56:03 +00:00
height_p = 32
width_p = 100
2020-07-16 00:51:55 +00:00
resized: bool = True
2020-07-24 01:44:11 +00:00
redraw: bool = False
2020-07-16 00:51:55 +00:00
buffer: str = "cpu"
Box.buffers.append(buffer)
2020-07-09 00:56:03 +00:00
@classmethod
def _calc_size(cls):
cls.width = round(Term.width * cls.width_p / 100)
cls.height = round(Term.height * cls.height_p / 100)
Box._b_cpu_h = cls.height
2020-07-16 00:51:55 +00:00
#THREADS = 64
cls.box_columns = ceil((THREADS + 1) / (cls.height - 5))
if cls.box_columns * (24 + 13 if CONFIG.check_temp else 24) < cls.width - (cls.width // 4): cls.column_size = 2
elif cls.box_columns * (19 + 6 if CONFIG.check_temp else 19) < cls.width - (cls.width // 4): cls.column_size = 1
2020-07-18 01:16:01 +00:00
elif cls.box_columns * (10 + 6 if CONFIG.check_temp else 10) < cls.width - (cls.width // 4): cls.column_size = 0
else: cls.box_columns = (cls.width - cls.width // 4) // (10 + 6 if CONFIG.check_temp else 10); cls.column_size = 0
2020-07-09 00:56:03 +00:00
2020-07-18 01:16:01 +00:00
if cls.column_size == 2: cls.box_width = (24 + 13 if CONFIG.check_temp else 24) * cls.box_columns - ((cls.box_columns - 1) * 1)
elif cls.column_size == 1: cls.box_width = (19 + 6 if CONFIG.check_temp else 19) * cls.box_columns - ((cls.box_columns - 1) * 1)
else: cls.box_width = (11 + 6 if CONFIG.check_temp else 11) * cls.box_columns + 1
2020-07-16 00:51:55 +00:00
cls.box_height = ceil(THREADS / cls.box_columns) + 4
2020-07-09 00:56:03 +00:00
2020-07-16 00:51:55 +00:00
if cls.box_height > cls.height - 2: cls.box_height = cls.height - 2
cls.box_x = (cls.width - 1) - cls.box_width
cls.box_y = cls.y + ceil((cls.height - 2) / 2) - ceil(cls.box_height / 2) + 1
2020-07-09 00:56:03 +00:00
@classmethod
def _draw_bg(cls) -> str:
2020-07-24 01:44:11 +00:00
Key.mouse["m"] = [[cls.x + 10 + i, cls.y] for i in range(6)]
2020-07-16 00:51:55 +00:00
return (f'{create_box(box=cls, line_color=THEME.cpu_box)}'
f'{Mv.to(cls.y, cls.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)}'
f'{create_box(x=cls.box_x, y=cls.box_y, width=cls.box_width, height=cls.box_height, line_color=THEME.div_line, fill=False, title=CPU_NAME[:18 if CONFIG.check_temp else 9])}')
2020-07-09 00:56:03 +00:00
@classmethod
2020-07-18 01:16:01 +00:00
def _draw_fg(cls):
cpu = CpuCollector
2020-07-24 01:44:11 +00:00
if cpu.redraw: cls.redraw = True
2020-07-16 00:51:55 +00:00
out: str = ""
lavg: str = ""
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
bx, by, bw, bh = cls.box_x + 1, cls.box_y + 1, cls.box_width - 2, cls.box_height - 2
hh: int = ceil(h / 2)
2020-07-24 01:44:11 +00:00
if cls.resized or cls.redraw:
2020-07-16 00:51:55 +00:00
Graphs.cpu["up"] = Graph(w - bw - 3, hh, THEME.gradient["cpu"], cpu.cpu_usage[0])
Graphs.cpu["down"] = Graph(w - bw - 3, h - hh, THEME.gradient["cpu"], cpu.cpu_usage[0], invert=True)
2020-07-18 01:16:01 +00:00
Meters.cpu = Meter(cpu.cpu_usage[0][-1], (bw - 9 - 13 if CONFIG.check_temp else bw - 9), "cpu")
2020-07-16 00:51:55 +00:00
if cls.column_size > 0:
for n in range(THREADS):
Graphs.cores[n] = Graph(5 * cls.column_size, 1, None, cpu.cpu_usage[n + 1])
if CONFIG.check_temp:
Graphs.temps[0] = Graph(5, 1, None, cpu.cpu_temp[0], max_value=cpu.cpu_temp_crit, offset=-23)
if cls.column_size > 1:
for n in range(1, THREADS + 1):
Graphs.temps[n] = Graph(5, 1, None, cpu.cpu_temp[n], max_value=cpu.cpu_temp_crit, offset=-23)
cx = cy = cc = 0
2020-07-18 01:16:01 +00:00
ccw = (bw + 1) // cls.box_columns
if cpu.cpu_freq:
freq: str = f'{cpu.cpu_freq} Mhz' if cpu.cpu_freq < 1000 else f'{float(cpu.cpu_freq / 1000):.1f} GHz'
out += f'{Mv.to(by - 1, bx + bw - 9)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title(freq)}{Fx.ub}{THEME.div_line(Symbol.title_right)}'
out += (f'{Mv.to(y, x)}{Graphs.cpu["up"](None if cls.resized else cpu.cpu_usage[0][-1])}{Mv.to(y + hh, x)}{Graphs.cpu["down"](None if cls.resized else cpu.cpu_usage[0][-1])}'
2020-07-18 01:16:01 +00:00
f'{THEME.main_fg}{Mv.to(by + cy, bx + cx)}{"CPU "}{Meters.cpu(cpu.cpu_usage[0][-1])}'
2020-07-16 00:51:55 +00:00
f'{THEME.gradient["cpu"][cpu.cpu_usage[0][-1]]}{cpu.cpu_usage[0][-1]:>4}{THEME.main_fg}%')
if CONFIG.check_temp:
out += (f'{THEME.inactive_fg} ⡀⡀⡀⡀⡀{Mv.l(5)}{THEME.gradient["temp"][cpu.cpu_temp[0][-1]]}{Graphs.temps[0](None if cls.resized else cpu.cpu_temp[0][-1])}'
2020-07-16 00:51:55 +00:00
f'{cpu.cpu_temp[0][-1]:>4}{THEME.main_fg}°C')
cy += 1
for n in range(1, THREADS + 1):
out += f'{THEME.main_fg}{Mv.to(by + cy, bx + cx)}{"Core" + str(n):<{7 if cls.column_size > 0 else 5}}'
if cls.column_size > 0:
out += f'{THEME.inactive_fg}{"" * (5 * cls.column_size)}{Mv.l(5 * cls.column_size)}{THEME.gradient["cpu"][cpu.cpu_usage[n][-1]]}{Graphs.cores[n-1](None if cls.resized else cpu.cpu_usage[n][-1])}'
2020-07-16 00:51:55 +00:00
else:
out += f'{THEME.gradient["cpu"][cpu.cpu_usage[n][-1]]}'
out += f'{cpu.cpu_usage[n][-1]:>4}{THEME.main_fg}%'
if CONFIG.check_temp:
if cls.column_size > 1:
out += f'{THEME.inactive_fg} ⡀⡀⡀⡀⡀{Mv.l(5)}{THEME.gradient["temp"][cpu.cpu_temp[n][-1]]}{Graphs.temps[n](None if cls.resized else cpu.cpu_temp[n][-1])}'
2020-07-16 00:51:55 +00:00
else:
out += f'{THEME.gradient["temp"][cpu.cpu_temp[n][-1]]}'
out += f'{cpu.cpu_temp[n][-1]:>4}{THEME.main_fg}°C'
cy += 1
if cy == bh:
cc += 1; cy = 1; cx = ccw * cc
if cc == cls.box_columns: break
if cy < bh - 1: cy = bh - 1
if cls.column_size == 2 and CONFIG.check_temp:
lavg = f'Load Average: {" ".join(str(l) for l in cpu.load_avg):^18.18}'
elif cls.column_size == 2 or (cls.column_size == 1 and CONFIG.check_temp):
lavg = f'L-AVG: {" ".join(str(l) for l in cpu.load_avg):^14.14}'
else:
lavg = f'{" ".join(str(round(l, 1)) for l in cpu.load_avg):^11.11}'
out += f'{Mv.to(by + cy, bx + cx)}{THEME.main_fg}{lavg}'
out += f'{Mv.to(y + h - 1, x + 1)}{THEME.inactive_fg}up {cpu.uptime}'
2020-07-24 01:44:11 +00:00
Draw.buffer(cls.buffer, f'{out}{Term.fg}', only_save=Menu.active)
cls.resized = cls.redraw = False
2020-07-16 00:51:55 +00:00
2020-07-09 00:56:03 +00:00
class MemBox(Box):
name = "mem"
height_p = 40
width_p = 45
x = 1
y = 1
2020-07-18 01:16:01 +00:00
mem_meter: int = 0
mem_size: int = 0
disk_meter: int = 0
2020-07-09 00:56:03 +00:00
divider: int = 0
mem_width: int = 0
disks_width: int = 0
2020-07-18 01:16:01 +00:00
graph_height: int
resized: bool = True
redraw: bool = False
2020-07-16 00:51:55 +00:00
buffer: str = "mem"
2020-07-18 01:16:01 +00:00
swap_on: bool = CONFIG.show_swap
2020-07-16 00:51:55 +00:00
Box.buffers.append(buffer)
2020-07-18 01:16:01 +00:00
mem_names: List[str] = ["used", "available", "cached", "free"]
swap_names: List[str] = ["used", "free"]
2020-07-09 00:56:03 +00:00
@classmethod
def _calc_size(cls):
cls.width = round(Term.width * cls.width_p / 100)
cls.height = round(Term.height * cls.height_p / 100) + 1
Box._b_mem_h = cls.height
cls.y = Box._b_cpu_h + 1
2020-07-18 01:16:01 +00:00
if CONFIG.show_disks:
cls.mem_width = ceil((cls.width - 3) / 2)
cls.disks_width = cls.width - cls.mem_width - 3
if cls.mem_width + cls.disks_width < cls.width - 2: cls.mem_width += 1
cls.divider = cls.x + cls.mem_width
else:
cls.mem_width = cls.width - 1
item_height: int = 6 if cls.swap_on and not CONFIG.swap_disk else 4
if cls.height - (3 if cls.swap_on and not CONFIG.swap_disk else 2) > 2 * item_height: cls.mem_size = 3
2020-07-18 01:16:01 +00:00
elif cls.mem_width > 25: cls.mem_size = 2
else: cls.mem_size = 1
cls.mem_meter = cls.width - (cls.disks_width if CONFIG.show_disks else 0) - (9 if cls.mem_size > 2 else 20)
if cls.mem_size == 1: cls.mem_meter += 6
if cls.mem_meter < 1: cls.mem_meter = 0
if CONFIG.mem_graphs:
cls.graph_height = round(((cls.height - (2 if cls.swap_on and not CONFIG.swap_disk else 1)) - (2 if cls.mem_size == 3 else 1) * item_height) / item_height)
2020-07-18 01:16:01 +00:00
if cls.graph_height == 0: cls.graph_height = 1
if cls.graph_height > 1: cls.mem_meter += 6
else:
cls.graph_height = 0
if CONFIG.show_disks:
cls.disk_meter = cls.width - cls.mem_width - 23
if cls.disks_width < 25:
cls.disk_meter += 10
if cls.disk_meter < 1: cls.disk_meter = 0
2020-07-09 00:56:03 +00:00
@classmethod
def _draw_bg(cls) -> str:
2020-07-18 01:16:01 +00:00
out: str = ""
out += f'{create_box(box=cls, line_color=THEME.mem_box)}'
if CONFIG.show_disks:
out += (f'{Mv.to(cls.y, cls.divider + 2)}{THEME.mem_box(Symbol.title_left)}{Fx.b}{THEME.title("disks")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}'
f'{Mv.to(cls.y, cls.divider)}{THEME.mem_box(Symbol.div_up)}'
f'{Mv.to(cls.y + cls.height - 1, cls.divider)}{THEME.mem_box(Symbol.div_down)}{THEME.div_line}'
f'{"".join(f"{Mv.to(cls.y + i, cls.divider)}{Symbol.v_line}" for i in range(1, cls.height - 1))}')
return out
@classmethod
def _draw_fg(cls):
mem = MemCollector
2020-07-24 01:44:11 +00:00
if mem.redraw: cls.redraw = True
2020-07-18 01:16:01 +00:00
out: str = ""
2020-07-24 01:44:11 +00:00
out_misc: str = ""
2020-07-18 01:16:01 +00:00
gbg: str = ""
gmv: str = ""
gli: str = ""
2020-07-24 01:44:11 +00:00
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
if cls.resized or cls.redraw:
2020-07-24 01:44:11 +00:00
cls._calc_size()
out_misc += cls._draw_bg()
2020-07-18 01:16:01 +00:00
Meters.mem = {}
Meters.swap = {}
Meters.disks_used = {}
Meters.disks_free = {}
if cls.mem_meter > 0:
for name in cls.mem_names:
if CONFIG.mem_graphs:
Meters.mem[name] = Graph(cls.mem_meter, cls.graph_height, THEME.gradient[name], mem.vlist[name])
else:
Meters.mem[name] = Meter(mem.percent[name], cls.mem_meter, name)
if cls.swap_on:
for name in cls.swap_names:
if CONFIG.mem_graphs and not CONFIG.swap_disk:
Meters.swap[name] = Graph(cls.mem_meter, cls.graph_height, THEME.gradient[name], mem.swap_vlist[name])
elif CONFIG.swap_disk:
Meters.disks_used["__swap"] = Meter(mem.swap_percent["used"], cls.disk_meter, "used")
if len(mem.disks) * 3 <= h + 1:
Meters.disks_free["__swap"] = Meter(mem.swap_percent["free"], cls.disk_meter, "free")
break
else:
Meters.swap[name] = Meter(mem.swap_percent[name], cls.mem_meter, name)
if cls.disk_meter > 0:
for n, name in enumerate(mem.disks.keys()):
if n * 2 > h: break
2020-07-18 01:16:01 +00:00
Meters.disks_used[name] = Meter(mem.disks[name]["used_percent"], cls.disk_meter, "used")
if len(mem.disks) * 3 <= h + 1:
Meters.disks_free[name] = Meter(mem.disks[name]["free_percent"], cls.disk_meter, "free")
2020-07-24 01:44:11 +00:00
Key.mouse["h"] = [[x + cls.mem_width - 8 + i, y-1] for i in range(5)]
out_misc += (f'{Mv.to(y-1, x + cls.mem_width - 9)}{THEME.mem_box(Symbol.title_left)}{Fx.b if CONFIG.mem_graphs else ""}'
f'{THEME.title("grap")}{THEME.hi_fg("h")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}')
if CONFIG.show_disks:
Key.mouse["p"] = [[x + w - 6 + i, y-1] for i in range(4)]
out_misc += (f'{Mv.to(y-1, x + w - 7)}{THEME.mem_box(Symbol.title_left)}{Fx.b if CONFIG.swap_disk else ""}'
f'{THEME.title("swa")}{THEME.hi_fg("p")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}')
Draw.buffer("mem_misc", out_misc, only_save=True)
2020-07-18 01:16:01 +00:00
#* Mem
cx = 1; cy = 1
2020-07-27 01:13:13 +00:00
out += f'{Mv.to(y, x+1)}{THEME.title}{Fx.b}Total:{mem.string["total"]:>{cls.mem_width - 9}}{Fx.ub}{THEME.main_fg}'
2020-07-18 01:16:01 +00:00
if cls.graph_height > 0:
gli = f'{Mv.l(2)}{THEME.mem_box(Symbol.title_right)}{THEME.div_line}{Symbol.h_line * (cls.mem_width - 1)}{"" if CONFIG.show_disks else THEME.mem_box}{Symbol.title_left}{Mv.l(cls.mem_width - 1)}{THEME.title}'
if cls.graph_height >= 2:
gbg = f'{Mv.l(1)}'
gmv = f'{Mv.l(cls.mem_width - 2)}{Mv.u(cls.graph_height - 1)}'
big_mem: bool = True if cls.mem_width > 21 else False
2020-07-18 01:16:01 +00:00
for name in cls.mem_names:
if cls.mem_size > 2:
out += (f'{Mv.to(y+cy, x+cx)}{gli}{name.capitalize()[:None if big_mem else 5]+":":<{1 if big_mem else 6.6}}{Mv.to(y+cy, x+cx + cls.mem_width - 3 - (len(mem.string[name])))}{Fx.trans(mem.string[name])}'
f'{Mv.to(y+cy+1, x+cx)}{gbg}{Meters.mem[name](None if cls.resized else mem.percent[name])}{gmv}{str(mem.percent[name])+"%":>4}')
cy += 2 if not cls.graph_height else cls.graph_height + 1
2020-07-18 01:16:01 +00:00
else:
out += f'{Mv.to(y+cy, x+cx)}{name.capitalize():{5.5 if cls.mem_size > 1 else 1.1}} {gbg}{Meters.mem[name](None if cls.resized else mem.percent[name])}{mem.string[name][:None if cls.mem_size > 1 else -2]:>{9 if cls.mem_size > 1 else 7}}'
cy += 1 if not cls.graph_height else cls.graph_height
2020-07-18 01:16:01 +00:00
#* Swap
if cls.swap_on and CONFIG.show_swap and not CONFIG.swap_disk:
if h - cy > 5:
if cls.graph_height > 0: out += f'{Mv.to(y+cy, x+cx)}{gli}'
cy += 1
2020-07-27 01:13:13 +00:00
out += f'{Mv.to(y+cy, x+cx)}{THEME.title}{Fx.b}Swap:{mem.swap_string["total"]:>{cls.mem_width - 8}}{Fx.ub}{THEME.main_fg}'
cy += 1
2020-07-18 01:16:01 +00:00
for name in cls.swap_names:
if cls.mem_size > 2:
out += (f'{Mv.to(y+cy, x+cx)}{gli}{name.capitalize()[:None if big_mem else 5]+":":<{1 if big_mem else 6.6}}{Mv.to(y+cy, x+cx + cls.mem_width - 3 - (len(mem.swap_string[name])))}{Fx.trans(mem.swap_string[name])}'
f'{Mv.to(y+cy+1, x+cx)}{gbg}{Meters.swap[name](None if cls.resized else mem.swap_percent[name])}{gmv}{str(mem.swap_percent[name])+"%":>4}')
cy += 2 if not cls.graph_height else cls.graph_height + 1
2020-07-18 01:16:01 +00:00
else:
out += f'{Mv.to(y+cy, x+cx)}{name.capitalize():{5.5 if cls.mem_size > 1 else 1.1}} {gbg}{Meters.swap[name](None if cls.resized else mem.swap_percent[name])}{mem.swap_string[name][:None if cls.mem_size > 1 else -2]:>{9 if cls.mem_size > 1 else 7}}'; cy += 1 if not cls.graph_height else cls.graph_height
2020-07-18 01:16:01 +00:00
if cls.graph_height > 0 and not cy == h: out += f'{Mv.to(y+cy, x+cx)}{gli}'
#* Disks
if CONFIG.show_disks:
cx = x + cls.mem_width - 1; cy = 0
big_disk: bool = True if cls.disks_width >= 25 else False
gli = f'{Mv.l(2)}{THEME.div_line}{Symbol.title_right}{Symbol.h_line * cls.disks_width}{THEME.mem_box}{Symbol.title_left}{Mv.l(cls.disks_width - 1)}'
2020-07-18 01:16:01 +00:00
for name, item in mem.disks.items():
if cy > h - 2: break
out += Fx.trans(f'{Mv.to(y+cy, x+cx)}{gli}{THEME.title}{Fx.b}{item["name"]:{cls.disks_width - 2}.12}{Mv.to(y+cy, x + cx + cls.disks_width - 11)}{item["total"][:None if big_disk else -2]:>9}')
out += f'{Mv.to(y+cy, x + cx + (cls.disks_width // 2) - (len(item["io"]) // 2) - 2)}{Fx.ub}{THEME.main_fg}{item["io"]}{Fx.ub}{THEME.main_fg}{Mv.to(y+cy+1, x+cx)}'
out += f'Used:{str(item["used_percent"]) + "%":>4} ' if big_disk else "U "
out += f'{Meters.disks_used[name]}{item["used"][:None if big_disk else -2]:>{9 if big_disk else 7}}'
cy += 2
2020-07-18 01:16:01 +00:00
if len(mem.disks) * 3 <= h + 1:
if cy > h - 1: break
out += Mv.to(y+cy, x+cx)
out += f'Free:{str(item["free_percent"]) + "%":>4} ' if big_disk else f'{"F "}'
out += f'{Meters.disks_free[name]}{item["free"][:None if big_disk else -2]:>{9 if big_disk else 7}}'
cy += 1
2020-07-18 01:16:01 +00:00
if len(mem.disks) * 4 <= h + 1: cy += 1
2020-07-24 01:44:11 +00:00
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
cls.resized = cls.redraw = False
2020-07-09 00:56:03 +00:00
class NetBox(Box, SubBox):
name = "net"
height_p = 28
width_p = 45
x = 1
y = 1
resized: bool = True
redraw: bool = True
graph_height: Dict[str, int] = {}
symbols: Dict[str, str] = {"download" : "", "upload" : ""}
2020-07-16 00:51:55 +00:00
buffer: str = "net"
2020-07-16 00:51:55 +00:00
Box.buffers.append(buffer)
2020-07-09 00:56:03 +00:00
@classmethod
def _calc_size(cls):
cls.width = round(Term.width * cls.width_p / 100)
cls.height = Term.height - Box._b_cpu_h - Box._b_mem_h
cls.y = Term.height - cls.height + 1
cls.box_width = 27
cls.box_height = 9 if cls.height > 10 else cls.height - 2
cls.box_x = cls.width - cls.box_width - 1
2020-07-09 00:56:03 +00:00
cls.box_y = cls.y + ((cls.height - 2) // 2) - cls.box_height // 2 + 1
cls.graph_height["download"] = round((cls.height - 2) / 2)
cls.graph_height["upload"] = cls.height - 2 - cls.graph_height["download"]
cls.redraw = True
2020-07-09 00:56:03 +00:00
@classmethod
def _draw_bg(cls) -> str:
return f'{create_box(box=cls, line_color=THEME.net_box)}\
{create_box(x=cls.box_x, y=cls.box_y, width=cls.box_width, height=cls.box_height, line_color=THEME.div_line, fill=False, title="Download", title2="Upload")}'
@classmethod
def _draw_fg(cls):
net = NetCollector
2020-07-24 01:44:11 +00:00
if net.redraw: cls.redraw = True
if not net.nic: return
out: str = ""
out_misc: str = ""
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
bx, by, bw, bh = cls.box_x + 1, cls.box_y + 1, cls.box_width - 2, cls.box_height - 2
2020-07-24 01:44:11 +00:00
reset: bool = bool(net.stats[net.nic]["download"]["offset"])
if cls.resized or cls.redraw:
2020-07-24 01:44:11 +00:00
out_misc += cls._draw_bg()
Key.mouse["b"] = [[x+w - len(net.nic) - 9 + i, y-1] for i in range(4)]
Key.mouse["n"] = [[x+w - 5 + i, y-1] for i in range(4)]
Key.mouse["z"] = [[x+w - len(net.nic) - 14 + i, y-1] for i in range(4)]
2020-07-24 01:44:11 +00:00
out_misc += (f'{Mv.to(y-1, x+w - 25)}{THEME.net_box}{Symbol.h_line * (10 - len(net.nic))}{Symbol.title_left}{Fx.b if reset else ""}{THEME.hi_fg("z")}{THEME.title("ero")}'
f'{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}'
f'{THEME.net_box}{Symbol.title_left}{Fx.b}{THEME.hi_fg("<b")} {THEME.title(net.nic[:10])} {THEME.hi_fg("n>")}{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}')
Draw.buffer("net_misc", out_misc, only_save=True)
cy = 0
for direction in ["download", "upload"]:
strings = net.strings[net.nic][direction]
stats = net.stats[net.nic][direction]
2020-07-24 01:44:11 +00:00
if stats["redraw"] or cls.resized:
if cls.redraw: stats["redraw"] = True
Graphs.net[direction] = Graph(w - bw - 3, cls.graph_height[direction], THEME.gradient[direction], stats["speed"], max_value=stats["graph_top"], invert=False if direction == "download" else True)
out += f'{Mv.to(y if direction == "download" else y + cls.graph_height["download"], x)}{Graphs.net[direction](None if stats["redraw"] else stats["speed"][-1])}'
2020-07-24 01:44:11 +00:00
out += f'{Mv.to(by+cy, bx)}{THEME.main_fg}{cls.symbols[direction]} {strings["byte_ps"]:<10.10}{Mv.to(by+cy, bx+bw - 12)}{"(" + strings["bit_ps"] + ")":>12.12}'
cy += 1 if bh != 3 else 2
if bh >= 6:
2020-07-24 01:44:11 +00:00
out += f'{Mv.to(by+cy, bx)}{cls.symbols[direction]} {"Top:"}{Mv.to(by+cy, bx+bw - 12)}{"(" + strings["top"] + ")":>12.12}'
cy += 1
if bh >= 4:
out += f'{Mv.to(by+cy, bx)}{cls.symbols[direction]} {"Total:"}{Mv.to(by+cy, bx+bw - 10)}{strings["total"]:>10.10}'
if bh > 2 and bh % 2: cy += 2
else: cy += 1
stats["redraw"] = False
out += f'{Mv.to(y, x)}{THEME.inactive_fg(net.strings[net.nic]["download"]["graph_top"])}{Mv.to(y+h-1, x)}{THEME.inactive_fg(net.strings[net.nic]["upload"]["graph_top"])}'
2020-07-24 01:44:11 +00:00
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
cls.redraw = cls.resized = False
2020-07-09 00:56:03 +00:00
class ProcBox(Box):
name = "proc"
height_p = 68
width_p = 55
x = 1
y = 1
2020-07-27 01:13:13 +00:00
current_y: int = 0
2020-07-24 01:44:11 +00:00
select_max: int = 0
selected: int = 0
2020-07-27 01:13:13 +00:00
selected_pid: int = 0
filtering: bool = False
2020-07-24 01:44:11 +00:00
moved: bool = False
start: int = 1
count: int = 0
2020-07-09 00:56:03 +00:00
detailed: bool = False
detailed_x: int = 0
detailed_y: int = 0
detailed_width: int = 0
detailed_height: int = 8
2020-07-24 01:44:11 +00:00
resized: bool = True
redraw: bool = True
2020-07-16 00:51:55 +00:00
buffer: str = "proc"
2020-07-24 01:44:11 +00:00
pid_counter: Dict[int, int] = {}
2020-07-16 00:51:55 +00:00
Box.buffers.append(buffer)
2020-07-09 00:56:03 +00:00
@classmethod
def _calc_size(cls):
cls.width = round(Term.width * cls.width_p / 100)
cls.height = round(Term.height * cls.height_p / 100)
cls.x = Term.width - cls.width + 1
cls.y = Box._b_cpu_h + 1
cls.detailed_x = cls.x
cls.detailed_y = cls.y
cls.detailed_height = 8
cls.detailed_width = cls.width
2020-07-24 01:44:11 +00:00
cls.redraw = True
cls.resized = True
2020-07-09 00:56:03 +00:00
@classmethod
def _draw_bg(cls) -> str:
return create_box(box=cls, line_color=THEME.proc_box)
2020-07-24 01:44:11 +00:00
@classmethod
2020-07-27 01:13:13 +00:00
def selector(cls, key: str, mouse_pos: Tuple[int, int] = (0, 0)):
2020-07-24 01:44:11 +00:00
old: Tuple[int, int] = (cls.start, cls.selected)
if key == "up":
if cls.selected == 1 and cls.start > 1:
cls.start -= 1
elif cls.selected == 1:
cls.selected = 0
elif cls.selected > 1:
cls.selected -= 1
elif key == "down":
if cls.selected == cls.select_max and cls.start < ProcCollector.num_procs - cls.select_max + 1:
cls.start += 1
elif cls.selected < cls.select_max:
cls.selected += 1
elif key == "mouse_scroll_up" and cls.start > 1:
cls.start -= 5
elif key == "mouse_scroll_down" and cls.start < ProcCollector.num_procs - cls.select_max + 1:
cls.start += 5
elif key == "page_up" and cls.start > 1:
cls.start -= cls.select_max
elif key == "page_down" and cls.start < ProcCollector.num_procs - cls.select_max + 1:
cls.start += cls.select_max
elif key == "home":
if cls.start > 1: cls.start = 1
elif cls.selected > 0: cls.selected = 0
elif key == "end":
if cls.start < ProcCollector.num_procs - cls.select_max + 1: cls.start = ProcCollector.num_procs - cls.select_max + 1
elif cls.selected < cls.select_max: cls.selected = cls.select_max
2020-07-27 01:13:13 +00:00
elif key == "mouse_click":
if mouse_pos[0] > cls.x + cls.width - 4 and mouse_pos[1] > cls.current_y and mouse_pos[1] < cls.current_y + cls.select_max:
if mouse_pos[1] == cls.current_y + 1:
cls.start = 1
elif mouse_pos[1] == cls.current_y + cls.select_max - 1:
cls.start = ProcCollector.num_procs - cls.select_max + 1
else:
cls.start = round((mouse_pos[1] - cls.current_y - 1) * ((ProcCollector.num_procs - cls.select_max - 2) / (cls.select_max - 2)))
else:
cls.selected = mouse_pos[1] - cls.current_y if mouse_pos[1] >= cls.current_y else cls.selected
elif key == "mouse_unselect":
cls.selected = 0
2020-07-24 01:44:11 +00:00
2020-07-27 01:13:13 +00:00
if cls.start > ProcCollector.num_procs - cls.select_max + 1 and ProcCollector.num_procs > cls.select_max: cls.start = ProcCollector.num_procs - cls.select_max + 1
elif cls.start > ProcCollector.num_procs: cls.start = ProcCollector.num_procs
2020-07-24 01:44:11 +00:00
if cls.start < 1: cls.start = 1
2020-07-27 01:13:13 +00:00
if cls.selected > ProcCollector.num_procs and ProcCollector.num_procs < cls.select_max: cls.selected = ProcCollector.num_procs
elif cls.selected > cls.select_max: cls.selected = cls.select_max
if cls.selected < 0: cls.selected = 0
2020-07-24 01:44:11 +00:00
if old != (cls.start, cls.selected):
cls.moved = True
2020-07-27 01:13:13 +00:00
Collector.collect(ProcCollector, proc_interrupt=True, only_draw=True)
2020-07-24 01:44:11 +00:00
@classmethod
def _draw_fg(cls):
proc = ProcCollector
2020-07-27 01:13:13 +00:00
if proc.proc_interrupt: return
2020-07-24 01:44:11 +00:00
if proc.redraw: cls.redraw = True
out: str = ""
out_misc: str = ""
n: int = 0
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
prog_len: int; arg_len: int; val: int; c_color: str; m_color: str; t_color: str; sort_pos: int; tree_len: int; is_selected: bool; calc: int
l_count: int = 0
loc_string: str
2020-07-27 01:13:13 +00:00
scroll_pos: int = 0
2020-07-24 01:44:11 +00:00
indent: str = ""
offset: int = 0
vals: List[str]
g_color: str = ""
2020-07-27 01:13:13 +00:00
s_len: int = 0
if proc.search_filter: s_len = len(proc.search_filter[:10])
2020-07-24 01:44:11 +00:00
end: str = ""
2020-07-27 01:13:13 +00:00
if w > 67:
arg_len = w - 53 - (1 if proc.num_procs > cls.select_max else 0)
prog_len = 15
else:
arg_len = 0
prog_len = w - 38 - (1 if proc.num_procs > cls.select_max else 0)
2020-07-24 01:44:11 +00:00
if CONFIG.proc_tree:
tree_len = arg_len + prog_len + 6
arg_len = 0
if cls.resized or cls.redraw:
2020-07-27 01:13:13 +00:00
cls.select_max = h - 1
cls.current_y = y
2020-07-24 01:44:11 +00:00
sort_pos = x + w - len(CONFIG.proc_sorting) - 7
Key.mouse["left"] = [[sort_pos + i, y-1] for i in range(3)]
Key.mouse["right"] = [[sort_pos + len(CONFIG.proc_sorting) + 3 + i, y-1] for i in range(3)]
Key.mouse["e"] = [[sort_pos - 5 + i, y-1] for i in range(4)]
2020-07-27 01:13:13 +00:00
out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.h_line * (w - 9))}'
2020-07-24 01:44:11 +00:00
f'{Mv.to(y-1, sort_pos)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("<")} {THEME.title(CONFIG.proc_sorting)} '
2020-07-27 01:13:13 +00:00
f'{THEME.hi_fg(">")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
if w > 37 + s_len:
out_misc += (f'{Mv.to(y-1, sort_pos - 6)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_tree else ""}'
f'{THEME.title("tre")}{THEME.hi_fg("e")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
if w > 45 + s_len:
2020-07-24 01:44:11 +00:00
Key.mouse["r"] = [[sort_pos - 14 + i, y-1] for i in range(7)]
out_misc += (f'{Mv.to(y-1, sort_pos - 15)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_reversed else ""}'
f'{THEME.hi_fg("r")}{THEME.title("everse")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
2020-07-27 01:13:13 +00:00
if w > 55 + s_len:
Key.mouse["o"] = [[sort_pos - 24 + i, y-1] for i in range(8)]
out_misc += (f'{Mv.to(y-1, sort_pos - 25)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_per_core else ""}'
2020-07-24 01:44:11 +00:00
f'{THEME.title("per-c")}{THEME.hi_fg("o")}{THEME.title("re")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
2020-07-27 01:13:13 +00:00
if w > 65 + s_len:
Key.mouse["g"] = [[sort_pos - 34 + i, y-1] for i in range(8)]
out_misc += (f'{Mv.to(y-1, sort_pos - 35)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_gradient else ""}{THEME.hi_fg("g")}'
f'{THEME.title("radient")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
if w > 73 + s_len:
Key.mouse["c"] = [[sort_pos - 42 + i, y-1] for i in range(6)]
out_misc += (f'{Mv.to(y-1, sort_pos - 43)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_colors else ""}'
f'{THEME.hi_fg("c")}{THEME.title("olors")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
Key.mouse["f"] = [[x+9 + i, y-1] for i in range(6 if not proc.search_filter else 2 + len(proc.search_filter[-10:]))]
if proc.search_filter:
Key.mouse["delete"] = [[x+12 + len(proc.search_filter[-10:]) + i, y-1] for i in range(3)]
elif "delete" in Key.mouse:
del Key.mouse["delete"]
out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.title_left)}{Fx.b if cls.filtering or proc.search_filter else ""}{THEME.hi_fg("f")}{THEME.title}' +
("ilter" if not proc.search_filter and not cls.filtering else f' {proc.search_filter[-(10 if w < 83 else w - 74):]}{(Fx.bl + "" + Fx.ubl) if cls.filtering else THEME.hi_fg(" del")}') +
f'{THEME.proc_box(Symbol.title_right)}')
2020-07-24 01:44:11 +00:00
Draw.buffer("proc_misc", out_misc, only_save=True)
selected: str = CONFIG.proc_sorting
if selected == "memory": selected = "mem"
if selected == "threads" and not CONFIG.proc_tree and not arg_len: selected = "tr"
if CONFIG.proc_tree:
2020-07-27 01:13:13 +00:00
out += (f'{THEME.title}{Fx.b}{Mv.to(y, x)}{" Tree:":<{tree_len-2}}' "Threads: " f'{"User:":<9}Mem%{"Cpu%":>11}{Fx.ub}{THEME.main_fg} ' +
(" " if proc.num_procs > cls.select_max else ""))
2020-07-24 01:44:11 +00:00
if selected in ["program", "arguments"]: selected = "tree"
else:
out += (f'{THEME.title}{Fx.b}{Mv.to(y, x)}{"Pid:":>7} {"Program:" if prog_len > 8 else "Prg:":<{prog_len}}' + (f'{"Arguments:":<{arg_len-4}}' if arg_len else "") +
2020-07-27 01:13:13 +00:00
f'{"Threads:" if arg_len else " Tr:"} {"User:":<9}Mem%{"Cpu%":>11}{Fx.ub}{THEME.main_fg} ' +
(" " if proc.num_procs > cls.select_max else ""))
2020-07-24 01:44:11 +00:00
if selected == "program" and prog_len <= 8: selected = "prg"
selected = selected.split(" ")[0].capitalize()
out = out.replace(selected, f'{Fx.u}{selected}{Fx.uu}')
cy = 1
2020-07-27 01:13:13 +00:00
if cls.start > proc.num_procs - cls.select_max + 1 and proc.num_procs > cls.select_max: cls.start = proc.num_procs - cls.select_max + 1
elif cls.start > proc.num_procs: cls.start = proc.num_procs
if cls.start < 1: cls.start = 1
if cls.selected > proc.num_procs and proc.num_procs < cls.select_max: cls.selected = proc.num_procs
elif cls.selected > cls.select_max: cls.selected = cls.select_max
if cls.selected < 0: cls.selected = 0
2020-07-24 01:44:11 +00:00
for n, (pid, items) in enumerate(proc.processes.items(), start=1):
if n < cls.start: continue
l_count += 1
if l_count == cls.selected:
is_selected = True
cls.selected_pid = pid
else: is_selected = False
name, cmd, threads, username, mem, cpu = items.values()
if cpu > 1.0 or pid in Graphs.pid_cpu:
if pid not in Graphs.pid_cpu:
Graphs.pid_cpu[pid] = Graph(5, 1, None, [0])
cls.pid_counter[pid] = 0
elif cpu < 1.0:
cls.pid_counter[pid] += 1
if cls.pid_counter[pid] > 10:
del cls.pid_counter[pid], Graphs.pid_cpu[pid]
else:
cls.pid_counter[pid] = 0
if CONFIG.proc_tree:
indent = name
name = cmd
offset = tree_len - len(f'{indent}{pid}')
if offset < 1: offset = 0
indent = f'{indent:.{tree_len - len(str(pid))}}'
else:
offset = prog_len - 1
end = f'{THEME.main_fg}{Fx.ub}' if CONFIG.proc_colors else Fx.ub
if cls.selected > cy: calc = cls.selected - cy
elif cls.selected > 0 and cls.selected <= cy: calc = cy - cls.selected
else: calc = cy
if CONFIG.proc_colors and not is_selected:
vals = []
for v in [int(cpu), int(mem), int(threads // 3)]:
if CONFIG.proc_gradient:
val = ((v if v <= 100 else 100) + 100) - calc * 100 // cls.select_max
vals += [f'{THEME.gradient["proc_color" if val < 100 else "cpu"][val if val < 100 else val - 100]}']
else:
vals += [f'{THEME.gradient["cpu"][v if v <= 100 else 100]}']
c_color, m_color, t_color = vals
2020-07-27 01:13:13 +00:00
else:
c_color = m_color = t_color = Fx.b
2020-07-24 01:44:11 +00:00
if CONFIG.proc_gradient and not is_selected:
g_color = f'{THEME.gradient["proc"][calc * 100 // cls.select_max]}'
if is_selected:
c_color = m_color = t_color = g_color = end = ""
out += f'{THEME.selected_bg}{THEME.selected_fg}{Fx.b}'
out += (f'{Mv.to(y+cy, x)}{g_color}{indent}{pid:>{(1 if CONFIG.proc_tree else 7)}} ' +
f'{c_color}{name:<{offset}.{offset}} {end}' +
(f'{g_color}{cmd:<{arg_len}.{arg_len-1}}' if arg_len else "") +
t_color + (f'{threads:>4} ' if threads < 1000 else "999> ") + end +
g_color + (f'{username:<9.9}' if len(username) < 10 else f'{username[:8]:<8}+') +
m_color + (f'{mem:>4.1f}' if mem < 100 else f'{mem:>4.0f} ') + end +
2020-07-27 01:13:13 +00:00
f' {THEME.inactive_fg}{""*5}{THEME.main_fg}{g_color}{c_color}' + (f' {cpu:>4.1f} ' if cpu < 100 else f'{cpu:>5.0f} ') + end +
(" " if proc.num_procs > cls.select_max else ""))
2020-07-24 01:44:11 +00:00
if pid in Graphs.pid_cpu:
2020-07-27 01:13:13 +00:00
#if is_selected: c_color = THEME.proc_misc
2020-07-24 01:44:11 +00:00
out += f'{Mv.to(y+cy, x + w - 11)}{c_color if CONFIG.proc_colors else THEME.proc_misc}{Graphs.pid_cpu[pid](None if cls.moved else round(cpu))}{THEME.main_fg}'
2020-07-27 01:13:13 +00:00
if is_selected: out += f'{Fx.ub}{Term.fg}{Term.bg}{Mv.to(y+cy, x + w - 1)}{" " if proc.num_procs > cls.select_max else ""}'
2020-07-24 01:44:11 +00:00
cy += 1
if cy == h: break
if cy < h:
for i in range(h-cy):
out += f'{Mv.to(y+cy+i, x)}{" " * w}'
2020-07-27 01:13:13 +00:00
loc_string = f'{cls.start + cls.selected - 1}/{proc.num_procs}'
2020-07-24 01:44:11 +00:00
out += (f'{Mv.to(y+h, x + w - 15 - len(loc_string))}{THEME.proc_box}{Symbol.h_line*10}{Symbol.title_left}{THEME.title}'
f'{Fx.b}{loc_string}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
2020-07-27 01:13:13 +00:00
if proc.num_procs > cls.select_max:
Key.mouse["mouse_scroll_up"] = [[x+w-1, y]]
Key.mouse["mouse_scroll_down"] = [[x+w-1, y+h-1]]
scroll_pos = round(cls.start * (cls.select_max - 2) / (proc.num_procs - (cls.select_max - 2)))
if scroll_pos < 0 or cls.start == 1: scroll_pos = 0
elif scroll_pos > h - 3 or cls.start >= proc.num_procs - cls.select_max: scroll_pos = h - 3
out += (f'{Mv.to(y, x+w-1)}{Fx.b}{THEME.main_fg}{Mv.to(y+h-1, x+w-1)}{Fx.ub}'
f'{Mv.to(y+1+scroll_pos, x+w-1)}')
elif "scroll_up" in Key.mouse:
del Key.mouse["scroll_up"], Key.mouse["scroll_down"]
#▼▲ ↓ ↑
2020-07-24 01:44:11 +00:00
cls.count += 1
if cls.count == 100:
cls.count == 0
for p in list(cls.pid_counter):
if not psutil.pid_exists(p):
del cls.pid_counter[p], Graphs.pid_cpu[p]
Draw.buffer(cls.buffer, f'{out}{out_misc}{Term.fg}', only_save=Menu.active)
cls.redraw = cls.resized = cls.moved = False
2020-07-09 00:56:03 +00:00
class Collector:
'''Data collector master class
* .start(): Starts collector thread
* .stop(): Stops collector thread
* .collect(*collectors: Collector, draw_now: bool = True, interrupt: bool = False): queues up collectors to run'''
stopping: bool = False
started: bool = False
draw_now: bool = False
2020-07-24 01:44:11 +00:00
redraw: bool = False
only_draw: bool = False
2020-07-09 00:56:03 +00:00
thread: threading.Thread
collect_run = threading.Event()
collect_idle = threading.Event()
collect_idle.set()
collect_done = threading.Event()
collect_queue: List = []
collect_interrupt: bool = False
2020-07-27 01:13:13 +00:00
proc_interrupt: bool = False
2020-07-24 01:44:11 +00:00
use_draw_list: bool = False
2020-07-09 00:56:03 +00:00
@classmethod
def start(cls):
cls.stopping = False
cls.thread = threading.Thread(target=cls._runner, args=())
cls.thread.start()
cls.started = True
@classmethod
def stop(cls):
if cls.started and cls.thread.is_alive():
cls.stopping = True
2020-07-18 01:16:01 +00:00
cls.started = False
cls.collect_queue = []
cls.collect_idle.set()
cls.collect_done.set()
2020-07-09 00:56:03 +00:00
try:
cls.thread.join()
except:
pass
@classmethod
def _runner(cls):
'''This is meant to run in it's own thread, collecting and drawing when collect_run is set'''
2020-07-16 00:51:55 +00:00
draw_buffers: List[str] = []
debugged: bool = False
2020-07-16 00:51:55 +00:00
try:
while not cls.stopping:
cls.collect_run.wait(0.1)
if not cls.collect_run.is_set():
continue
2020-07-18 01:16:01 +00:00
draw_buffers = []
cls.collect_interrupt = False
2020-07-16 00:51:55 +00:00
cls.collect_run.clear()
cls.collect_idle.clear()
2020-07-24 01:44:11 +00:00
cls.collect_done.clear()
if DEBUG and not debugged: TimeIt.start("Collect and draw")
2020-07-16 00:51:55 +00:00
while cls.collect_queue:
collector = cls.collect_queue.pop()
2020-07-24 01:44:11 +00:00
if not cls.only_draw:
collector._collect()
2020-07-16 00:51:55 +00:00
collector._draw()
2020-07-24 01:44:11 +00:00
if cls.use_draw_list: draw_buffers.append(collector.buffer)
2020-07-18 01:16:01 +00:00
if cls.collect_interrupt: break
if DEBUG and not debugged: TimeIt.stop("Collect and draw"); debugged = True
2020-07-18 01:16:01 +00:00
if cls.draw_now and not Menu.active and not cls.collect_interrupt:
2020-07-24 01:44:11 +00:00
if cls.use_draw_list: Draw.out(*draw_buffers)
else: Draw.out()
2020-07-16 00:51:55 +00:00
cls.collect_idle.set()
cls.collect_done.set()
except Exception as e:
errlog.exception(f'Data collection thread failed with exception: {e}')
2020-07-27 01:13:13 +00:00
cls.collect_idle.set()
2020-07-09 00:56:03 +00:00
cls.collect_done.set()
2020-07-16 00:51:55 +00:00
clean_quit(1, thread=True)
2020-07-09 00:56:03 +00:00
@classmethod
2020-07-27 01:13:13 +00:00
def collect(cls, *collectors, draw_now: bool = True, interrupt: bool = False, proc_interrupt: bool = False, redraw: bool = False, only_draw: bool = False):
2020-07-09 00:56:03 +00:00
'''Setup collect queue for _runner'''
cls.collect_interrupt = interrupt
2020-07-27 01:13:13 +00:00
cls.proc_interrupt = proc_interrupt
2020-07-09 00:56:03 +00:00
cls.collect_idle.wait()
cls.collect_interrupt = False
2020-07-27 01:13:13 +00:00
cls.proc_interrupt = False
2020-07-24 01:44:11 +00:00
cls.use_draw_list = False
2020-07-09 00:56:03 +00:00
cls.draw_now = draw_now
2020-07-24 01:44:11 +00:00
cls.redraw = redraw
cls.only_draw = only_draw
2020-07-09 00:56:03 +00:00
if collectors:
cls.collect_queue = [*collectors]
2020-07-24 01:44:11 +00:00
cls.use_draw_list = True
2020-07-09 00:56:03 +00:00
else:
cls.collect_queue = list(cls.__subclasses__())
cls.collect_run.set()
class CpuCollector(Collector):
2020-07-18 01:16:01 +00:00
'''Collects cpu usage for cpu and cores, cpu frequency, load_avg, uptime and cpu temps'''
2020-07-16 00:51:55 +00:00
cpu_usage: List[List[int]] = []
cpu_temp: List[List[int]] = []
cpu_temp_high: int = 0
cpu_temp_crit: int = 0
2020-07-16 00:51:55 +00:00
for _ in range(THREADS + 1):
2020-07-09 00:56:03 +00:00
cpu_usage.append([])
cpu_temp.append([])
2020-07-09 00:56:03 +00:00
cpu_freq: int = 0
load_avg: List[float] = []
2020-07-16 00:51:55 +00:00
uptime: str = ""
buffer: str = CpuBox.buffer
2020-07-09 00:56:03 +00:00
@staticmethod
def _get_sensors() -> str:
'''Check if we can get cpu temps and return method of getting temps'''
if SYSTEM == "MacOS":
try:
if which("osx-cpu-temp") and subprocess.check_output("osx-cpu-temp", text=True).rstrip().endswith("°C"):
return "osx-cpu-temp"
except: pass
elif hasattr(psutil, "sensors_temperatures"):
try:
temps = psutil.sensors_temperatures()
if temps:
for _, entries in temps.items():
for entry in entries:
if entry.label.startswith(("Package", "Core 0", "Tdie")):
return "psutil"
except: pass
try:
if SYSTEM == "Linux" and which("vcgencmd") and subprocess.check_output("vcgencmd measure_temp", text=True).rstrip().endswith("'C"):
return "vcgencmd"
except: pass
CONFIG.check_temp = False
return ""
sensor_method: str = _get_sensors.__func__() # type: ignore
got_sensors: bool = True if sensor_method else False
2020-07-09 00:56:03 +00:00
@classmethod
def _collect(cls):
cls.cpu_usage[0].append(round(psutil.cpu_percent(percpu=False)))
for n, thread in enumerate(psutil.cpu_percent(percpu=True), start=1):
cls.cpu_usage[n].append(round(thread))
if len(cls.cpu_usage[n]) > Term.width * 2:
del cls.cpu_usage[n][0]
if hasattr(psutil.cpu_freq(), "current"):
cls.cpu_freq = round(psutil.cpu_freq().current)
cls.load_avg = [round(lavg, 2) for lavg in os.getloadavg()]
2020-07-16 00:51:55 +00:00
cls.uptime = str(timedelta(seconds=round(time()-psutil.boot_time(),0)))[:-3]
2020-07-09 00:56:03 +00:00
if CONFIG.check_temp and cls.got_sensors:
cls._collect_temps()
@classmethod
def _collect_temps(cls):
temp: int
cores: List[int] = []
cpu_type: str = ""
if cls.sensor_method == "psutil":
for _, entries in psutil.sensors_temperatures().items():
for entry in entries:
if entry.label.startswith(("Package", "Tdie")):
cpu_type = "ryzen" if entry.label.startswith("Package") else "ryzen"
if not cls.cpu_temp_high:
cls.cpu_temp_high, cls.cpu_temp_crit = round(entry.high), round(entry.critical)
temp = round(entry.current)
elif entry.label.startswith(("Core", "Tccd")):
if not cpu_type:
cpu_type = "other"
if not cls.cpu_temp_high:
cls.cpu_temp_high, cls.cpu_temp_crit = round(entry.high), round(entry.critical)
temp = round(entry.current)
cores.append(round(entry.current))
if len(cores) < THREADS:
if cpu_type == "intel" or (cpu_type == "other" and len(cores) == THREADS // 2):
cls.cpu_temp[0].append(temp)
for n, t in enumerate(cores, start=1):
cls.cpu_temp[n].append(t)
cls.cpu_temp[THREADS // 2 + n].append(t)
elif cpu_type == "ryzen" or cpu_type == "other":
cls.cpu_temp[0].append(temp)
if len(cores) < 1: cores.append(temp)
z = 1
for t in cores:
for i in range(THREADS // len(cores)):
cls.cpu_temp[z + i].append(t)
z += i
else:
cores.insert(0, temp)
for n, t in enumerate(cores):
cls.cpu_temp[n].append(t)
else:
2020-07-09 00:56:03 +00:00
try:
if cls.sensor_method == "osx-cpu-temp":
temp = round(float(subprocess.check_output("osx-cpu-temp", text=True).rstrip().rstrip("°C")))
elif cls.sensor_method == "vcgencmd":
temp = round(float(subprocess.check_output("vcgencmd measure_temp", text=True).rstrip().rstrip("'C")))
except Exception as e:
errlog.exception(f'{e}')
cls.got_sensors = False
CONFIG.check_temp = False
CpuBox._calc_size()
else:
2020-07-09 00:56:03 +00:00
for n in range(THREADS + 1):
cls.cpu_temp[n].append(temp)
if len(cls.cpu_temp[0]) > 5:
for n in range(len(cls.cpu_temp)):
del cls.cpu_temp[n][0]
2020-07-09 00:56:03 +00:00
@classmethod
def _draw(cls):
2020-07-18 01:16:01 +00:00
CpuBox._draw_fg()
class MemCollector(Collector):
'''Collects memory and disks information'''
values: Dict[str, int] = {}
vlist: Dict[str, List[int]] = {}
percent: Dict[str, int] = {}
string: Dict[str, str] = {}
swap_values: Dict[str, int] = {}
swap_vlist: Dict[str, List[int]] = {}
swap_percent: Dict[str, int] = {}
swap_string: Dict[str, str] = {}
disks: Dict[str, Dict]
disk_hist: Dict[str, Tuple] = {}
timestamp: float = time()
old_disks: List[str] = []
2020-07-18 01:16:01 +00:00
excludes: List[str] = ["squashfs"]
if SYSTEM == "BSD": excludes += ["devfs", "tmpfs", "procfs", "linprocfs", "gvfs", "fusefs"]
buffer: str = MemBox.buffer
@classmethod
def _collect(cls):
#* Collect memory
mem = psutil.virtual_memory()
if hasattr(mem, "cached"):
cls.values["cached"] = mem.cached
else:
cls.values["cached"] = mem.active
cls.values["total"], cls.values["free"], cls.values["available"] = mem.total, mem.free, mem.available
cls.values["used"] = cls.values["total"] - cls.values["available"]
for key, value in cls.values.items():
cls.string[key] = floating_humanizer(value)
if key == "total": continue
cls.percent[key] = round(value * 100 / cls.values["total"])
if CONFIG.mem_graphs:
if not key in cls.vlist: cls.vlist[key] = []
cls.vlist[key].append(cls.percent[key])
if len(cls.vlist[key]) > MemBox.width: del cls.vlist[key][0]
#* Collect swap
if CONFIG.show_swap or CONFIG.swap_disk:
swap = psutil.swap_memory()
cls.swap_values["total"], cls.swap_values["free"] = swap.total, swap.free
cls.swap_values["used"] = cls.swap_values["total"] - cls.swap_values["free"]
if swap.total:
if not MemBox.swap_on:
MemBox.redraw = True
2020-07-18 01:16:01 +00:00
MemBox.swap_on = True
for key, value in cls.swap_values.items():
cls.swap_string[key] = floating_humanizer(value)
if key == "total": continue
cls.swap_percent[key] = round(value * 100 / cls.swap_values["total"])
if CONFIG.mem_graphs:
if not key in cls.swap_vlist: cls.swap_vlist[key] = []
cls.swap_vlist[key].append(cls.swap_percent[key])
if len(cls.swap_vlist[key]) > MemBox.width: del cls.swap_vlist[key][0]
else:
if MemBox.swap_on:
MemBox.redraw = True
2020-07-18 01:16:01 +00:00
MemBox.swap_on = False
else:
if MemBox.swap_on:
MemBox.redraw = True
2020-07-18 01:16:01 +00:00
MemBox.swap_on = False
if not CONFIG.show_disks: return
#* Collect disks usage
disk_read: int = 0
disk_write: int = 0
dev_name: str
disk_name: str
filtering: Tuple = ()
filter_exclude: bool = False
io_string: str
u_percent: int
disk_list: List[str] = []
cls.disks = {}
if CONFIG.disks_filter:
if CONFIG.disks_filter.startswith("exclude="):
filter_exclude = True
filtering = tuple(v.strip() for v in CONFIG.disks_filter.replace("exclude=", "").strip().split(","))
else:
filtering = tuple(v.strip() for v in CONFIG.disks_filter.strip().split(","))
io_counters = psutil.disk_io_counters(perdisk=True if SYSTEM == "Linux" else False, nowrap=True)
for disk in psutil.disk_partitions():
disk_io = None
io_string = ""
disk_name = disk.mountpoint.rsplit('/', 1)[-1] if not disk.mountpoint == "/" else "root"
while disk_name in disk_list: disk_name += "_"
disk_list += [disk_name]
if cls.excludes and disk.fstype in cls.excludes:
continue
if filtering and ((not filter_exclude and not disk_name.endswith(filtering)) or (filter_exclude and disk_name.endswith(filtering))):
continue
#elif filtering and disk_name.endswith(filtering)
if SYSTEM == "MacOS" and disk.mountpoint == "/private/var/vm":
continue
try:
disk_u = psutil.disk_usage(disk.mountpoint)
except:
pass
u_percent = round(disk_u.percent)
cls.disks[disk.device] = {}
cls.disks[disk.device]["name"] = disk_name
cls.disks[disk.device]["used_percent"] = u_percent
cls.disks[disk.device]["free_percent"] = 100 - u_percent
for name in ["total", "used", "free"]:
cls.disks[disk.device][name] = floating_humanizer(getattr(disk_u, name, 0))
#* Collect disk io
try:
if SYSTEM == "Linux":
dev_name = os.path.realpath(disk.device).rsplit('/', 1)[-1]
if dev_name.startswith("md"):
try:
dev_name = dev_name[:dev_name.index("p")]
except:
pass
disk_io = io_counters[dev_name]
elif disk.mountpoint == "/":
disk_io = io_counters
else:
raise Exception
disk_read = round((disk_io.read_bytes - cls.disk_hist[disk.device][0]) / (time() - cls.timestamp))
disk_write = round((disk_io.write_bytes - cls.disk_hist[disk.device][1]) / (time() - cls.timestamp))
2020-07-18 01:16:01 +00:00
except:
pass
disk_read = disk_write = 0
if disk_io:
cls.disk_hist[disk.device] = (disk_io.read_bytes, disk_io.write_bytes)
if MemBox.disks_width > 30:
if disk_read > 0:
io_string += f'{floating_humanizer(disk_read, short=True)} '
if disk_write > 0:
io_string += f'{floating_humanizer(disk_write, short=True)}'
elif disk_read + disk_write > 0:
io_string += f'▼▲{floating_humanizer(disk_read + disk_write, short=True)}'
cls.disks[disk.device]["io"] = io_string
if CONFIG.swap_disk and MemBox.swap_on:
2020-07-18 01:16:01 +00:00
cls.disks["__swap"] = {}
cls.disks["__swap"]["name"] = "swap"
cls.disks["__swap"]["used_percent"] = cls.swap_percent["used"]
cls.disks["__swap"]["free_percent"] = cls.swap_percent["free"]
for name in ["total", "used", "free"]:
cls.disks["__swap"][name] = cls.swap_string[name]
cls.disks["__swap"]["io"] = ""
if len(cls.disks) > 2:
try:
new = { list(cls.disks)[0] : cls.disks.pop(list(cls.disks)[0])}
new["__swap"] = cls.disks.pop("__swap")
new.update(cls.disks)
cls.disks = new
except:
pass
if disk_list != cls.old_disks:
MemBox.redraw = True
cls.old_disks = disk_list.copy()
cls.timestamp = time()
2020-07-18 01:16:01 +00:00
@classmethod
def _draw(cls):
MemBox._draw_fg()
2020-07-09 00:56:03 +00:00
class NetCollector(Collector):
2020-07-24 01:44:11 +00:00
'''Collects network stats'''
buffer: str = NetBox.buffer
nics: List[str] = []
nic_i: int = 0
nic: str = ""
new_nic: str = ""
2020-07-24 01:44:11 +00:00
reset: bool = False
graph_raise: Dict[str, int] = {"download" : 5, "upload" : 5}
graph_lower: Dict[str, int] = {"download" : 5, "upload" : 5}
min_top: int = 10<<10
#* Stats structure = stats[netword device][download, upload][total, last, top, graph_top, offset, speed, redraw, graph_raise, graph_low] = int, List[int], bool
stats: Dict[str, Dict[str, Dict[str, Any]]] = {}
#* Strings structure strings[network device][download, upload][total, byte_ps, bit_ps, top, graph_top] = str
strings: Dict[str, Dict[str, Dict[str, str]]] = {}
switched: bool = False
timestamp: float = time()
2020-07-09 00:56:03 +00:00
@classmethod
def _get_nics(cls):
'''Get a list of all network devices sorted by highest throughput'''
cls.nic_i = 0
cls.nic = ""
io_all = psutil.net_io_counters(pernic=True)
if not io_all: return
up_stat = psutil.net_if_stats()
for nic in sorted(psutil.net_if_addrs(), key=lambda nic: (io_all[nic].bytes_recv + io_all[nic].bytes_sent), reverse=True):
if nic not in up_stat or not up_stat[nic].isup:
continue
cls.nics.append(nic)
if not cls.nics: cls.nics = [""]
cls.nic = cls.nics[cls.nic_i]
@classmethod
def switch(cls, key: str):
if len(cls.nics) < 2: return
cls.nic_i += +1 if key == "n" else -1
if cls.nic_i >= len(cls.nics): cls.nic_i = 0
elif cls.nic_i < 0: cls.nic_i = len(cls.nics) - 1
cls.new_nic = cls.nics[cls.nic_i]
cls.switched = True
2020-07-27 01:13:13 +00:00
Collector.collect(NetCollector, redraw=True)
@classmethod
def _collect(cls):
speed: int
stat: Dict
up_stat = psutil.net_if_stats()
if cls.switched:
cls.nic = cls.new_nic
cls.switched = False
if not cls.nic or cls.nic not in up_stat or not up_stat[cls.nic].isup:
cls._get_nics()
if not cls.nic: return
try:
io_all = psutil.net_io_counters(pernic=True)[cls.nic]
except KeyError:
pass
return
if not cls.nic in cls.stats:
cls.stats[cls.nic] = {}
cls.strings[cls.nic] = { "download" : {}, "upload" : {}}
for direction, value in ["download", io_all.bytes_recv], ["upload", io_all.bytes_sent]:
cls.stats[cls.nic][direction] = { "total" : value, "last" : value, "top" : 0, "graph_top" : cls.min_top, "offset" : 0, "speed" : [], "redraw" : True, "graph_raise" : 5, "graph_lower" : 5 }
for v in ["total", "byte_ps", "bit_ps", "top", "graph_top"]:
cls.strings[cls.nic][direction][v] = ""
cls.stats[cls.nic]["download"]["total"] = io_all.bytes_recv
cls.stats[cls.nic]["upload"]["total"] = io_all.bytes_sent
for direction in ["download", "upload"]:
stat = cls.stats[cls.nic][direction]
strings = cls.strings[cls.nic][direction]
#* Calculate current speed
stat["speed"].append(round((stat["total"] - stat["last"]) / (time() - cls.timestamp)))
stat["last"] = stat["total"]
speed = stat["speed"][-1]
2020-07-24 01:44:11 +00:00
if stat["offset"] and stat["offset"] > stat["total"]:
cls.reset = True
if cls.reset:
if not stat["offset"]:
stat["offset"] = stat["total"]
else:
stat["offset"] = 0
if direction == "upload":
cls.reset = False
NetBox.redraw = True
if len(stat["speed"]) > NetBox.width * 2:
del stat["speed"][0]
2020-07-24 01:44:11 +00:00
strings["total"] = floating_humanizer(stat["total"] - stat["offset"])
strings["byte_ps"] = floating_humanizer(stat["speed"][-1], per_second=True)
strings["bit_ps"] = floating_humanizer(stat["speed"][-1], bit=True, per_second=True)
if speed > stat["top"] or not stat["top"]:
stat["top"] = speed
strings["top"] = floating_humanizer(stat["top"], bit=True, per_second=True)
if speed > stat["graph_top"]:
stat["graph_raise"] += 1
if stat["graph_lower"] > 0: stat["graph_lower"] -= 1
elif stat["graph_top"] > cls.min_top and speed < stat["graph_top"] // 10:
stat["graph_lower"] += 1
if stat["graph_raise"] > 0: stat["graph_raise"] -= 1
if stat["graph_raise"] >= 5 or stat["graph_lower"] >= 5:
if stat["graph_raise"] >= 5:
stat["graph_top"] = round(max(stat["speed"][-10:]) / 0.8)
elif stat["graph_lower"] >= 5:
stat["graph_top"] = max(stat["speed"][-10:]) * 3
if stat["graph_top"] < cls.min_top: stat["graph_top"] = cls.min_top
stat["graph_raise"] = 0
stat["graph_lower"] = 0
stat["redraw"] = True
strings["graph_top"] = floating_humanizer(stat["graph_top"], short=True)
cls.timestamp = time()
2020-07-18 01:16:01 +00:00
2020-07-09 00:56:03 +00:00
@classmethod
def _draw(cls):
NetBox._draw_fg()
2020-07-24 01:44:11 +00:00
class ProcCollector(Collector): #! add interrupt on _collect and _draw
'''Collects process stats'''
buffer: str = ProcBox.buffer
search_filter: str = ""
processes: Dict = {}
num_procs: int = 0
2020-07-27 01:13:13 +00:00
detailed: bool = False
details: Dict[str, Union[str, int, float, NamedTuple]] = {}
details_cpu: List[int] = []
2020-07-24 01:44:11 +00:00
proc_dict: Dict = {}
p_values: List[str] = ["pid", "name", "cmdline", "num_threads", "username", "memory_percent", "cpu_percent", "cpu_times", "create_time"]
sort_expr: Dict = {}
sort_expr["pid"] = compile("p.info['pid']", "str", "eval")
sort_expr["program"] = compile("p.info['name']", "str", "eval")
sort_expr["arguments"] = compile("' '.join(str(p.info['cmdline'])) or p.info['name']", "str", "eval")
sort_expr["threads"] = compile("str(p.info['num_threads'])", "str", "eval")
sort_expr["user"] = compile("p.info['username']", "str", "eval")
sort_expr["memory"] = compile("str(p.info['memory_percent'])", "str", "eval")
sort_expr["cpu lazy"] = compile("(sum(p.info['cpu_times'][:2] if not p.info['cpu_times'] == 0.0 else [0.0, 0.0]) * 1000 / (time() - p.info['create_time']))", "str", "eval")
sort_expr["cpu responsive"] = compile("(p.info['cpu_percent'] if CONFIG.proc_per_core else (p.info['cpu_percent'] / THREADS))", "str", "eval")
@classmethod
def _collect(cls):
'''List all processess with pid, name, arguments, threads, username, memory percent and cpu percent'''
out: Dict = {}
sorting: str = CONFIG.proc_sorting
reverse: bool = not CONFIG.proc_reversed
proc_per_cpu: bool = CONFIG.proc_per_core
search: str = cls.search_filter
err: float = 0.0
n: int = 0
2020-07-27 01:13:13 +00:00
if cls.detailed and not cls.details.get("killed", False):
try:
det = psutil.Process(ProcBox.selected_pid)
except (psutil.NoSuchProcess, psutil.ZombieProcess):
cls.details["killed"] = True
cls.details["status"] = psutil.STATUS_DEAD
else:
cls.details = det.as_dict(attrs=["pid", "name", "username", "status", "cmdline", "memory_info", "memory_percent", "create_time", "num_threads", "cpu_percent", "cpu_num"], ad_value="")
if det.parent() != None: cls.details["parent_name"] = det.parent().name()
else: cls.details["parent_name"] = ""
cls.details["killed"] = False
if isinstance(cls.details["cmdline"], list): cls.details["cmdline"] = " ".join(cls.details["cmdline"]) or f'[{cls.details["name"]}]'
if hasattr(cls.details["memory_info"], "rss"): cls.details["memory_bytes"] = floating_humanizer(cls.details["memory_info"].rss) # type: ignore
else: cls.details["memory_bytes"] = "? Bytes"
if isinstance(cls.details["create_time"], float): cls.details["uptime"] = f'{timedelta(seconds=round(time()-cls.details["create_time"],0))}'
else: cls.details["uptime"] = "??:??:??"
for v in ["cpu_percent", "memory_percent"]:
if isinstance(cls.details[v], float):
if cls.details[v] >= 100: cls.details[v] = round(cls.details[v]) # type: ignore
elif cls.details[v] >= 10: cls.details[v] = round(cls.details[v], 1) # type: ignore
else: cls.details[v] = round(cls.details[v], 2) # type: ignore
else:
cls.details[v] = 0.0
cls.details_cpu.append(round(cls.details["cpu_percent"])) # type: ignore
if len(cls.details_cpu) > ProcBox.width: del cls.details_cpu[0]
2020-07-24 01:44:11 +00:00
if CONFIG.proc_tree and sorting == "arguments":
sorting = "program"
sort_cmd = cls.sort_expr[sorting]
if CONFIG.proc_tree:
cls._tree(sort_cmd=sort_cmd, reverse=reverse, proc_per_cpu=proc_per_cpu, search=search)
return
for p in sorted(psutil.process_iter(cls.p_values, err), key=lambda p: eval(sort_cmd), reverse=reverse):
2020-07-27 01:13:13 +00:00
if cls.collect_interrupt or cls.proc_interrupt:
2020-07-24 01:44:11 +00:00
return
if p.info["name"] == "idle" or p.info["name"] == err or p.info["pid"] == err:
continue
if p.info["cmdline"] == err:
p.info["cmdline"] = ""
if p.info["username"] == err:
p.info["username"] = ""
if p.info["num_threads"] == err:
p.info["num_threads"] = 0
if search:
for value in [ p.info["name"], " ".join(p.info["cmdline"]), str(p.info["pid"]), p.info["username"] ]:
for s in search.split(","):
if s.strip() in value:
break
else: continue
break
else: continue
cpu = p.info["cpu_percent"] if proc_per_cpu else (p.info["cpu_percent"] / psutil.cpu_count())
mem = p.info["memory_percent"]
cmd = " ".join(p.info["cmdline"]) or "[" + p.info["name"] + "]"
out[p.info["pid"]] = {
"name" : p.info["name"],
"cmd" : cmd,
"threads" : p.info["num_threads"],
"username" : p.info["username"],
"mem" : mem,
"cpu" : cpu }
n += 1
cls.num_procs = n
cls.processes = out.copy()
@classmethod
def _tree(cls, sort_cmd, reverse: bool, proc_per_cpu: bool, search: str):
'''List all processess in a tree view with pid, name, threads, username, memory percent and cpu percent'''
out: Dict = {}
err: float = 0.0
infolist: Dict = {}
tree = defaultdict(list)
n: int = 0
for p in sorted(psutil.process_iter(cls.p_values, err), key=lambda p: eval(sort_cmd), reverse=reverse):
if cls.collect_interrupt: return
try:
tree[p.ppid()].append(p.pid)
except (psutil.NoSuchProcess, psutil.ZombieProcess):
pass
else:
infolist[p.pid] = p.info
n += 1
if 0 in tree and 0 in tree[0]:
tree[0].remove(0)
def create_tree(pid: int, tree: defaultdict, indent: str = "", inindent: str = " ", found: bool = False):
nonlocal infolist, proc_per_cpu, search, out
name: str; threads: int; username: str; mem: float; cpu: float
cont: bool = True
getinfo: Union[Dict, None]
if cls.collect_interrupt: return
try:
name = psutil.Process(pid).name()
if name == "idle": return
except psutil.Error:
pass
cont = False
2020-07-27 01:13:13 +00:00
name = ""
2020-07-24 01:44:11 +00:00
if pid in infolist:
getinfo = infolist[pid]
else:
getinfo = None
if search and not found:
for value in [ name, str(pid), getinfo["username"] if getinfo else "" ]:
for s in search.split(","):
if s.strip() in value:
found = True
break
else: continue
break
else: cont = False
if cont:
if getinfo:
if getinfo["num_threads"] == err: threads = 0
else: threads = getinfo["num_threads"]
if getinfo["username"] == err: username = ""
else: username = getinfo["username"]
cpu = getinfo["cpu_percent"] if proc_per_cpu else (getinfo["cpu_percent"] / psutil.cpu_count())
mem = p.info["memory_percent"]
else:
threads = 0
username = ""
mem = cpu = 0.0
out[pid] = {
"indent" : inindent,
"name": name,
"threads" : threads,
"username" : username,
"mem" : mem,
"cpu" : cpu }
if pid not in tree:
return
children = tree[pid][:-1]
for child in children:
create_tree(child, tree, indent + "", indent + " ├─ ", found=found)
child = tree[pid][-1]
create_tree(child, tree, indent + " ", indent + " └─ ")
create_tree(min(tree), tree)
if cls.collect_interrupt: return
2020-07-27 01:13:13 +00:00
cls.num_procs = len(out)
2020-07-24 01:44:11 +00:00
cls.processes = out.copy()
@classmethod
def sorting(cls, key: str):
2020-07-27 01:13:13 +00:00
index: int = CONFIG.sorting_options.index(CONFIG.proc_sorting) + (1 if key == "right" else -1)
if index >= len(CONFIG.sorting_options): index = 0
elif index < 0: index = len(CONFIG.sorting_options) - 1
CONFIG.proc_sorting = CONFIG.sorting_options[index]
Collector.collect(ProcCollector, interrupt=True, redraw=True)
2020-07-24 01:44:11 +00:00
@classmethod
def _draw(cls):
ProcBox._draw_fg()
class Menu:
2020-07-24 01:44:11 +00:00
'''Holds all menus'''
2020-07-16 00:51:55 +00:00
active: bool = False
2020-07-24 01:44:11 +00:00
close: bool = False
resized: bool = True
menus: Dict[str, Dict[str, str]] = {}
menu_length: Dict[str, int] = {}
for name, menu in MENUS.items():
menu_length[name] = len(menu["normal"][0])
menus[name] = {}
for sel in ["normal", "selected"]:
menus[name][sel] = ""
for i in range(len(menu[sel])):
menus[name][sel] += Fx.trans(f'{Color.fg(MENU_COLORS[sel][i])}{menu[sel][i]}')
if i < len(menu[sel]) - 1: menus[name][sel] += f'{Mv.d(1)}{Mv.l(len(menu[sel][i]))}'
@classmethod
def main(cls):
out: str = ""
banner: str = ""
redraw: bool = True
key: str = ""
mx: int = 0
my: int = 0
skip: bool = False
mouse_over: bool = False
mouse_items: Dict[str, Dict[str, int]] = {}
cls.active = True
cls.resized = True
menu_names: List[str] = list(cls.menus.keys())
menu_index: int = 0
menu_current: str = menu_names[0]
background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
while not cls.close:
key = ""
if cls.resized:
banner = (f'{Banner.draw(Term.height // 2 - 10, center=True)}{Mv.d(1)}{Mv.l(46)}{Colors.black_bg}{Colors.default}{Fx.b}← esc'
f'{Mv.r(30)}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}')
cy = 0
for name, menu in cls.menus.items():
ypos = Term.height // 2 - 2 + cy
xpos = Term.width // 2 - (cls.menu_length[name] // 2)
mouse_items[name] = { "x1" : xpos, "x2" : xpos + cls.menu_length[name] - 1, "y1" : ypos, "y2" : ypos + 2 }
cy += 3
redraw = True
cls.resized = False
if redraw:
out = ""
for name, menu in cls.menus.items():
out += f'{Mv.to(mouse_items[name]["y1"], mouse_items[name]["x1"])}{menu["selected" if name == menu_current else "normal"]}'
if skip and redraw:
Draw.now(out)
elif not skip:
Draw.now(f'{background}{banner}{out}')
skip = redraw = False
if Key.has_key() or Key.input_wait(Timer.left(), mouse=True):
if Key.mouse_moved():
mx, my = Key.get_mouse()
for name, pos in mouse_items.items():
if mx >= pos["x1"] and mx <= pos["x2"] and my >= pos["y1"] and my <= pos["y2"]:
mouse_over = True
if name != menu_current:
menu_current = name
menu_index = menu_names.index(name)
redraw = True
break
else:
mouse_over = False
else:
key = Key.get()
if key == "mouse_click" and not mouse_over:
key = "m"
if key == "q":
clean_quit()
elif key in ["escape", "m"]:
cls.close = True
break
elif key in ["up", "mouse_scroll_up", "shift_tab"]:
menu_index -= 1
if menu_index < 0: menu_index = len(menu_names) - 1
menu_current = menu_names[menu_index]
redraw = True
elif key in ["down", "mouse_scroll_down", "tab"]:
menu_index += 1
if menu_index > len(menu_names) - 1: menu_index = 0
menu_current = menu_names[menu_index]
redraw = True
elif key == "enter" or (key == "mouse_click" and mouse_over):
if menu_current == "quit":
clean_quit()
if Timer.not_zero() and not cls.resized:
skip = True
else:
Collector.collect()
Collector.collect_done.wait(1)
background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
Timer.stamp()
Draw.now(f'{Draw.saved_buffer()}')
cls.active = False
cls.close = False
2020-07-16 00:51:55 +00:00
class Timer:
timestamp: float
2020-07-24 01:44:11 +00:00
return_zero = False
@classmethod
def stamp(cls):
cls.timestamp = time()
@classmethod
def not_zero(cls) -> bool:
2020-07-24 01:44:11 +00:00
if cls.return_zero:
cls.return_zero = False
return False
if cls.timestamp + (CONFIG.update_ms / 1000) > time():
return True
else:
return False
@classmethod
def left(cls) -> float:
return cls.timestamp + (CONFIG.update_ms / 1000) - time()
@classmethod
def finish(cls):
2020-07-24 01:44:11 +00:00
cls.return_zero = True
cls.timestamp = time() - (CONFIG.update_ms / 1000)
2020-07-24 01:44:11 +00:00
Key.break_wait()
2020-07-09 00:56:03 +00:00
#? Functions ------------------------------------------------------------------------------------->
def get_cpu_name() -> str:
'''Fetch a suitable CPU identifier from the CPU model name string'''
name: str = ""
nlist: List = []
command: str = ""
cmd_out: str = ""
rem_line: str = ""
if SYSTEM == "Linux":
command = "cat /proc/cpuinfo"
rem_line = "model name"
elif SYSTEM == "MacOS":
command ="sysctl -n machdep.cpu.brand_string"
elif SYSTEM == "BSD":
command ="sysctl hw.model"
rem_line = "hw.model"
try:
cmd_out = subprocess.check_output("LANG=C " + command, shell=True, universal_newlines=True)
except:
pass
2020-07-09 00:56:03 +00:00
if rem_line:
for line in cmd_out.split("\n"):
if rem_line in line:
name = re.sub( ".*" + rem_line + ".*:", "", line,1).lstrip()
else:
name = cmd_out
nlist = name.split(" ")
if "Xeon" in name:
name = nlist[nlist.index("CPU")+1]
elif "Ryzen" in name:
name = " ".join(nlist[nlist.index("Ryzen"):nlist.index("Ryzen")+3])
elif "CPU" in name:
name = nlist[nlist.index("CPU")-1]
return name
def create_box(x: int = 0, y: int = 0, width: int = 0, height: int = 0, title: str = "", title2: str = "", line_color: Color = None, title_color: Color = None, fill: bool = True, box = None) -> str:
2020-07-09 00:56:03 +00:00
'''Create a box from a box object or by given arguments'''
out: str = f'{Term.fg}{Term.bg}'
if not line_color: line_color = THEME.div_line
if not title_color: title_color = THEME.title
2020-07-09 00:56:03 +00:00
#* Get values from box class if given
if box:
x = box.x
y = box.y
width = box.width
height =box.height
title = box.name
vlines: Tuple[int, int] = (x, x + width - 1)
hlines: Tuple[int, int] = (y, y + height - 1)
#* Fill box if enabled
if fill:
for i in range(y + 1, y + height - 1):
out += f'{Mv.to(i, x)}{" " * (width - 1)}'
out += f'{line_color}'
#* Draw all horizontal lines
for hpos in hlines:
out += f'{Mv.to(hpos, x)}{Symbol.h_line * (width - 1)}'
#* Draw all vertical lines
for vpos in vlines:
for hpos in range(y, y + height - 1):
out += f'{Mv.to(hpos, vpos)}{Symbol.v_line}'
#* Draw corners
out += f'{Mv.to(y, x)}{Symbol.left_up}\
{Mv.to(y, x + width - 1)}{Symbol.right_up}\
{Mv.to(y + height - 1, x)}{Symbol.left_down}\
{Mv.to(y + height - 1, x + width - 1)}{Symbol.right_down}'
#* Draw titles if enabled
if title:
out += f'{Mv.to(y, x + 2)}{Symbol.title_left}{title_color}{Fx.b}{title}{Fx.ub}{line_color}{Symbol.title_right}'
2020-07-09 00:56:03 +00:00
if title2:
out += f'{Mv.to(y + height - 1, x + 2)}{Symbol.title_left}{title_color}{Fx.b}{title2}{Fx.ub}{line_color}{Symbol.title_right}'
2020-07-09 00:56:03 +00:00
return f'{out}{Term.fg}{Mv.to(y + 1, x + 1)}'
def now_sleeping(signum, frame):
"""Reset terminal settings and stop background input read before putting to sleep"""
Key.stop()
Collector.stop()
2020-07-24 01:44:11 +00:00
Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off)
2020-07-09 00:56:03 +00:00
Term.echo(True)
os.kill(os.getpid(), signal.SIGSTOP)
def now_awake(signum, frame):
"""Set terminal settings and restart background input read"""
Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor, Term.mouse_on)
2020-07-09 00:56:03 +00:00
Term.echo(False)
Key.start()
Term.refresh()
Box.calc_sizes()
Box.draw_bg()
2020-07-09 00:56:03 +00:00
Collector.start()
#Draw.out()
2020-07-09 00:56:03 +00:00
def quit_sigint(signum, frame):
"""SIGINT redirection to clean_quit()"""
clean_quit()
2020-07-16 00:51:55 +00:00
def clean_quit(errcode: int = 0, errmsg: str = "", thread: bool = False):
2020-07-09 00:56:03 +00:00
"""Stop background input read, save current config and reset terminal settings before quitting"""
2020-07-16 00:51:55 +00:00
global THREAD_ERROR
if thread:
THREAD_ERROR = errcode
interrupt_main()
return
if THREAD_ERROR: errcode = THREAD_ERROR
2020-07-09 00:56:03 +00:00
Key.stop()
Collector.stop()
if not errcode: CONFIG.save_config()
2020-07-24 01:44:11 +00:00
Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off)
2020-07-09 00:56:03 +00:00
Term.echo(True)
if errcode == 0:
errlog.info(f'Exiting. Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n')
else:
errlog.warning(f'Exiting with errorcode ({errcode}). Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n')
2020-07-18 01:16:01 +00:00
if not errmsg: errmsg = f'Bpytop exited with errorcode ({errcode}). See {CONFIG_DIR}/error.log for more information!'
2020-07-09 00:56:03 +00:00
if errmsg: print(errmsg)
2020-07-16 00:51:55 +00:00
2020-07-09 00:56:03 +00:00
raise SystemExit(errcode)
def floating_humanizer(value: Union[float, int], bit: bool = False, per_second: bool = False, start: int = 0, short: bool = False) -> str:
'''Scales up in steps of 1024 to highest possible unit and returns string with unit suffixed
* bit=True or defaults to bytes
* start=int to set 1024 multiplier starting unit
* short=True always returns 0 decimals and shortens unit to 1 character
'''
out: str = ""
unit: Tuple[str, ...] = UNITS["bit"] if bit else UNITS["byte"]
selector: int = start if start else 0
mult: int = 8 if bit else 1
if value <= 0: value = 0
if isinstance(value, float): value = round(value * 100 * mult)
elif value > 0: value *= 100 * mult
2020-07-27 01:13:13 +00:00
else: value = 0
2020-07-09 00:56:03 +00:00
while len(f'{value}') > 5 and value >= 102400:
value >>= 10
if value < 100:
out = f'{value}'
break
selector += 1
else:
if len(f'{value}') < 5 and len(f'{value}') >= 2 and selector > 0:
decimals = 5 - len(f'{value}')
out = f'{value}'[:-2] + "." + f'{value}'[-decimals:]
elif len(f'{value}') >= 2:
out = f'{value}'[:-2]
else:
out = f'{value}'
2020-07-09 00:56:03 +00:00
if short: out = out.split(".")[0]
out += f'{"" if short else " "}{unit[selector][0] if short else unit[selector]}'
if per_second: out += "ps" if bit else "/s"
2020-07-09 00:56:03 +00:00
return out
def process_keys():
2020-07-27 01:13:13 +00:00
mouse_pos: Tuple[int, int] = (0, 0)
filtered: bool = False
while Key.has_key():
key = Key.get()
2020-07-27 01:13:13 +00:00
if key in ["mouse_scroll_up", "mouse_scroll_down", "mouse_click"]:
2020-07-24 01:44:11 +00:00
mouse_pos = Key.get_mouse()
2020-07-27 01:13:13 +00:00
if mouse_pos[0] >= ProcBox.x and mouse_pos[1] >= ProcBox.current_y:
2020-07-24 01:44:11 +00:00
pass
2020-07-27 01:13:13 +00:00
elif key == "mouse_click":
key = "mouse_unselect"
else:
key = "_null"
if ProcBox.filtering:
if key in ["enter", "mouse_click", "mouse_unselect"]:
ProcBox.filtering = False
Collector.collect(ProcCollector, redraw=True, only_draw=True)
continue
elif key in ["escape", "delete"]:
ProcCollector.search_filter = ""
ProcBox.filtering = False
elif len(key) == 1:
ProcCollector.search_filter += key
elif key == "backspace" and len(ProcCollector.search_filter) > 0:
ProcCollector.search_filter = ProcCollector.search_filter[:-1]
else:
continue
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True)
if filtered: Collector.collect_done.wait(0.1)
filtered = True
continue
2020-07-24 01:44:11 +00:00
if key == "_null":
continue
elif key == "q":
clean_quit()
2020-07-24 01:44:11 +00:00
elif key == "+" and CONFIG.update_ms + 100 <= 86399900:
CONFIG.update_ms += 100
Box.draw_update_ms()
2020-07-24 01:44:11 +00:00
elif key == "-" and CONFIG.update_ms - 100 >= 100:
CONFIG.update_ms -= 100
Box.draw_update_ms()
2020-07-24 01:44:11 +00:00
elif key in ["b", "n"]:
NetCollector.switch(key)
2020-07-24 01:44:11 +00:00
elif key in ["m", "escape"]:
Menu.main()
elif key == "z":
NetCollector.reset = not NetCollector.reset
Collector.collect(NetCollector)
2020-07-24 01:44:11 +00:00
elif key in ["left", "right"]:
ProcCollector.sorting(key)
elif key == "e":
CONFIG.proc_tree = not CONFIG.proc_tree
Collector.collect(ProcCollector, interrupt=True, redraw=True)
elif key == "r":
CONFIG.proc_reversed = not CONFIG.proc_reversed
Collector.collect(ProcCollector, interrupt=True, redraw=True)
elif key == "c":
CONFIG.proc_colors = not CONFIG.proc_colors
Collector.collect(ProcCollector, redraw=True, only_draw=True)
elif key == "g":
CONFIG.proc_gradient = not CONFIG.proc_gradient
Collector.collect(ProcCollector, redraw=True, only_draw=True)
elif key == "o":
CONFIG.proc_per_core = not CONFIG.proc_per_core
Collector.collect(ProcCollector, interrupt=True, redraw=True)
elif key == "h":
CONFIG.mem_graphs = not CONFIG.mem_graphs
2020-07-27 01:13:13 +00:00
Collector.collect(MemCollector, interrupt=True, redraw=True)
2020-07-24 01:44:11 +00:00
elif key == "p":
CONFIG.swap_disk = not CONFIG.swap_disk
2020-07-27 01:13:13 +00:00
Collector.collect(MemCollector, interrupt=True, redraw=True)
elif key == "f":
ProcBox.filtering = True
if not ProcCollector.search_filter: ProcBox.start = 0
Collector.collect(ProcCollector, redraw=True, only_draw=True)
elif key == "delete" and ProcCollector.search_filter:
ProcCollector.search_filter = ""
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True)
elif key == "enter" and ProcBox.selected > 0:
if ProcCollector.details.get("pid", None) == ProcBox.selected_pid:
ProcCollector.detailed = False
elif psutil.pid_exists(ProcBox.selected_pid):
ProcCollector.detailed = True
else:
continue
ProcCollector.details = {}
ProcCollector.details_cpu = []
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True)
2020-07-09 00:56:03 +00:00
2020-07-27 01:13:13 +00:00
elif key in ["up", "down", "mouse_scroll_up", "mouse_scroll_down", "page_up", "page_down", "home", "end", "mouse_click", "mouse_unselect"]:
ProcBox.selector(key, mouse_pos)
2020-07-24 01:44:11 +00:00
2020-07-09 00:56:03 +00:00
#? Pre main -------------------------------------------------------------------------------------->
2020-07-09 00:56:03 +00:00
CPU_NAME: str = get_cpu_name()
2020-07-09 00:56:03 +00:00
if __name__ == "__main__":
#? Init -------------------------------------------------------------------------------------->
if DEBUG: TimeIt.start("Init")
class Init:
running: bool = True
initbg_colors: List[str] = []
initbg_data: List[int]
initbg_up: Graph
initbg_down: Graph
resized = False
@staticmethod
def fail(err):
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Symbol.fail}')
sleep(2)
errlog.exception(f'{err}')
clean_quit(1, errmsg=f'Error during init! See {CONFIG_DIR}/error.log for more information.')
@classmethod
def success(cls, start: bool = False):
if not CONFIG.show_init or cls.resized: return
if start:
Draw.buffer("init", z=1)
Draw.buffer("initbg", z=10)
for i in range(51):
for _ in range(2): cls.initbg_colors.append(Color.fg(i, i, i))
2020-07-27 01:13:13 +00:00
Draw.buffer("banner", (f'{Banner.draw(Term.height // 2 - 10, center=True)}{Mv.d(1)}{Mv.l(11)}{Colors.black_bg}{Colors.default}'
f'{Fx.b}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}{Color.fg("#50")}'), z=2)
for _i in range(7):
perc = f'{str(round((_i + 1) * 14 + 2)) + "%":>5}'
2020-07-27 01:13:13 +00:00
Draw.buffer("+banner", f'{Mv.to(Term.height // 2 - 2 + _i, Term.width // 2 - 28)}{Fx.trans(perc)}{Symbol.v_line}')
Draw.out("banner")
2020-07-27 01:13:13 +00:00
Draw.buffer("+init!", f'{Color.fg("#cc")}{Fx.b}{Mv.to(Term.height // 2 - 2, Term.width // 2 - 21)}{Mv.save}')
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
2020-07-27 01:13:13 +00:00
cls.draw_bg(10)
2020-07-18 01:16:01 +00:00
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(x)}{Mv.to(Term.height // 2, 0)}{cls.initbg_down(x)}')
Draw.out("initbg", "banner", "init")
@classmethod
def done(cls):
cls.running = False
if not CONFIG.show_init: return
if cls.resized:
Draw.now(Term.clear)
else:
2020-07-27 01:13:13 +00:00
cls.draw_bg(20)
Draw.clear("initbg", "banner", "init", saved=True)
del cls.initbg_up, cls.initbg_down, cls.initbg_data, cls.initbg_colors
2020-07-09 00:56:03 +00:00
2020-07-27 01:13:13 +00:00
#? Switch to alternate screen, clear screen, hide cursor, enable mouse reporting and disable input echo
Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor, Term.mouse_on)
2020-07-09 00:56:03 +00:00
Term.echo(False)
Term.refresh()
2020-07-09 00:56:03 +00:00
#? Draw banner and init status
if CONFIG.show_init:
Init.success(start=True)
2020-07-09 00:56:03 +00:00
#? Load theme
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Loading theme and creating colors... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
THEME: Theme = Theme(CONFIG.color_theme)
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Setup boxes
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Doing some maths and drawing... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
Box.calc_sizes()
Box.draw_bg(now=False)
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Setup signal handlers for SIGSTP, SIGCONT, SIGINT and SIGWINCH
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Setting up signal handlers... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
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:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Start a separate thread for reading keyboard input
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Starting input reader thread... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
Key.start()
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Start a separate thread for data collection and drawing
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Starting data collection and drawer thread... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
Collector.start()
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Collect data and draw to buffer
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Collecting data and drawing... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
Collector.collect(draw_now=False)
2020-07-09 00:56:03 +00:00
pass
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
#? Draw to screen
if CONFIG.show_init:
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Finishing up... ")}{Mv.save}')
2020-07-09 00:56:03 +00:00
try:
Collector.collect_done.wait()
2020-07-09 00:56:03 +00:00
except Exception as e:
Init.fail(e)
2020-07-09 00:56:03 +00:00
else:
Init.success()
2020-07-09 00:56:03 +00:00
Init.done()
Term.refresh()
Draw.out(clear=True)
if DEBUG: TimeIt.stop("Init")
2020-07-09 00:56:03 +00:00
2020-07-27 01:13:13 +00:00
#? Main loop ------------------------------------------------------------------------------------->
def main():
while not False:
Term.refresh()
Timer.stamp()
while Timer.not_zero():
if Key.input_wait(Timer.left()) and not Menu.active:
process_keys()
Collector.collect()
2020-07-09 00:56:03 +00:00
#? Start main loop
2020-07-27 01:13:13 +00:00
try:
main()
except Exception as e:
errlog.exception(f'{e}')
clean_quit(1)
2020-07-09 00:56:03 +00:00
else:
#? Quit cleanly even if false starts being true...
clean_quit()