mirror of https://github.com/aristocratos/bpytop
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4400 lines
160 KiB
4400 lines
160 KiB
#!/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
|
|
import urllib.request
|
|
from time import time, sleep, strftime, localtime
|
|
from datetime import timedelta
|
|
from _thread import interrupt_main
|
|
from collections import defaultdict
|
|
from select import select
|
|
from distutils.util import strtobool
|
|
from string import Template
|
|
from math import ceil, floor
|
|
from random import randint
|
|
from shutil import which
|
|
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple
|
|
|
|
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)
|
|
|
|
VERSION: str = "1.0.8"
|
|
|
|
#? Argument parser ------------------------------------------------------------------------------->
|
|
if len(sys.argv) > 1:
|
|
for arg in sys.argv[1:]:
|
|
if not arg in ["-m", "--mini", "-v", "--version", "-h", "--help", "--debug"]:
|
|
print(f'Unrecognized argument: {arg}\n'
|
|
f'Use argument -h or --help for help')
|
|
raise SystemExit(1)
|
|
|
|
if "-h" in sys.argv or "--help" in sys.argv:
|
|
print(f'USAGE: {sys.argv[0]} [argument]\n\n'
|
|
f'Arguments:\n'
|
|
f' -m, --mini Start in minimal mode without memory and net boxes\n'
|
|
f' -v, --version Show version info and exit\n'
|
|
f' -h, --help Show this help message and exit\n'
|
|
f' --debug Start with loglevel set to DEBUG overriding value set in config\n'
|
|
)
|
|
raise SystemExit(0)
|
|
elif "-v" in sys.argv or "--version" in sys.argv:
|
|
print(f'bpytop version: {VERSION}\n'
|
|
f'psutil version: {".".join(str(x) for x in psutil.version_info)}')
|
|
raise SystemExit(0)
|
|
|
|
|
|
#? Variables ------------------------------------------------------------------------------------->
|
|
|
|
BANNER_SRC: List[Tuple[str, str, str]] = [
|
|
("#ffa50a", "#0fd7ff", "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗"),
|
|
("#f09800", "#00bfe6", "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗"),
|
|
("#db8b00", "#00a6c7", "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝"),
|
|
("#c27b00", "#008ca8", "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ "),
|
|
("#a86b00", "#006e85", "██████╔╝██║ ██║ ██║ ╚██████╔╝██║"),
|
|
("#000000", "#000000", "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝"),
|
|
]
|
|
|
|
#*?This is the template used to create the config file
|
|
DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + '''
|
|
|
|
#* Color theme, looks for a .theme file in "/usr/[local/]share/bpytop/themes" and "~/.config/bpytop/themes", "Default" for builtin default theme.
|
|
#* Prefix name by a plus sign (+) for a theme located in user themes folder, i.e. color_theme="+monokai"
|
|
color_theme="$color_theme"
|
|
|
|
#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs.
|
|
update_ms=$update_ms
|
|
|
|
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive",
|
|
#* "cpu lazy" updates top process over time, "cpu responsive" updates top process directly.
|
|
proc_sorting="$proc_sorting"
|
|
|
|
#* Reverse sorting order, True or False.
|
|
proc_reversed=$proc_reversed
|
|
|
|
#* Show processes as a tree
|
|
proc_tree=$proc_tree
|
|
|
|
#* 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
|
|
|
|
#* Show process memory as bytes instead of percent
|
|
proc_mem_bytes=$proc_mem_bytes
|
|
|
|
#* Check cpu temperature, needs "vcgencmd" on Raspberry Pi and "osx-cpu-temp" on MacOS X.
|
|
check_temp=$check_temp
|
|
|
|
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
|
|
draw_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.
|
|
background_update=$background_update
|
|
|
|
#* Custom cpu model name, empty string to disable.
|
|
custom_cpu_name="$custom_cpu_name"
|
|
|
|
#* 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"
|
|
disks_filter="$disks_filter"
|
|
|
|
#* 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
|
|
|
|
#* Minimum value the network graphs scales down to, default "10K" = 10 KibiBytes, possible units "K" (KiB), "M" (MiB), "G" (GiB), no unit for bytes.
|
|
net_download_min="$net_download_min"
|
|
net_upload_min="$net_upload_min"
|
|
|
|
#* Start in network graphs auto rescaling mode, ignores any values set above and rescale down to default value "10K".
|
|
net_auto_min=$net_auto_min
|
|
|
|
#* 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.
|
|
update_check=$update_check
|
|
|
|
#* Enable start in mini mode, can be toggled with shift+m at any time.
|
|
mini_mode=$mini_mode
|
|
|
|
#* 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
|
|
''')
|
|
|
|
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')
|
|
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 = ""
|
|
for td in ["local/", ""]:
|
|
if os.path.isdir(f'/usr/{td}share/bpytop/themes'):
|
|
THEME_DIR = f'/usr/{td}share/bpytop/themes'
|
|
break
|
|
USER_THEME_DIR: str = f'{CONFIG_DIR}/themes'
|
|
|
|
CORES: int = psutil.cpu_count(logical=False) or 1
|
|
THREADS: int = psutil.cpu_count(logical=True) or 1
|
|
|
|
THREAD_ERROR: int = 0
|
|
|
|
if "--debug" in sys.argv:
|
|
DEBUG = True
|
|
else:
|
|
DEBUG = False
|
|
|
|
DEFAULT_THEME: Dict[str, str] = {
|
|
"main_bg" : "",
|
|
"main_fg" : "#cc",
|
|
"title" : "#ee",
|
|
"hi_fg" : "#969696",
|
|
"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"
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
#? 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 -------------------------------------------------------------->
|
|
|
|
class TimeIt:
|
|
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):
|
|
def timed(*args, **kw):
|
|
ts = time()
|
|
out = func(*args, **kw)
|
|
errlog.debug(f'{func.__name__} completed in {time() - ts:.6f} seconds')
|
|
return out
|
|
return timed
|
|
|
|
#? Set up config class and load config ----------------------------------------------------------->
|
|
|
|
class Config:
|
|
'''Holds all config variables and functions for loading from and saving to disk'''
|
|
keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_colors", "proc_gradient", "proc_per_core", "proc_mem_bytes",
|
|
"disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "net_download_min", "net_upload_min", "net_auto_min", "show_init", "mini_mode"]
|
|
conf_dict: Dict[str, Union[str, int, bool]] = {}
|
|
color_theme: str = "Default"
|
|
update_ms: int = 2000
|
|
proc_sorting: str = "cpu lazy"
|
|
proc_reversed: bool = False
|
|
proc_tree: bool = False
|
|
proc_colors: bool = True
|
|
proc_gradient: bool = True
|
|
proc_per_core: bool = False
|
|
proc_mem_bytes: bool = True
|
|
check_temp: bool = True
|
|
draw_clock: str = "%X"
|
|
background_update: bool = True
|
|
custom_cpu_name: str = ""
|
|
disks_filter: str = ""
|
|
update_check: bool = True
|
|
mem_graphs: bool = True
|
|
show_swap: bool = True
|
|
swap_disk: bool = True
|
|
show_disks: bool = True
|
|
net_download_min: str = "10K"
|
|
net_upload_min: str = "10K"
|
|
net_auto_min: bool = False
|
|
show_init: bool = True
|
|
mini_mode: bool = False
|
|
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"]
|
|
|
|
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!')
|
|
elif conf["version"] != VERSION:
|
|
self.recreate = True
|
|
self.info.append(f'Config file version and bpytop version missmatch, will be recreated on exit!')
|
|
for key in self.keys:
|
|
if key in conf.keys() and conf[key] != "_error_":
|
|
setattr(self, key, conf[key])
|
|
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]] = {}
|
|
conf_file: str = ""
|
|
if os.path.isfile(self.config_file):
|
|
conf_file = self.config_file
|
|
elif os.path.isfile("/etc/bpytop.conf"):
|
|
conf_file = "/etc/bpytop.conf"
|
|
else:
|
|
return new_config
|
|
try:
|
|
with open(conf_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):
|
|
line = line.replace(key + "=", "")
|
|
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!')
|
|
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!')
|
|
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:
|
|
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!')
|
|
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()}')
|
|
errlog.info(f'Loglevel set to {CONFIG.log_level}')
|
|
errlog.debug(f'Using psutil version {".".join(str(x) for x in psutil.version_info)}')
|
|
errlog.debug(f'CMD: {" ".join(sys.argv)}')
|
|
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 = []
|
|
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
|
|
resized: bool = False
|
|
_w : int = 0
|
|
_h : int = 0
|
|
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()
|
|
|
|
@classmethod
|
|
def refresh(cls, *args, force: bool = False):
|
|
"""Update width, height and set resized flag if terminal has been resized"""
|
|
if cls.resized: cls.winch.set(); return
|
|
cls._w, cls._h = os.get_terminal_size()
|
|
if (cls._w, cls._h) == (cls.width, cls.height) and not force: return
|
|
if force: Collector.collect_interrupt = True
|
|
while (cls._w, cls._h) != (cls.width, cls.height) or (cls._w < 80 or cls._h < 24):
|
|
if Init.running: Init.resized = True
|
|
CpuBox.clock_block = True
|
|
cls.resized = True
|
|
Collector.collect_interrupt = True
|
|
cls.width, cls.height = cls._w, cls._h
|
|
Draw.now(Term.clear)
|
|
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}')
|
|
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()
|
|
cls._w, cls._h = os.get_terminal_size()
|
|
|
|
Key.mouse = {}
|
|
Box.calc_sizes()
|
|
if Init.running: cls.resized = False; return
|
|
if Menu.active: Menu.resized = True
|
|
Box.draw_bg(now=False)
|
|
cls.resized = False
|
|
Timer.finish()
|
|
|
|
@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)
|
|
|
|
@staticmethod
|
|
def title(text: str = "") -> str:
|
|
if text: text = f' {text}'
|
|
return f'\033]0;{os.environ.get("TERMINAL_TITLE", "")}{text}\a'
|
|
|
|
class Fx:
|
|
"""Text effects
|
|
* 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 ."""
|
|
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
|
|
|
|
#* Precompiled regex for finding a 24-bit color escape sequence in a string
|
|
color_re = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m")
|
|
|
|
@staticmethod
|
|
def trans(string: str):
|
|
return string.replace(" ", "\033[1C")
|
|
|
|
@classmethod
|
|
def uncolor(cls, string: str) -> str:
|
|
return f'{cls.color_re.sub("", string)}'
|
|
|
|
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'
|
|
|
|
save: str = "\033[s" #* Save cursor position
|
|
restore: str = "\033[u" #* Restore saved cursor postion
|
|
t = to
|
|
r = right
|
|
l = left
|
|
u = up
|
|
d = down
|
|
|
|
class Key:
|
|
"""Handles the threaded input reader for keypresses and mouse events"""
|
|
list: List[str] = []
|
|
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",
|
|
"\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"
|
|
}
|
|
new = threading.Event()
|
|
idle = threading.Event()
|
|
mouse_move = threading.Event()
|
|
mouse_report: bool = False
|
|
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 ""
|
|
|
|
@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 = []
|
|
|
|
@classmethod
|
|
def input_wait(cls, sec: float = 0.0, mouse: bool = False) -> bool:
|
|
'''Returns True if key is detected else waits out timer and returns False'''
|
|
if cls.list: return True
|
|
if mouse: Draw.now(Term.mouse_direct_on)
|
|
cls.new.wait(sec if sec > 0 else 0.0)
|
|
if mouse: Draw.now(Term.mouse_direct_off, Term.mouse_on)
|
|
|
|
if cls.new.is_set():
|
|
cls.new.clear()
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@classmethod
|
|
def break_wait(cls):
|
|
cls.list.append("_null")
|
|
cls.new.set()
|
|
sleep(0.01)
|
|
cls.new.clear()
|
|
|
|
@classmethod
|
|
def _get_key(cls):
|
|
"""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."""
|
|
input_key: str = ""
|
|
clean_key: str = ""
|
|
try:
|
|
while not cls.stopping:
|
|
with Raw(sys.stdin):
|
|
if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input on stdin then restart loop to check for stop flag
|
|
continue
|
|
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)
|
|
if input_key.startswith("\033[<"):
|
|
_ = 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:
|
|
cls.mouse_pos = (int(input_key.split(";")[1]), int(input_key.split(";")[2].rstrip("mM")))
|
|
except:
|
|
pass
|
|
else:
|
|
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
|
|
else:
|
|
for code in cls.escape.keys(): #* Go trough dict of escape codes to get the cleaned key name
|
|
if input_key.lstrip("\033").startswith(code):
|
|
clean_key = cls.escape[code]
|
|
break
|
|
else: #* If not found in escape dict and length of key is 1, assume regular character
|
|
if len(input_key) == 1:
|
|
clean_key = input_key
|
|
if clean_key:
|
|
cls.list.append(clean_key) #* Store up to 10 keys in input queue for later processing
|
|
if len(cls.list) > 10: del cls.list[0]
|
|
clean_key = ""
|
|
cls.new.set() #* Set threading event to interrupt main thread sleep
|
|
input_key = ""
|
|
|
|
|
|
except Exception as e:
|
|
errlog.exception(f'Input thread failed with exception: {e}')
|
|
cls.idle.set()
|
|
cls.list.clear()
|
|
clean_quit(1, thread=True)
|
|
|
|
class Draw:
|
|
'''Holds the draw buffer and manages IO blocking queue
|
|
* .buffer([+]name[!], *args, append=False, now=False, z=100) : Add *args to buffer
|
|
* - Adding "+" prefix to name sets append to True and appends to name's current string
|
|
* - Adding "!" suffix to name sets now to True and print name's current string
|
|
* .out(clear=False) : Print all strings in buffer, clear=True clear all buffers after
|
|
* .now(*args) : Prints all arguments as a string
|
|
* .clear(*names) : Clear named buffers, all if no argument
|
|
* .last_screen() : Prints all saved buffers
|
|
'''
|
|
strings: Dict[str, str] = {}
|
|
z_order: Dict[str, int] = {}
|
|
saved: Dict[str, str] = {}
|
|
save: Dict[str, bool] = {}
|
|
once: Dict[str, bool] = {}
|
|
idle = threading.Event()
|
|
idle.set()
|
|
|
|
@classmethod
|
|
def now(cls, *args):
|
|
'''Wait for input reader and self to be idle then print to screen'''
|
|
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
|
|
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):
|
|
string: str = ""
|
|
if name.startswith("+"):
|
|
name = name.lstrip("+")
|
|
append = True
|
|
if name.endswith("!"):
|
|
name = name.rstrip("!")
|
|
now = True
|
|
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
|
|
if args: string = "".join(args)
|
|
if only_save:
|
|
if name not in cls.saved or not append: cls.saved[name] = ""
|
|
cls.saved[name] += string
|
|
else:
|
|
if name not in cls.strings or not append: cls.strings[name] = ""
|
|
cls.strings[name] += string
|
|
if now:
|
|
cls.out(name)
|
|
|
|
@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):
|
|
if name in names and name in cls.strings:
|
|
out += cls.strings[name]
|
|
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]
|
|
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
|
|
def saved_buffer(cls) -> str:
|
|
out: str = ""
|
|
for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True):
|
|
if name in cls.saved:
|
|
out += cls.saved[name]
|
|
return out
|
|
|
|
|
|
@classmethod
|
|
def clear(cls, *names, saved: bool = False):
|
|
if names:
|
|
for name in names:
|
|
if name in cls.strings:
|
|
del cls.strings[name]
|
|
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]
|
|
else:
|
|
cls.strings = {}
|
|
cls.save = {}
|
|
cls.once = {}
|
|
if saved:
|
|
cls.saved = {}
|
|
cls.z_order = {}
|
|
|
|
class Color:
|
|
'''Holds representations for a 24-bit color value
|
|
__init__(color, depth="fg", default=False)
|
|
-- color accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit hexadecimal: string "#FF" or decimal RGB "255 255 255" as a string.
|
|
-- depth accepts "fg" or "bg"
|
|
__call__(*args) joins str arguments to a string and apply color
|
|
__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
|
|
|
|
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:
|
|
if len(args) < 1: return ""
|
|
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:
|
|
if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="fg")
|
|
else: return cls.escape_color(hexa=args[0], depth="fg")
|
|
|
|
@classmethod
|
|
def bg(cls, *args) -> str:
|
|
if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="bg")
|
|
else: return cls.escape_color(hexa=args[0], depth="bg")
|
|
|
|
class Colors:
|
|
'''Standard colors for menus and dialogs'''
|
|
default = Color("#cc")
|
|
white = Color("#ff")
|
|
red = Color("#bf3636")
|
|
green = Color("#68bf36")
|
|
blue = Color("#0fd7ff")
|
|
yellow = Color("#db8b00")
|
|
black_bg = Color("#00", depth="bg")
|
|
null = Color("")
|
|
|
|
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" : [],
|
|
"upload" : [],
|
|
"proc" : [],
|
|
"proc_color" : []
|
|
}
|
|
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!')
|
|
theme = "Default"
|
|
CONFIG.color_theme = theme
|
|
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
|
|
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
|
|
|
|
rgb: Dict[str, Tuple[int, int, int]]
|
|
colors: List[List[int]] = []
|
|
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')) ]
|
|
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)]]
|
|
if r == 100:
|
|
break
|
|
self.gradient[name] += [ Color.fg(*color) for color in colors ]
|
|
|
|
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)
|
|
|
|
@classmethod
|
|
def refresh(cls):
|
|
'''Sets themes dict with names and paths to all found themes'''
|
|
cls.themes = { "Default" : "Default" }
|
|
try:
|
|
for d in (THEME_DIR, USER_THEME_DIR):
|
|
if not d: continue
|
|
for f in os.listdir(d):
|
|
if f.endswith(".theme"):
|
|
cls.themes[f'{"" 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):
|
|
if len(line) > length: length = len(line)
|
|
out_var = ""
|
|
line_color = Color.fg(color)
|
|
line_color2 = Color.fg(color2)
|
|
line_dark = Color.fg(f'#{80 - num * 6}')
|
|
for n, letter in enumerate(line):
|
|
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
|
|
elif letter == " ":
|
|
letter = f'{Mv.r(1)}'
|
|
c_color = ""
|
|
elif letter != "█" and c_color != line_dark:
|
|
c_color = line_dark
|
|
out_var += line_dark
|
|
out_var += letter
|
|
out.append(out_var)
|
|
|
|
@classmethod
|
|
def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False):
|
|
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] = {
|
|
0.0 : " ", 0.1 : "⢀", 0.2 : "⢠", 0.3 : "⢰", 0.4 : "⢸",
|
|
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 : "⣿"
|
|
}
|
|
graph_up_small = graph_up.copy()
|
|
graph_up_small[0.0] = "\033[1C"
|
|
|
|
graph_down: Dict[float, str] = {
|
|
0.0 : " ", 0.1 : "⠈", 0.2 : "⠘", 0.3 : "⠸", 0.4 : "⢸",
|
|
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 : "⣿"
|
|
}
|
|
graph_down_small = graph_down.copy()
|
|
graph_down_small[0.0] = "\033[1C"
|
|
meter: str = "■"
|
|
up: str = "↑"
|
|
down: str = "↓"
|
|
left: str = "←"
|
|
right: str = "→"
|
|
enter: 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
|
|
* __call__ : same as add
|
|
'''
|
|
out: str
|
|
width: int
|
|
height: int
|
|
graphs: Dict[bool, List[str]]
|
|
colors: List[str]
|
|
invert: bool
|
|
max_value: int
|
|
color_max_value: int
|
|
offset: int
|
|
current: bool
|
|
last: int
|
|
symbol: Dict[float, str]
|
|
|
|
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, color_max_value: int = None):
|
|
self.graphs: Dict[bool, List[str]] = {False : [], True : []}
|
|
self.current: bool = True
|
|
self.width = width
|
|
self.height = height
|
|
self.invert = invert
|
|
self.offset = offset
|
|
if not data: data = [0]
|
|
if max_value:
|
|
self.max_value = max_value
|
|
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
|
|
if color_max_value:
|
|
self.color_max_value = color_max_value
|
|
else:
|
|
self.color_max_value = self.max_value
|
|
if self.color_max_value and self.max_value:
|
|
color_scale = int(100.0 * self.max_value / self.color_max_value)
|
|
else:
|
|
color_scale = 100
|
|
self.colors: List[str] = []
|
|
if isinstance(color, list) and height > 1:
|
|
for i in range(1, height + 1): self.colors.insert(0, color[min(100, i * color_scale // height)]) #* Calculate colors of graph
|
|
if invert: self.colors.reverse()
|
|
elif isinstance(color, Color) and height > 1:
|
|
self.colors = [ f'{color}' for _ in range(height) ]
|
|
else:
|
|
if isinstance(color, list): self.colors = color
|
|
elif isinstance(color, Color): self.colors = [ f'{color}' for _ in range(101) ]
|
|
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
|
|
filler = self.symbol[0.0] * (width - value_width)
|
|
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)]
|
|
if data: self.last = data[-1]
|
|
self.out = ""
|
|
|
|
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]}'
|
|
if self.colors: self.out += f'{Term.fg}'
|
|
|
|
def __call__(self, value: Union[int, None] = None) -> str:
|
|
if not isinstance(value, int): return self.out
|
|
self.current = not self.current
|
|
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
|
|
|
|
def add(self, value: Union[int, None] = None) -> str:
|
|
return self.__call__(value)
|
|
|
|
def __str__(self):
|
|
return self.out
|
|
|
|
def __repr__(self):
|
|
return repr(self.out)
|
|
|
|
|
|
class Graphs:
|
|
'''Holds all graphs and lists of graphs for dynamically created graphs'''
|
|
cpu: Dict[str, Graph] = {}
|
|
cores: List[Graph] = [NotImplemented] * THREADS
|
|
temps: List[Graph] = [NotImplemented] * (THREADS + 1)
|
|
net: Dict[str, Graph] = {}
|
|
detailed_cpu: Graph = NotImplemented
|
|
detailed_mem: Graph = NotImplemented
|
|
pid_cpu: Dict[int, Graph] = {}
|
|
|
|
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
|
|
color_gradient: List[str]
|
|
color_inactive: Color
|
|
gradient_name: str
|
|
width: int
|
|
saved: Dict[int, str]
|
|
|
|
def __init__(self, value: int, width: int, gradient_name: str):
|
|
self.gradient_name = gradient_name
|
|
self.color_gradient = THEME.gradient[gradient_name]
|
|
self.color_inactive = THEME.inactive_fg
|
|
self.width = width
|
|
self.saved = {}
|
|
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:
|
|
self.out = self.saved[value]
|
|
else:
|
|
self.out = self._create(value)
|
|
return self.out
|
|
|
|
def __str__(self) -> str:
|
|
return self.out
|
|
|
|
def __repr__(self):
|
|
return repr(self.out)
|
|
|
|
def _create(self, value: int) -> str:
|
|
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
|
|
return out
|
|
|
|
class Meters:
|
|
cpu: Meter
|
|
mem: Dict[str, Union[Meter, Graph]] = {}
|
|
swap: Dict[str, Union[Meter, Graph]] = {}
|
|
disks_used: Dict[str, Meter] = {}
|
|
disks_free: Dict[str, Meter] = {}
|
|
|
|
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
|
|
mini_mode: bool = True if CONFIG.mini_mode or "-m" in sys.argv or "--mini" in sys.argv else False
|
|
out: str
|
|
bg: str
|
|
_b_cpu_h: int
|
|
_b_mem_h: int
|
|
redraw_all: bool
|
|
buffers: List[str] = []
|
|
clock_on: bool = False
|
|
clock: str = ""
|
|
resized: bool = False
|
|
|
|
@classmethod
|
|
def calc_sizes(cls):
|
|
'''Calculate sizes of boxes'''
|
|
for sub in cls.__subclasses__():
|
|
sub._calc_size() # type: ignore
|
|
sub.resized = True # type: ignore
|
|
|
|
@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
|
|
if not "+" in Key.mouse:
|
|
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("+")} ',
|
|
f'{THEME.title(update_string)} {THEME.hi_fg("-")}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}', only_save=Menu.active, once=True)
|
|
if now and not Menu.active: Draw.clear("update_ms")
|
|
|
|
@classmethod
|
|
def draw_clock(cls, force: bool = False):
|
|
if force: pass
|
|
elif not cls.clock_on or Term.resized or strftime(CONFIG.draw_clock) == cls.clock: return
|
|
cls.clock = strftime(CONFIG.draw_clock)
|
|
clock_len = len(cls.clock[:(CpuBox.width-58)])
|
|
now: bool = False if Menu.active else not force
|
|
Draw.buffer("clock", (f'{Mv.to(CpuBox.y, ((CpuBox.width-2)//2)-(clock_len//2)-5)}{Fx.ub}{THEME.cpu_box}{Symbol.h_line * 4}'
|
|
f'{Symbol.title_left}{Fx.b}{THEME.title(cls.clock[:clock_len])}{Fx.ub}{THEME.cpu_box}{Symbol.title_right}{Symbol.h_line * 4}{Term.fg}'),
|
|
z=1, now=now, once=not force, only_save=Menu.active)
|
|
|
|
@classmethod
|
|
def draw_bg(cls, now: bool = True):
|
|
'''Draw all boxes outlines and titles'''
|
|
Draw.buffer("bg", "".join(sub._draw_bg() for sub in cls.__subclasses__()), now=now, z=1000, only_save=Menu.active, once=True) # type: ignore
|
|
cls.draw_update_ms(now=now)
|
|
if CONFIG.draw_clock: cls.draw_clock(force=True)
|
|
|
|
class SubBox:
|
|
box_x: int = 0
|
|
box_y: int = 0
|
|
box_width: int = 0
|
|
box_height: int = 0
|
|
box_columns: int = 0
|
|
column_size: int = 0
|
|
|
|
class CpuBox(Box, SubBox):
|
|
name = "cpu"
|
|
x = 1
|
|
y = 1
|
|
height_p = 32
|
|
width_p = 100
|
|
resized: bool = True
|
|
redraw: bool = False
|
|
buffer: str = "cpu"
|
|
clock_block: bool = True
|
|
Box.buffers.append(buffer)
|
|
|
|
@classmethod
|
|
def _calc_size(cls):
|
|
cpu = CpuCollector
|
|
height_p: int
|
|
if cls.mini_mode: height_p = 20
|
|
else: height_p = cls.height_p
|
|
cls.width = round(Term.width * cls.width_p / 100)
|
|
cls.height = round(Term.height * height_p / 100)
|
|
if cls.height < 8: cls.height = 8
|
|
Box._b_cpu_h = cls.height
|
|
#THREADS = 10
|
|
cls.box_columns = ceil((THREADS + 1) / (cls.height - 5))
|
|
if cls.box_columns * (20 + 13 if cpu.got_sensors else 21) < cls.width - (cls.width // 4):
|
|
cls.column_size = 2
|
|
cls.box_width = (20 + 13 if cpu.got_sensors else 21) * cls.box_columns - ((cls.box_columns - 1) * 1)
|
|
elif cls.box_columns * (15 + 6 if cpu.got_sensors else 15) < cls.width - (cls.width // 4):
|
|
cls.column_size = 1
|
|
cls.box_width = (15 + 6 if cpu.got_sensors else 15) * cls.box_columns - ((cls.box_columns - 1) * 1)
|
|
elif cls.box_columns * (8 + 6 if cpu.got_sensors else 8) < cls.width - (cls.width // 4):
|
|
cls.column_size = 0
|
|
else:
|
|
cls.box_columns = (cls.width - cls.width // 4) // (8 + 6 if cpu.got_sensors else 8); cls.column_size = 0
|
|
|
|
if cls.column_size == 0: cls.box_width = (8 + 6 if cpu.got_sensors else 8) * cls.box_columns + 1
|
|
|
|
cls.box_height = ceil(THREADS / cls.box_columns) + 4
|
|
|
|
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
|
|
|
|
@classmethod
|
|
def _draw_bg(cls) -> str:
|
|
if not "M" in Key.mouse:
|
|
Key.mouse["M"] = [[cls.x + 10 + i, cls.y] for i in range(6)]
|
|
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[:cls.box_width - 14] if not CONFIG.custom_cpu_name else CONFIG.custom_cpu_name[:cls.box_width - 14])}')
|
|
|
|
@classmethod
|
|
def _draw_fg(cls):
|
|
cpu = CpuCollector
|
|
if cpu.redraw: cls.redraw = True
|
|
out: str = ""
|
|
out_misc: 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)
|
|
|
|
if cls.resized or cls.redraw:
|
|
if not "m" in Key.mouse:
|
|
Key.mouse["m"] = [[cls.x + 16 + i, cls.y] for i in range(6)]
|
|
out_misc += f'{Mv.to(cls.y, cls.x + 16)}{THEME.cpu_box(Symbol.title_left)}{Fx.b if Box.mini_mode else ""}{THEME.hi_fg("m")}{THEME.title("ini")}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}'
|
|
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)
|
|
Meters.cpu = Meter(cpu.cpu_usage[0][-1], bw - (21 if cpu.got_sensors else 9), "cpu")
|
|
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 cpu.got_sensors:
|
|
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)
|
|
Draw.buffer("cpu_misc", out_misc, only_save=True)
|
|
|
|
cx = cy = cc = 0
|
|
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])}'
|
|
f'{THEME.main_fg}{Mv.to(by + cy, bx + cx)}{Fx.b}{"CPU "}{Fx.ub}{Meters.cpu(cpu.cpu_usage[0][-1])}'
|
|
f'{THEME.gradient["cpu"][cpu.cpu_usage[0][-1]]}{cpu.cpu_usage[0][-1]:>4}{THEME.main_fg}%')
|
|
if cpu.got_sensors:
|
|
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])}'
|
|
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)}{Fx.b + "C" + Fx.ub if THREADS < 100 else ""}{str(n):<{2 if cls.column_size == 0 else 3}}'
|
|
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])}'
|
|
else:
|
|
out += f'{THEME.gradient["cpu"][cpu.cpu_usage[n][-1]]}'
|
|
out += f'{cpu.cpu_usage[n][-1]:>{3 if cls.column_size < 2 else 4}}{THEME.main_fg}%'
|
|
if cpu.got_sensors:
|
|
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])}'
|
|
else:
|
|
out += f'{THEME.gradient["temp"][cpu.cpu_temp[n][-1]]}'
|
|
out += f'{cpu.cpu_temp[n][-1]:>4}{THEME.main_fg}°C'
|
|
out += f'{THEME.div_line(Symbol.v_line)}'
|
|
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 cpu.got_sensors:
|
|
lavg = f' Load AVG: {" ".join(str(l) for l in cpu.load_avg):^19.19}'
|
|
elif cls.column_size == 2 or (cls.column_size == 1 and cpu.got_sensors):
|
|
lavg = f'LAV: {" ".join(str(l) for l in cpu.load_avg):^14.14}'
|
|
elif cls.column_size == 1 or (cls.column_size == 0 and cpu.got_sensors):
|
|
lavg = f'L {" ".join(str(round(l, 1)) for l in cpu.load_avg):^11.11}'
|
|
else:
|
|
lavg = f'{" ".join(str(round(l, 1)) for l in cpu.load_avg[:2]):^7.7}'
|
|
out += f'{Mv.to(by + cy, bx + cx)}{THEME.main_fg}{lavg}{THEME.div_line(Symbol.v_line)}'
|
|
|
|
out += f'{Mv.to(y + h - 1, x + 1)}{THEME.inactive_fg}up {cpu.uptime}'
|
|
|
|
|
|
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
|
|
cls.resized = cls.redraw = cls.clock_block = False
|
|
|
|
class MemBox(Box):
|
|
name = "mem"
|
|
height_p = 40
|
|
width_p = 45
|
|
x = 1
|
|
y = 1
|
|
mem_meter: int = 0
|
|
mem_size: int = 0
|
|
disk_meter: int = 0
|
|
divider: int = 0
|
|
mem_width: int = 0
|
|
disks_width: int = 0
|
|
graph_height: int
|
|
resized: bool = True
|
|
redraw: bool = False
|
|
buffer: str = "mem"
|
|
swap_on: bool = CONFIG.show_swap
|
|
Box.buffers.append(buffer)
|
|
mem_names: List[str] = ["used", "available", "cached", "free"]
|
|
swap_names: List[str] = ["used", "free"]
|
|
|
|
@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
|
|
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
|
|
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)
|
|
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
|
|
|
|
@classmethod
|
|
def _draw_bg(cls) -> str:
|
|
if cls.mini_mode: return ""
|
|
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):
|
|
if cls.mini_mode: return
|
|
mem = MemCollector
|
|
if mem.redraw: cls.redraw = True
|
|
out: str = ""
|
|
out_misc: str = ""
|
|
gbg: str = ""
|
|
gmv: str = ""
|
|
gli: str = ""
|
|
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
|
|
if cls.resized or cls.redraw:
|
|
cls._calc_size()
|
|
out_misc += cls._draw_bg()
|
|
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 and CONFIG.show_disks:
|
|
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
|
|
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")
|
|
if not "g" in Key.mouse:
|
|
Key.mouse["g"] = [[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.hi_fg("g")}{THEME.title("raph")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}')
|
|
if CONFIG.show_disks:
|
|
if not "s" in Key.mouse:
|
|
Key.mouse["s"] = [[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.hi_fg("s")}{THEME.title("wap")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}')
|
|
|
|
|
|
Draw.buffer("mem_misc", out_misc, only_save=True)
|
|
|
|
#* Mem
|
|
cx = 1; cy = 1
|
|
|
|
out += f'{Mv.to(y, x+1)}{THEME.title}{Fx.b}Total:{mem.string["total"]:>{cls.mem_width - 9}}{Fx.ub}{THEME.main_fg}'
|
|
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
|
|
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
|
|
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
|
|
#* 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
|
|
|
|
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
|
|
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
|
|
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
|
|
|
|
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)}'
|
|
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
|
|
|
|
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
|
|
if len(mem.disks) * 4 <= h + 1: cy += 1
|
|
|
|
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
|
|
cls.resized = cls.redraw = False
|
|
|
|
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" : "▲"}
|
|
buffer: str = "net"
|
|
|
|
Box.buffers.append(buffer)
|
|
|
|
@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
|
|
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
|
|
|
|
@classmethod
|
|
def _draw_bg(cls) -> str:
|
|
if cls.mini_mode: return ""
|
|
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):
|
|
if cls.mini_mode: return
|
|
net = NetCollector
|
|
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
|
|
reset: bool = bool(net.stats[net.nic]["download"]["offset"])
|
|
|
|
if cls.resized or cls.redraw:
|
|
out_misc += cls._draw_bg()
|
|
if not "b" in Key.mouse:
|
|
Key.mouse["b"] = [[x+w - len(net.nic[:10]) - 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[:10]) - 14 + i, y-1] for i in range(4)]
|
|
|
|
|
|
out_misc += (f'{Mv.to(y-1, x+w - 25)}{THEME.net_box}{Symbol.h_line * (10 - len(net.nic[:10]))}{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}')
|
|
if w - len(net.nic[:10]) - 20 > 6:
|
|
if not "a" in Key.mouse: Key.mouse["a"] = [[x+w - 20 - len(net.nic[:10]) + i, y-1] for i in range(4)]
|
|
out_misc += (f'{Mv.to(y-1, x+w - 21 - len(net.nic[:10]))}{THEME.net_box(Symbol.title_left)}{Fx.b if net.auto_min else ""}{THEME.hi_fg("a")}{THEME.title("uto")}'
|
|
f'{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]
|
|
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, color_max_value=stats["top"])
|
|
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])}'
|
|
|
|
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:
|
|
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"])}'
|
|
|
|
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
|
|
cls.redraw = cls.resized = False
|
|
|
|
class ProcBox(Box):
|
|
name = "proc"
|
|
height_p = 68
|
|
width_p = 55
|
|
x = 1
|
|
y = 1
|
|
current_y: int = 0
|
|
current_h: int = 0
|
|
select_max: int = 0
|
|
selected: int = 0
|
|
selected_pid: int = 0
|
|
last_selection: int = 0
|
|
filtering: bool = False
|
|
moved: bool = False
|
|
start: int = 1
|
|
count: int = 0
|
|
s_len: int = 0
|
|
detailed: bool = False
|
|
detailed_x: int = 0
|
|
detailed_y: int = 0
|
|
detailed_width: int = 0
|
|
detailed_height: int = 8
|
|
resized: bool = True
|
|
redraw: bool = True
|
|
buffer: str = "proc"
|
|
pid_counter: Dict[int, int] = {}
|
|
Box.buffers.append(buffer)
|
|
|
|
@classmethod
|
|
def _calc_size(cls):
|
|
width_p: int; height_p: int
|
|
if cls.mini_mode:
|
|
width_p, height_p = 100, 80
|
|
else:
|
|
width_p, height_p = cls.width_p, cls.height_p
|
|
cls.width = round(Term.width * width_p / 100)
|
|
cls.height = round(Term.height * height_p / 100)
|
|
if cls.height + Box._b_cpu_h > Term.height: cls.height = Term.height - Box._b_cpu_h
|
|
cls.x = Term.width - cls.width + 1
|
|
cls.y = Box._b_cpu_h + 1
|
|
cls.current_y = cls.y
|
|
cls.current_h = cls.height
|
|
cls.select_max = cls.height - 3
|
|
cls.redraw = True
|
|
cls.resized = True
|
|
|
|
@classmethod
|
|
def _draw_bg(cls) -> str:
|
|
return create_box(box=cls, line_color=THEME.proc_box)
|
|
|
|
@classmethod
|
|
def selector(cls, key: str, mouse_pos: Tuple[int, int] = (0, 0)):
|
|
old: Tuple[int, int] = (cls.start, cls.selected)
|
|
new_sel: int
|
|
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 == 0 and ProcCollector.detailed and cls.last_selection:
|
|
cls.selected = cls.last_selection
|
|
cls.last_selection = 0
|
|
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
|
|
elif key == "mouse_click":
|
|
if mouse_pos[0] > cls.x + cls.width - 4 and mouse_pos[1] > cls.current_y + 1 and mouse_pos[1] < cls.current_y + 1 + cls.select_max + 1:
|
|
if mouse_pos[1] == cls.current_y + 2:
|
|
cls.start = 1
|
|
elif mouse_pos[1] == cls.current_y + 1 + cls.select_max:
|
|
cls.start = ProcCollector.num_procs - cls.select_max + 1
|
|
else:
|
|
cls.start = round((mouse_pos[1] - cls.current_y) * ((ProcCollector.num_procs - cls.select_max - 2) / (cls.select_max - 2)))
|
|
else:
|
|
new_sel = mouse_pos[1] - cls.current_y - 1 if mouse_pos[1] >= cls.current_y - 1 else 0
|
|
if new_sel > 0 and new_sel == cls.selected:
|
|
Key.list.insert(0, "enter")
|
|
return
|
|
elif new_sel > 0 and new_sel != cls.selected:
|
|
if cls.last_selection: cls.last_selection = 0
|
|
cls.selected = new_sel
|
|
elif key == "mouse_unselect":
|
|
cls.selected = 0
|
|
|
|
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
|
|
if cls.start < 1: cls.start = 1
|
|
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
|
|
|
|
if old != (cls.start, cls.selected):
|
|
cls.moved = True
|
|
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True, only_draw=True)
|
|
|
|
|
|
@classmethod
|
|
def _draw_fg(cls):
|
|
proc = ProcCollector
|
|
if proc.proc_interrupt: return
|
|
if proc.redraw: cls.redraw = True
|
|
out: str = ""
|
|
out_misc: str = ""
|
|
n: int = 0
|
|
x, y, w, h = cls.x + 1, cls.current_y + 1, cls.width - 2, cls.current_h - 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
|
|
dgx: int; dgw: int; dx: int; dw: int; dy: int
|
|
l_count: int = 0
|
|
scroll_pos: int = 0
|
|
killed: bool = True
|
|
indent: str = ""
|
|
offset: int = 0
|
|
vals: List[str]
|
|
g_color: str = ""
|
|
s_len: int = 0
|
|
if proc.search_filter: s_len = len(proc.search_filter[:10])
|
|
loc_string: str = f'{cls.start + cls.selected - 1}/{proc.num_procs}'
|
|
end: str = ""
|
|
|
|
if proc.detailed:
|
|
dgx, dgw = x, w // 3
|
|
dw = w - dgw - 1
|
|
if dw > 120:
|
|
dw = 120
|
|
dgw = w - 121
|
|
dx = x + dgw + 2
|
|
dy = cls.y + 1
|
|
|
|
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)
|
|
if CONFIG.proc_tree:
|
|
tree_len = arg_len + prog_len + 6
|
|
arg_len = 0
|
|
|
|
#* Buttons and titles only redrawn if needed
|
|
if cls.resized or cls.redraw:
|
|
s_len += len(CONFIG.proc_sorting)
|
|
if cls.resized or s_len != cls.s_len or proc.detailed:
|
|
cls.s_len = s_len
|
|
for k in ["e", "r", "c", "t", "k", "i", "enter", "left"]:
|
|
if k in Key.mouse: del Key.mouse[k]
|
|
if proc.detailed:
|
|
killed = proc.details["killed"]
|
|
main = THEME.main_fg if cls.selected == 0 and not killed else THEME.inactive_fg
|
|
hi = THEME.hi_fg if cls.selected == 0 and not killed else THEME.inactive_fg
|
|
title = THEME.title if cls.selected == 0 and not killed else THEME.inactive_fg
|
|
if cls.current_y != cls.y + 8 or cls.resized or Graphs.detailed_cpu is NotImplemented:
|
|
cls.current_y = cls.y + 8
|
|
cls.current_h = cls.height - 8
|
|
for i in range(7): out_misc += f'{Mv.to(dy+i, x)}{" " * w}'
|
|
out_misc += (f'{Mv.to(dy+7, x-1)}{THEME.proc_box}{Symbol.title_right}{Symbol.h_line*w}{Symbol.title_left}'
|
|
f'{Mv.to(dy+7, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(cls.name)}{Fx.ub}{THEME.proc_box(Symbol.title_right)}{THEME.div_line}')
|
|
for i in range(7):
|
|
out_misc += f'{Mv.to(dy + i, dgx + dgw + 1)}{Symbol.v_line}'
|
|
|
|
out_misc += (f'{Mv.to(dy-1, x-1)}{THEME.proc_box}{Symbol.left_up}{Symbol.h_line*w}{Symbol.right_up}'
|
|
f'{Mv.to(dy-1, dgx + dgw + 1)}{Symbol.div_up}'
|
|
f'{Mv.to(dy-1, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(str(proc.details["pid"]))}{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(proc.details["name"][:(dgw - 11)])}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
|
|
|
|
if cls.selected == 0:
|
|
Key.mouse["enter"] = [[dx+dw-10 + i, dy-1] for i in range(7)]
|
|
if cls.selected == 0 and not killed:
|
|
Key.mouse["t"] = [[dx+2 + i, dy-1] for i in range(9)]
|
|
|
|
out_misc += (f'{Mv.to(dy-1, dx+dw - 11)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{title if cls.selected > 0 else THEME.title}close{Fx.ub} {main if cls.selected > 0 else THEME.main_fg}{Symbol.enter}{THEME.proc_box(Symbol.title_right)}'
|
|
f'{Mv.to(dy-1, dx+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}t{title}erminate{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
|
|
if dw > 28:
|
|
if cls.selected == 0 and not killed and not "k" in Key.mouse: Key.mouse["k"] = [[dx + 13 + i, dy-1] for i in range(4)]
|
|
out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}k{title}ill{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
if dw > 39:
|
|
if cls.selected == 0 and not killed and not "i" in Key.mouse: Key.mouse["i"] = [[dx + 19 + i, dy-1] for i in range(9)]
|
|
out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}i{title}nterrupt{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
|
|
if Graphs.detailed_cpu is NotImplemented or cls.resized:
|
|
Graphs.detailed_cpu = Graph(dgw+1, 7, THEME.gradient["cpu"], proc.details_cpu)
|
|
Graphs.detailed_mem = Graph(dw // 3, 1, None, proc.details_mem)
|
|
|
|
cls.select_max = cls.height - 11
|
|
y = cls.y + 9
|
|
h = cls.height - 10
|
|
|
|
else:
|
|
if cls.current_y != cls.y or cls.resized:
|
|
cls.current_y = cls.y
|
|
cls.current_h = cls.height
|
|
y, h = cls.y + 1, cls.height - 2
|
|
out_misc += (f'{Mv.to(y-1, x-1)}{THEME.proc_box}{Symbol.left_up}{Symbol.h_line*w}{Symbol.right_up}'
|
|
f'{Mv.to(y-1, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(cls.name)}{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
f'{Mv.to(y+7, x-1)}{THEME.proc_box(Symbol.v_line)}{Mv.r(w)}{THEME.proc_box(Symbol.v_line)}')
|
|
cls.select_max = cls.height - 3
|
|
|
|
|
|
sort_pos = x + w - len(CONFIG.proc_sorting) - 7
|
|
if not "left" in Key.mouse:
|
|
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)]
|
|
|
|
|
|
out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.h_line * (w - 9))}' +
|
|
("" if not proc.detailed else f"{Mv.to(dy+7, dgx + dgw + 1)}{THEME.proc_box(Symbol.div_down)}") +
|
|
f'{Mv.to(y-1, sort_pos)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("<")} {THEME.title(CONFIG.proc_sorting)} '
|
|
f'{THEME.hi_fg(">")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
|
|
|
|
|
|
if w > 29 + s_len:
|
|
if not "e" in Key.mouse: Key.mouse["e"] = [[sort_pos - 5 + i, y-1] for i in range(4)]
|
|
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 > 37 + s_len:
|
|
if not "r" in Key.mouse: 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)}')
|
|
if w > 47 + s_len:
|
|
if not "c" in Key.mouse: Key.mouse["c"] = [[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 ""}'
|
|
f'{THEME.title("per-")}{THEME.hi_fg("c")}{THEME.title("ore")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
|
|
# if w > 57 + s_len:
|
|
# if not "G" in Key.mouse: 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 > 65 + s_len:
|
|
# if not "C" in Key.mouse: 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)}')
|
|
|
|
if not "f" in Key.mouse or cls.resized: 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:
|
|
if not "delete" in Key.mouse: 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)}')
|
|
|
|
|
|
main = THEME.inactive_fg if cls.selected == 0 else THEME.main_fg
|
|
hi = THEME.inactive_fg if cls.selected == 0 else THEME.hi_fg
|
|
title = THEME.inactive_fg if cls.selected == 0 else THEME.title
|
|
out_misc += (f'{Mv.to(y+h, x + 1)}{THEME.proc_box}{Symbol.h_line*(w-4)}'
|
|
f'{Mv.to(y+h, x+1)}{THEME.proc_box(Symbol.title_left)}{main}{Symbol.up} {Fx.b}{THEME.main_fg("select")} {Fx.ub}'
|
|
f'{THEME.inactive_fg if cls.selected == cls.select_max else THEME.main_fg}{Symbol.down}{THEME.proc_box(Symbol.title_right)}'
|
|
f'{THEME.proc_box(Symbol.title_left)}{title}{Fx.b}info {Fx.ub}{main}{Symbol.enter}{THEME.proc_box(Symbol.title_right)}')
|
|
if not "enter" in Key.mouse: Key.mouse["enter"] = [[x + 14 + i, y+h] for i in range(6)]
|
|
if w - len(loc_string) > 34:
|
|
if not "t" in Key.mouse: Key.mouse["t"] = [[x + 22 + i, y+h] for i in range(9)]
|
|
out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}t{title}erminate{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
if w - len(loc_string) > 40:
|
|
if not "k" in Key.mouse: Key.mouse["k"] = [[x + 33 + i, y+h] for i in range(4)]
|
|
out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}k{title}ill{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
if w - len(loc_string) > 51:
|
|
if not "i" in Key.mouse: Key.mouse["i"] = [[x + 39 + i, y+h] for i in range(9)]
|
|
out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}i{title}nterrupt{Fx.ub}{THEME.proc_box(Symbol.title_right)}'
|
|
|
|
#* Processes labels
|
|
selected: str = CONFIG.proc_sorting
|
|
label: str
|
|
if selected == "memory": selected = "mem"
|
|
if selected == "threads" and not CONFIG.proc_tree and not arg_len: selected = "tr"
|
|
if CONFIG.proc_tree:
|
|
label = (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 ""))
|
|
if selected in ["program", "arguments"]: selected = "tree"
|
|
else:
|
|
label = (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 "") +
|
|
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 ""))
|
|
if selected == "program" and prog_len <= 8: selected = "prg"
|
|
selected = selected.split(" ")[0].capitalize()
|
|
if CONFIG.proc_mem_bytes: label = label.replace("Mem%", "MemB")
|
|
label = label.replace(selected, f'{Fx.u}{selected}{Fx.uu}')
|
|
out_misc += label
|
|
|
|
Draw.buffer("proc_misc", out_misc, only_save=True)
|
|
|
|
#* Detailed box draw
|
|
if proc.detailed:
|
|
if proc.details["status"] == psutil.STATUS_RUNNING: stat_color = Fx.b
|
|
elif proc.details["status"] in [psutil.STATUS_DEAD, psutil.STATUS_STOPPED, psutil.STATUS_ZOMBIE]: stat_color = THEME.inactive_fg
|
|
else: stat_color = ""
|
|
expand = proc.expand
|
|
iw = (dw - 3) // (4 + expand)
|
|
iw2 = iw - 1
|
|
out += (f'{Mv.to(dy, dgx)}{Graphs.detailed_cpu(None if cls.moved or proc.details["killed"] else proc.details_cpu[-1])}'
|
|
f'{Mv.to(dy, dgx)}{THEME.title}{Fx.b}{0 if proc.details["killed"] else proc.details["cpu_percent"]}%{Mv.r(1)}{"" if SYSTEM == "MacOS" else (("C" if dgw < 20 else "Core") + str(proc.details["cpu_num"]))}')
|
|
for i, l in enumerate(["C", "P", "U"]):
|
|
out += f'{Mv.to(dy+2+i, dgx)}{l}'
|
|
for i, l in enumerate(["C", "M", "D"]):
|
|
out += f'{Mv.to(dy+4+i, dx+1)}{l}'
|
|
out += (f'{Mv.to(dy, dx+1)} {"Status:":^{iw}.{iw2}}{"Elapsed:":^{iw}.{iw2}}' +
|
|
(f'{"Parent:":^{iw}.{iw2}}' if dw > 28 else "") + (f'{"User:":^{iw}.{iw2}}' if dw > 38 else "") +
|
|
(f'{"Threads:":^{iw}.{iw2}}' if expand > 0 else "") + (f'{"Nice:":^{iw}.{iw2}}' if expand > 1 else "") +
|
|
(f'{"IO Read:":^{iw}.{iw2}}' if expand > 2 else "") + (f'{"IO Write:":^{iw}.{iw2}}' if expand > 3 else "") +
|
|
(f'{"TTY:":^{iw}.{iw2}}' if expand > 4 else "") +
|
|
f'{Mv.to(dy+1, dx+1)}{Fx.ub}{THEME.main_fg}{stat_color}{proc.details["status"]:^{iw}.{iw2}}{Fx.ub}{THEME.main_fg}{proc.details["uptime"]:^{iw}.{iw2}} ' +
|
|
(f'{proc.details["parent_name"]:^{iw}.{iw2}}' if dw > 28 else "") + (f'{proc.details["username"]:^{iw}.{iw2}}' if dw > 38 else "") +
|
|
(f'{proc.details["threads"]:^{iw}.{iw2}}' if expand > 0 else "") + (f'{proc.details["nice"]:^{iw}.{iw2}}' if expand > 1 else "") +
|
|
(f'{proc.details["io_read"]:^{iw}.{iw2}}' if expand > 2 else "") + (f'{proc.details["io_write"]:^{iw}.{iw2}}' if expand > 3 else "") +
|
|
(f'{proc.details["terminal"][-(iw2):]:^{iw}.{iw2}}' if expand > 4 else "") +
|
|
f'{Mv.to(dy+3, dx)}{THEME.title}{Fx.b}{("Memory: " if dw > 42 else "M:") + str(round(proc.details["memory_percent"], 1)) + "%":>{dw//3-1}}{Fx.ub} {THEME.inactive_fg}{"⡀"*(dw//3)}'
|
|
f'{Mv.l(dw//3)}{THEME.proc_misc}{Graphs.detailed_mem(None if cls.moved else proc.details_mem[-1])} '
|
|
f'{THEME.title}{Fx.b}{proc.details["memory_bytes"]:.{dw//3 - 2}}{THEME.main_fg}{Fx.ub}')
|
|
cy = dy + (4 if len(proc.details["cmdline"]) > dw - 5 else 5)
|
|
for i in range(ceil(len(proc.details["cmdline"]) / (dw - 5))):
|
|
out += f'{Mv.to(cy+i, dx + 3)}{proc.details["cmdline"][((dw-5)*i):][:(dw-5)]:{"^" if i == 0 else "<"}{dw-5}}'
|
|
if i == 2: break
|
|
|
|
#* Checking for selection out of bounds
|
|
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
|
|
|
|
#* Start iteration over all processes and info
|
|
cy = 1
|
|
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
|
|
|
|
indent, name, cmd, threads, username, mem, mem_b, cpu = [items.get(v, d) for v, d in [("indent", ""), ("name", ""), ("cmd", ""), ("threads", 0), ("username", "?"), ("mem", 0.0), ("mem_b", 0), ("cpu", 0.0)]]
|
|
|
|
if CONFIG.proc_tree:
|
|
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
|
|
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
|
|
|
|
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
|
|
else:
|
|
c_color = m_color = t_color = Fx.b
|
|
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}'
|
|
|
|
#* Creates one line for a process with all gathered information
|
|
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} ') if not CONFIG.proc_mem_bytes else f'{floating_humanizer(mem_b, short=True):>4.4}') + end +
|
|
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 ""))
|
|
|
|
#* Draw small cpu graph for process if cpu usage was above 1% in the last 10 updates
|
|
if pid in Graphs.pid_cpu:
|
|
out += f'{Mv.to(y+cy, x + w - (12 if proc.num_procs > cls.select_max else 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}'
|
|
|
|
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 ""}'
|
|
|
|
cy += 1
|
|
if cy == h: break
|
|
if cy < h:
|
|
for i in range(h-cy):
|
|
out += f'{Mv.to(y+cy+i, x)}{" " * w}'
|
|
|
|
#* Draw scrollbar if needed
|
|
if proc.num_procs > cls.select_max:
|
|
if cls.resized:
|
|
Key.mouse["mouse_scroll_up"] = [[x+w-2+i, y] for i in range(3)]
|
|
Key.mouse["mouse_scroll_down"] = [[x+w-2+i, y+h-1] for i in range(3)]
|
|
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"]
|
|
|
|
#* Draw current selection and number of processes
|
|
out += (f'{Mv.to(y+h, x + w - 3 - len(loc_string))}{THEME.proc_box}{Symbol.h_line*1}{Symbol.title_left}{THEME.title}'
|
|
f'{Fx.b}{loc_string}{Fx.ub}{THEME.proc_box(Symbol.title_right)}')
|
|
|
|
#* Clean up dead processes graphs and counters
|
|
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_misc}{out}{Term.fg}', only_save=Menu.active)
|
|
cls.redraw = cls.resized = cls.moved = False
|
|
|
|
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
|
|
redraw: bool = False
|
|
only_draw: bool = False
|
|
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
|
|
proc_interrupt: bool = False
|
|
use_draw_list: bool = False
|
|
|
|
@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
|
|
cls.started = False
|
|
cls.collect_queue = []
|
|
cls.collect_idle.set()
|
|
cls.collect_done.set()
|
|
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'''
|
|
draw_buffers: List[str] = []
|
|
debugged: bool = False
|
|
try:
|
|
while not cls.stopping:
|
|
if CONFIG.draw_clock: Box.draw_clock()
|
|
cls.collect_run.wait(0.1)
|
|
if not cls.collect_run.is_set():
|
|
continue
|
|
draw_buffers = []
|
|
cls.collect_interrupt = False
|
|
cls.collect_run.clear()
|
|
cls.collect_idle.clear()
|
|
cls.collect_done.clear()
|
|
if DEBUG and not debugged: TimeIt.start("Collect and draw")
|
|
while cls.collect_queue:
|
|
collector = cls.collect_queue.pop()
|
|
if not cls.only_draw:
|
|
collector._collect()
|
|
collector._draw()
|
|
if cls.use_draw_list: draw_buffers.append(collector.buffer)
|
|
if cls.collect_interrupt: break
|
|
if DEBUG and not debugged: TimeIt.stop("Collect and draw"); debugged = True
|
|
if cls.draw_now and not Menu.active and not cls.collect_interrupt:
|
|
if cls.use_draw_list: Draw.out(*draw_buffers)
|
|
else: Draw.out()
|
|
cls.collect_idle.set()
|
|
cls.collect_done.set()
|
|
except Exception as e:
|
|
errlog.exception(f'Data collection thread failed with exception: {e}')
|
|
cls.collect_idle.set()
|
|
cls.collect_done.set()
|
|
clean_quit(1, thread=True)
|
|
|
|
@classmethod
|
|
def collect(cls, *collectors, draw_now: bool = True, interrupt: bool = False, proc_interrupt: bool = False, redraw: bool = False, only_draw: bool = False):
|
|
'''Setup collect queue for _runner'''
|
|
cls.collect_interrupt = interrupt
|
|
cls.proc_interrupt = proc_interrupt
|
|
cls.collect_idle.wait()
|
|
cls.collect_interrupt = False
|
|
cls.proc_interrupt = False
|
|
cls.use_draw_list = False
|
|
cls.draw_now = draw_now
|
|
cls.redraw = redraw
|
|
cls.only_draw = only_draw
|
|
|
|
if collectors:
|
|
cls.collect_queue = [*collectors]
|
|
cls.use_draw_list = True
|
|
|
|
else:
|
|
cls.collect_queue = list(cls.__subclasses__())
|
|
|
|
cls.collect_run.set()
|
|
|
|
|
|
class CpuCollector(Collector):
|
|
'''Collects cpu usage for cpu and cores, cpu frequency, load_avg, uptime and cpu temps'''
|
|
cpu_usage: List[List[int]] = []
|
|
cpu_temp: List[List[int]] = []
|
|
cpu_temp_high: int = 0
|
|
cpu_temp_crit: int = 0
|
|
for _ in range(THREADS + 1):
|
|
cpu_usage.append([])
|
|
cpu_temp.append([])
|
|
freq_error: bool = False
|
|
cpu_freq: int = 0
|
|
load_avg: List[float] = []
|
|
uptime: str = ""
|
|
buffer: str = CpuBox.buffer
|
|
sensor_method: str = ""
|
|
got_sensors: bool = False
|
|
|
|
@classmethod
|
|
def get_sensors(cls):
|
|
'''Check if we can get cpu temps and return method of getting temps'''
|
|
cls.sensor_method = ""
|
|
if SYSTEM == "MacOS":
|
|
try:
|
|
if which("osx-cpu-temp") and subprocess.check_output("osx-cpu-temp", text=True).rstrip().endswith("°C"):
|
|
cls.sensor_method = "osx-cpu-temp"
|
|
except: pass
|
|
elif hasattr(psutil, "sensors_temperatures"):
|
|
try:
|
|
temps = psutil.sensors_temperatures()
|
|
if temps:
|
|
for name, entries in temps.items():
|
|
if name.lower() == "cpu":
|
|
cls.sensor_method = "psutil"
|
|
break
|
|
for entry in entries:
|
|
if entry.label.startswith(("Package", "Core 0", "Tdie", "CPU")):
|
|
cls.sensor_method = "psutil"
|
|
break
|
|
except: pass
|
|
if not cls.sensor_method and SYSTEM == "Linux":
|
|
try:
|
|
if which("vcgencmd") and subprocess.check_output("vcgencmd measure_temp", text=True).rstrip().endswith("'C"):
|
|
cls.sensor_method = "vcgencmd"
|
|
except: pass
|
|
cls.got_sensors = True if cls.sensor_method else False
|
|
|
|
@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]
|
|
try:
|
|
if hasattr(psutil.cpu_freq(), "current"):
|
|
cls.cpu_freq = round(psutil.cpu_freq().current)
|
|
except Exception as e:
|
|
if not cls.freq_error:
|
|
cls.freq_error = True
|
|
errlog.error("Exception while getting cpu frequency!")
|
|
errlog.exception(f'{e}')
|
|
else:
|
|
pass
|
|
cls.load_avg = [round(lavg, 2) for lavg in os.getloadavg()]
|
|
cls.uptime = str(timedelta(seconds=round(time()-psutil.boot_time(),0)))[:-3]
|
|
|
|
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":
|
|
try:
|
|
for name, entries in psutil.sensors_temperatures().items():
|
|
for entry in entries:
|
|
if entry.label.startswith(("Package", "Tdie")) and hasattr(entry, "current"):
|
|
cpu_type = "intel" if entry.label.startswith("Package") else "ryzen"
|
|
if not cls.cpu_temp_high:
|
|
if hasattr(entry, "high") and entry.high: cls.cpu_temp_high = round(entry.high)
|
|
else: cls.cpu_temp_high = 80
|
|
if hasattr(entry, "critical") and entry.critical: cls.cpu_temp_crit = round(entry.critical)
|
|
else: cls.cpu_temp_crit = 95
|
|
temp = round(entry.current)
|
|
elif (entry.label.startswith(("Core", "Tccd", "CPU")) or (name.lower() == "cpu" and not entry.label)) and hasattr(entry, "current"):
|
|
if not cpu_type:
|
|
cpu_type = "other"
|
|
if not cls.cpu_temp_high:
|
|
if hasattr(entry, "high") and entry.high: cls.cpu_temp_high = round(entry.high)
|
|
else: cls.cpu_temp_high = 80
|
|
if hasattr(entry, "critical") and entry.critical: cls.cpu_temp_crit = round(entry.critical)
|
|
else: cls.cpu_temp_crit = 95
|
|
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):
|
|
try:
|
|
cls.cpu_temp[n].append(t)
|
|
cls.cpu_temp[THREADS // 2 + n].append(t)
|
|
except IndexError:
|
|
break
|
|
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:
|
|
try:
|
|
for i in range(THREADS // len(cores)):
|
|
cls.cpu_temp[z + i].append(t)
|
|
z += i
|
|
except IndexError:
|
|
break
|
|
if cls.cpu_temp[0]:
|
|
for n in range(1, len(cls.cpu_temp)):
|
|
if len(cls.cpu_temp[n]) != len(cls.cpu_temp[n-1]):
|
|
cls.cpu_temp[n] = cls.cpu_temp[n//2].copy()
|
|
else:
|
|
cores.insert(0, temp)
|
|
for n, t in enumerate(cores):
|
|
try:
|
|
cls.cpu_temp[n].append(t)
|
|
except IndexError:
|
|
break
|
|
except Exception as e:
|
|
errlog.exception(f'{e}')
|
|
cls.got_sensors = False
|
|
#CONFIG.check_temp = False
|
|
CpuBox._calc_size()
|
|
|
|
else:
|
|
try:
|
|
if cls.sensor_method == "osx-cpu-temp":
|
|
temp = round(float(subprocess.check_output("osx-cpu-temp", text=True).strip().rstrip("°C")))
|
|
elif cls.sensor_method == "vcgencmd":
|
|
temp = round(float(subprocess.check_output(["vcgencmd", "measure_temp"], text=True).strip().rstrip("'C")))
|
|
except Exception as e:
|
|
errlog.exception(f'{e}')
|
|
cls.got_sensors = False
|
|
#CONFIG.check_temp = False
|
|
CpuBox._calc_size()
|
|
else:
|
|
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]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
def _draw(cls):
|
|
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()
|
|
|
|
io_error: bool = False
|
|
|
|
old_disks: List[str] = []
|
|
|
|
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
|
|
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
|
|
MemBox.swap_on = False
|
|
else:
|
|
if MemBox.swap_on:
|
|
MemBox.redraw = True
|
|
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(","))
|
|
|
|
try:
|
|
io_counters = psutil.disk_io_counters(perdisk=True if SYSTEM == "Linux" else False, nowrap=True)
|
|
except ValueError as e:
|
|
if not cls.io_error:
|
|
cls.io_error = True
|
|
errlog.error(f'Non fatal error during disk io collection!')
|
|
if psutil.version_info[0] < 5 or (psutil.version_info[0] == 5 and psutil.version_info[1] < 7):
|
|
errlog.error(f'Caused by outdated psutil version.')
|
|
errlog.exception(f'{e}')
|
|
io_counters = None
|
|
|
|
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
|
|
if io_counters:
|
|
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))
|
|
except:
|
|
disk_read = disk_write = 0
|
|
else:
|
|
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:
|
|
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()
|
|
|
|
@classmethod
|
|
def _draw(cls):
|
|
MemBox._draw_fg()
|
|
|
|
class NetCollector(Collector):
|
|
'''Collects network stats'''
|
|
buffer: str = NetBox.buffer
|
|
nics: List[str] = []
|
|
nic_i: int = 0
|
|
nic: str = ""
|
|
new_nic: str = ""
|
|
nic_error: bool = False
|
|
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()
|
|
net_min: Dict[str, int] = {"download" : -1, "upload" : -1}
|
|
auto_min: bool = CONFIG.net_auto_min
|
|
|
|
@classmethod
|
|
def _get_nics(cls):
|
|
'''Get a list of all network devices sorted by highest throughput'''
|
|
cls.nic_i = 0
|
|
cls.nic = ""
|
|
try:
|
|
io_all = psutil.net_io_counters(pernic=True)
|
|
except Exception as e:
|
|
if not cls.nic_error:
|
|
cls.nic_error = True
|
|
errlog.exception(f'{e}')
|
|
if not io_all: return
|
|
up_stat = psutil.net_if_stats()
|
|
for nic in sorted(io_all.keys(), key=lambda nic: (getattr(io_all[nic], "bytes_recv", 0) + getattr(io_all[nic], "bytes_sent", 0)), 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
|
|
Collector.collect(NetCollector, redraw=True)
|
|
|
|
@classmethod
|
|
def _collect(cls):
|
|
speed: int
|
|
stat: Dict
|
|
up_stat = psutil.net_if_stats()
|
|
|
|
if cls.net_min["download"] == -1 or cls.net_min["upload"] == -1:
|
|
cls.net_min["download"] = 10 << 10 if cls.auto_min else units_to_bytes(CONFIG.net_download_min)
|
|
cls.net_min["upload"] = 10 << 10 if cls.auto_min else units_to_bytes(CONFIG.net_upload_min)
|
|
try:
|
|
cls.stats[cls.nic]["download"].update({"graph_top" : cls.net_min["download"], "graph_lower" : 7})
|
|
cls.stats[cls.nic]["upload"].update({"graph_top" : cls.net_min["upload"], "graph_lower" : 7})
|
|
except:
|
|
pass
|
|
|
|
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.net_min[direction], "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]
|
|
|
|
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]
|
|
|
|
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.net_min[direction] 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.net_min[direction]: stat["graph_top"] = cls.net_min[direction]
|
|
stat["graph_raise"] = 0
|
|
stat["graph_lower"] = 0
|
|
stat["redraw"] = True
|
|
strings["graph_top"] = floating_humanizer(stat["graph_top"], short=True)
|
|
|
|
cls.timestamp = time()
|
|
|
|
|
|
|
|
@classmethod
|
|
def _draw(cls):
|
|
NetBox._draw_fg()
|
|
|
|
|
|
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
|
|
det_cpu: float = 0.0
|
|
detailed: bool = False
|
|
detailed_pid: Union[int, None] = None
|
|
details: Dict[str, Any] = {}
|
|
details_cpu: List[int] = []
|
|
details_mem: List[int] = []
|
|
expand: int = 0
|
|
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("p.info['num_threads']", "str", "eval")
|
|
sort_expr["user"] = compile("p.info['username']", "str", "eval")
|
|
sort_expr["memory"] = compile("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 = {}
|
|
cls.det_cpu = 0.0
|
|
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
|
|
|
|
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)
|
|
else:
|
|
for p in sorted(psutil.process_iter(cls.p_values + ["memory_info"] if CONFIG.proc_mem_bytes else [], err), key=lambda p: eval(sort_cmd), reverse=reverse):
|
|
if cls.collect_interrupt or cls.proc_interrupt:
|
|
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:
|
|
if cls.detailed and p.info["pid"] == cls.detailed_pid:
|
|
cls.det_cpu = p.info["cpu_percent"]
|
|
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"]
|
|
if CONFIG.proc_mem_bytes and hasattr(p.info["memory_info"], "rss"):
|
|
mem_b = p.info["memory_info"].rss
|
|
else:
|
|
mem_b = 0
|
|
|
|
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,
|
|
"mem_b" : mem_b,
|
|
"cpu" : cpu }
|
|
|
|
n += 1
|
|
|
|
cls.num_procs = n
|
|
cls.processes = out.copy()
|
|
|
|
if cls.detailed:
|
|
cls.expand = ((ProcBox.width - 2) - ((ProcBox.width - 2) // 3) - 40) // 10
|
|
if cls.expand > 5: cls.expand = 5
|
|
if cls.detailed and not cls.details.get("killed", False):
|
|
try:
|
|
c_pid = cls.detailed_pid
|
|
det = psutil.Process(c_pid)
|
|
except (psutil.NoSuchProcess, psutil.ZombieProcess):
|
|
cls.details["killed"] = True
|
|
cls.details["status"] = psutil.STATUS_DEAD
|
|
ProcBox.redraw = True
|
|
else:
|
|
attrs: List[str] = ["status", "memory_info", "create_time"]
|
|
if not SYSTEM == "MacOS": attrs.extend(["cpu_num"])
|
|
if cls.expand:
|
|
attrs.extend(["nice", "terminal"])
|
|
if not SYSTEM == "MacOS": attrs.extend(["io_counters"])
|
|
|
|
if not c_pid in cls.processes: attrs.extend(["pid", "name", "cmdline", "num_threads", "username", "memory_percent"])
|
|
|
|
cls.details = det.as_dict(attrs=attrs, ad_value="")
|
|
if det.parent() != None: cls.details["parent_name"] = det.parent().name()
|
|
else: cls.details["parent_name"] = ""
|
|
|
|
cls.details["pid"] = c_pid
|
|
if c_pid in cls.processes:
|
|
cls.details["name"] = cls.processes[c_pid]["name"]
|
|
cls.details["cmdline"] = cls.processes[c_pid]["cmd"]
|
|
cls.details["threads"] = f'{cls.processes[c_pid]["threads"]}'
|
|
cls.details["username"] = cls.processes[c_pid]["username"]
|
|
cls.details["memory_percent"] = cls.processes[c_pid]["mem"]
|
|
cls.details["cpu_percent"] = round(cls.processes[c_pid]["cpu"] * (1 if CONFIG.proc_per_core else THREADS))
|
|
else:
|
|
cls.details["cmdline"] = " ".join(cls.details["cmdline"]) or "[" + cls.details["name"] + "]"
|
|
cls.details["threads"] = f'{cls.details["num_threads"]}'
|
|
cls.details["cpu_percent"] = round(cls.det_cpu)
|
|
|
|
cls.details["killed"] = False
|
|
if SYSTEM == "MacOS":
|
|
cls.details["cpu_num"] = -1
|
|
cls.details["io_counters"] = ""
|
|
|
|
|
|
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):
|
|
uptime = timedelta(seconds=round(time()-cls.details["create_time"],0))
|
|
if uptime.days > 0: cls.details["uptime"] = f'{uptime.days}d {str(uptime).split(",")[1][:-3].strip()}'
|
|
else: cls.details["uptime"] = f'{uptime}'
|
|
else: cls.details["uptime"] = "??:??:??"
|
|
|
|
if cls.expand:
|
|
if cls.expand > 1 : cls.details["nice"] = f'{cls.details["nice"]}'
|
|
if SYSTEM == "BSD":
|
|
if cls.expand > 2:
|
|
if hasattr(cls.details["io_counters"], "read_count"): cls.details["io_read"] = f'{cls.details["io_counters"].read_count}'
|
|
else: cls.details["io_read"] = "?"
|
|
if cls.expand > 3:
|
|
if hasattr(cls.details["io_counters"], "write_count"): cls.details["io_write"] = f'{cls.details["io_counters"].write_count}'
|
|
else: cls.details["io_write"] = "?"
|
|
else:
|
|
if cls.expand > 2:
|
|
if hasattr(cls.details["io_counters"], "read_bytes"): cls.details["io_read"] = floating_humanizer(cls.details["io_counters"].read_bytes)
|
|
else: cls.details["io_read"] = "?"
|
|
if cls.expand > 3:
|
|
if hasattr(cls.details["io_counters"], "write_bytes"): cls.details["io_write"] = floating_humanizer(cls.details["io_counters"].write_bytes)
|
|
else: cls.details["io_write"] = "?"
|
|
if cls.expand > 4 : cls.details["terminal"] = f'{cls.details["terminal"]}'.replace("/dev/", "")
|
|
|
|
cls.details_cpu.append(cls.details["cpu_percent"])
|
|
mem = cls.details["memory_percent"]
|
|
if mem > 80: mem = round(mem)
|
|
elif mem > 60: mem = round(mem * 1.2)
|
|
elif mem > 30: mem = round(mem * 1.5)
|
|
elif mem > 10: mem = round(mem * 2)
|
|
elif mem > 5: mem = round(mem * 10)
|
|
else: mem = round(mem * 20)
|
|
cls.details_mem.append(mem)
|
|
if len(cls.details_cpu) > ProcBox.width: del cls.details_cpu[0]
|
|
if len(cls.details_mem) > ProcBox.width: del cls.details_mem[0]
|
|
|
|
@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
|
|
det_cpu: float = 0.0
|
|
infolist: Dict = {}
|
|
tree = defaultdict(list)
|
|
n: int = 0
|
|
for p in sorted(psutil.process_iter(cls.p_values + ["memory_info"] if CONFIG.proc_mem_bytes else [], 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, det_cpu
|
|
name: str; threads: int; username: str; mem: float; cpu: float
|
|
cont: bool = True
|
|
getinfo: Dict = {}
|
|
if cls.collect_interrupt: return
|
|
try:
|
|
name = psutil.Process(pid).name()
|
|
if name == "idle": return
|
|
except psutil.Error:
|
|
pass
|
|
cont = False
|
|
name = ""
|
|
if pid in infolist:
|
|
getinfo = infolist[pid]
|
|
|
|
if search and not found:
|
|
if cls.detailed and pid == cls.detailed_pid:
|
|
det_cpu = getinfo["cpu_percent"]
|
|
if "username" in getinfo and isinstance(getinfo["username"], float): getinfo["username"] = ""
|
|
if "cmdline" in getinfo and isinstance(getinfo["cmdline"], float): getinfo["cmdline"] = ""
|
|
for value in [ name, str(pid), getinfo.get("username", ""), " ".join(getinfo.get("cmdline", "")) ]:
|
|
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 = getinfo["memory_percent"]
|
|
if getinfo["cmdline"] == err: cmd = ""
|
|
else: cmd = " ".join(getinfo["cmdline"]) or "[" + getinfo["name"] + "]"
|
|
if CONFIG.proc_mem_bytes and hasattr(getinfo["memory_info"], "rss"):
|
|
mem_b = getinfo["memory_info"].rss
|
|
else:
|
|
mem_b = 0
|
|
else:
|
|
threads = mem_b = 0
|
|
username = ""
|
|
mem = cpu = 0.0
|
|
|
|
out[pid] = {
|
|
"indent" : inindent,
|
|
"name": name,
|
|
"cmd" : cmd,
|
|
"threads" : threads,
|
|
"username" : username,
|
|
"mem" : mem,
|
|
"mem_b" : mem_b,
|
|
"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)
|
|
cls.det_cpu = det_cpu
|
|
|
|
if cls.collect_interrupt: return
|
|
cls.num_procs = len(out)
|
|
cls.processes = out.copy()
|
|
|
|
@classmethod
|
|
def sorting(cls, key: str):
|
|
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]
|
|
if "left" in Key.mouse: del Key.mouse["left"]
|
|
Collector.collect(ProcCollector, interrupt=True, redraw=True)
|
|
|
|
@classmethod
|
|
def _draw(cls):
|
|
ProcBox._draw_fg()
|
|
|
|
class Menu:
|
|
'''Holds all menus'''
|
|
active: bool = False
|
|
close: bool = False
|
|
resized: bool = True
|
|
menus: Dict[str, Dict[str, str]] = {}
|
|
menu_length: Dict[str, int] = {}
|
|
background: str = ""
|
|
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]
|
|
cls.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}')
|
|
if UpdateChecker.version != VERSION:
|
|
banner += f'{Mv.to(Term.height, 1)}{Fx.b}{THEME.title}New release {UpdateChecker.version} available at https://github.com/aristocratos/bpytop{Fx.ub}{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'{cls.background}{banner}{out}')
|
|
skip = redraw = False
|
|
|
|
if 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()
|
|
elif menu_current == "options":
|
|
cls.options()
|
|
cls.resized = True
|
|
elif menu_current == "help":
|
|
cls.help()
|
|
cls.resized = True
|
|
|
|
if Timer.not_zero() and not cls.resized:
|
|
skip = True
|
|
else:
|
|
Collector.collect()
|
|
Collector.collect_done.wait(1)
|
|
if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
|
|
Timer.stamp()
|
|
|
|
|
|
Draw.now(f'{Draw.saved_buffer()}')
|
|
cls.background = ""
|
|
cls.active = False
|
|
cls.close = False
|
|
|
|
@classmethod
|
|
def help(cls):
|
|
out: str = ""
|
|
out_misc : str = ""
|
|
redraw: bool = True
|
|
key: str = ""
|
|
skip: bool = False
|
|
main_active: bool = True if cls.active else False
|
|
cls.active = True
|
|
cls.resized = True
|
|
if not cls.background:
|
|
cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
|
|
help_items: Dict[str, str] = {
|
|
"(Mouse 1)" : "Clicks buttons and selects in process list.",
|
|
"Selected (Mouse 1)" : "Show detailed information for selected process.",
|
|
"(Mouse scroll)" : "Scrolls any scrollable list/text under cursor.",
|
|
"(Esc, shift+m)" : "Toggles main menu.",
|
|
"(m)" : "Toggle mini mode.",
|
|
"(F2, o)" : "Shows options.",
|
|
"(F1, h)" : "Shows this window.",
|
|
"(ctrl+z)" : "Sleep program and put in background.",
|
|
"(ctrl+c, q)" : "Quits program.",
|
|
"(+) / (-)" : "Add/Subtract 100ms to/from update timer.",
|
|
"(Up) (Down)" : "Select in process list.",
|
|
"(Enter)" : "Show detailed information for selected process.",
|
|
"(Pg Up) (Pg Down)" : "Jump 1 page in process list.",
|
|
"(Home) (End)" : "Jump to first or last page in process list.",
|
|
"(Left) (Right)" : "Select previous/next sorting column.",
|
|
"(b) (n)" : "Select previous/next network device.",
|
|
"(z)" : "Toggle totals reset for current network device",
|
|
"(f)" : "Input a string to filter processes with.",
|
|
"(c)" : "Toggle per-core cpu usage of processes.",
|
|
"(r)" : "Reverse sorting order in processes box.",
|
|
"(e)" : "Toggle processes tree view",
|
|
"(delete)" : "Clear any entered filter.",
|
|
"Selected (T, t)" : "Terminate selected process with SIGTERM - 15.",
|
|
"Selected (K, k)" : "Kill selected process with SIGKILL - 9.",
|
|
"Selected (I, i)" : "Interrupt selected process with SIGINT - 2.",
|
|
"_1" : " ",
|
|
"_2" : "For bug reporting and project updates, visit:",
|
|
"_3" : "https://github.com/aristocratos/bpytop",
|
|
}
|
|
|
|
while not cls.close:
|
|
key = ""
|
|
if cls.resized:
|
|
y = 8 if Term.height < len(help_items) + 10 else Term.height // 2 - len(help_items) // 2 + 4
|
|
out_misc = (f'{Banner.draw(y-7, 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}')
|
|
x = Term.width//2-36
|
|
h, w = Term.height-2-y, 72
|
|
if len(help_items) > h:
|
|
pages = ceil(len(help_items) / h)
|
|
else:
|
|
h = len(help_items)
|
|
pages = 0
|
|
page = 1
|
|
out_misc += create_box(x, y, w, h+3, "help", line_color=THEME.div_line)
|
|
redraw = True
|
|
cls.resized = False
|
|
|
|
if redraw:
|
|
out = ""
|
|
cy = 0
|
|
if pages:
|
|
out += (f'{Mv.to(y, x+56)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title("pg")}{Fx.ub}{THEME.main_fg(Symbol.up)} {Fx.b}{THEME.title}{page}/{pages} '
|
|
f'pg{Fx.ub}{THEME.main_fg(Symbol.down)}{THEME.div_line(Symbol.title_right)}')
|
|
out += f'{Mv.to(y+1, x+1)}{THEME.title}{Fx.b}{"Keys:":^20}Description:{THEME.main_fg}'
|
|
for n, (keys, desc) in enumerate(help_items.items()):
|
|
if pages and n < (page - 1) * h: continue
|
|
out += f'{Mv.to(y+2+cy, x+1)}{Fx.b}{("" if keys.startswith("_") else keys):^20.20}{Fx.ub}{desc:50.50}'
|
|
cy += 1
|
|
if cy == h: break
|
|
if cy < h:
|
|
for i in range(h-cy):
|
|
out += f'{Mv.to(y+2+cy+i, x+1)}{" " * (w-2)}'
|
|
|
|
if skip and redraw:
|
|
Draw.now(out)
|
|
elif not skip:
|
|
Draw.now(f'{cls.background}{out_misc}{out}')
|
|
skip = redraw = False
|
|
|
|
if Key.input_wait(Timer.left()):
|
|
key = Key.get()
|
|
|
|
if key == "mouse_click":
|
|
mx, my = Key.get_mouse()
|
|
if mx >= x and mx < x + w and my >= y and my < y + h + 3:
|
|
if pages and my == y and mx > x + 56 and mx < x + 61:
|
|
key = "up"
|
|
elif pages and my == y and mx > x + 63 and mx < x + 68:
|
|
key = "down"
|
|
else:
|
|
key = "escape"
|
|
|
|
if key == "q":
|
|
clean_quit()
|
|
elif key in ["escape", "M", "enter", "backspace", "h", "f1"]:
|
|
cls.close = True
|
|
break
|
|
elif key in ["up", "mouse_scroll_up", "page_up"] and pages:
|
|
page -= 1
|
|
if page < 1: page = pages
|
|
redraw = True
|
|
elif key in ["down", "mouse_scroll_down", "page_down"] and pages:
|
|
page += 1
|
|
if page > pages: page = 1
|
|
redraw = True
|
|
|
|
if Timer.not_zero() and not cls.resized:
|
|
skip = True
|
|
else:
|
|
Collector.collect()
|
|
Collector.collect_done.wait(1)
|
|
if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
|
|
Timer.stamp()
|
|
|
|
if main_active:
|
|
cls.close = False
|
|
return
|
|
Draw.now(f'{Draw.saved_buffer()}')
|
|
cls.background = ""
|
|
cls.active = False
|
|
cls.close = False
|
|
|
|
@classmethod
|
|
def options(cls):
|
|
out: str = ""
|
|
out_misc : str = ""
|
|
redraw: bool = True
|
|
key: str = ""
|
|
skip: bool = False
|
|
main_active: bool = True if cls.active else False
|
|
cls.active = True
|
|
cls.resized = True
|
|
d_quote: str
|
|
inputting: bool = False
|
|
input_val: str = ""
|
|
Theme.refresh()
|
|
if not cls.background:
|
|
cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
|
|
option_items: Dict[str, List[str]] = {
|
|
"color_theme" : [
|
|
'Set color theme.',
|
|
'',
|
|
'Choose from all theme files in',
|
|
'"/usr/[local/]share/bpytop/themes" and',
|
|
'"~/.config/bpytop/themes".',
|
|
'',
|
|
'"Default" for builtin default theme.',
|
|
'User themes are prefixed by a plus sign "+".',
|
|
'',
|
|
'For theme updates see:',
|
|
'https://github.com/aristocratos/bpytop'],
|
|
"mini_mode" : [
|
|
'Enable bpytop mini mode at start.',
|
|
'',
|
|
'Disables net and mem boxes and lowers height',
|
|
'of cpu box.'],
|
|
"update_ms" : [
|
|
'Update time in milliseconds.',
|
|
'',
|
|
'Recommended 2000 ms or above for better sample',
|
|
'times for graphs.',
|
|
'',
|
|
'Min value: 100 ms',
|
|
'Max value: 86400000 ms = 24 hours.'],
|
|
"proc_sorting" : [
|
|
'Processes sorting option.',
|
|
'',
|
|
'Possible values: "pid", "program", "arguments",',
|
|
'"threads", "user", "memory", "cpu lazy" and',
|
|
'"cpu responsive".',
|
|
'',
|
|
'"cpu lazy" updates top process over time,',
|
|
'"cpu responsive" updates top process directly.'],
|
|
"proc_reversed" : [
|
|
'Reverse processes sorting order.',
|
|
'',
|
|
'True or False.'],
|
|
"proc_tree" : [
|
|
'Processes tree view.',
|
|
'',
|
|
'Set true to show processes grouped by parents,',
|
|
'with lines drawn between parent and child',
|
|
'process.'],
|
|
"proc_colors" : [
|
|
'Enable colors in process view.',
|
|
'',
|
|
'Uses the cpu graph gradient colors.'],
|
|
"proc_gradient" : [
|
|
'Enable process view gradient fade.',
|
|
'',
|
|
'Fades from top or current selection.',
|
|
'Max fade value is equal to current themes',
|
|
'"inactive_fg" color value.'],
|
|
"proc_per_core" : [
|
|
'Process usage per core.',
|
|
'',
|
|
'If process cpu usage should be of the core',
|
|
'it\'s running on or usage of the total',
|
|
'available cpu power.',
|
|
'',
|
|
'If true and process is multithreaded',
|
|
'cpu usage can reach over 100%.'],
|
|
"proc_mem_bytes" : [
|
|
'Show memory as bytes in process list.',
|
|
' ',
|
|
'True or False.'
|
|
],
|
|
"check_temp" : [
|
|
'Enable cpu temperature reporting.',
|
|
'',
|
|
'True or False.'],
|
|
"draw_clock" : [
|
|
'Draw a clock at top of screen.',
|
|
'',
|
|
'Formatting according to strftime, empty',
|
|
'string to disable.',
|
|
'',
|
|
'Examples:',
|
|
'"%X" locale HH:MM:SS',
|
|
'"%H" 24h hour, "%I" 12h hour',
|
|
'"%M" minute, "%S" second',
|
|
'"%d" day, "%m" month, "%y" year'],
|
|
"background_update" : [
|
|
'Update main ui when menus are showing.',
|
|
'',
|
|
'True or False.',
|
|
'',
|
|
'Set this to false if the menus is flickering',
|
|
'too much for a comfortable experience.'],
|
|
"custom_cpu_name" : [
|
|
'Custom cpu model name in cpu percentage box.',
|
|
'',
|
|
'Empty string to disable.'],
|
|
"disks_filter" : [
|
|
'Optional filter for shown disks.',
|
|
'',
|
|
'Should be last folder in path of a mountpoint,',
|
|
'"root" replaces "/", separate multiple values',
|
|
'with a comma.',
|
|
'Begin line with "exclude=" to change to exclude',
|
|
'filter.',
|
|
'Oterwise defaults to "most include" filter.',
|
|
'',
|
|
'Example: disks_filter="exclude=boot, home"'],
|
|
"mem_graphs" : [
|
|
'Show graphs for memory values.',
|
|
'',
|
|
'True or False.'],
|
|
"show_swap" : [
|
|
'If swap memory should be shown in memory box.',
|
|
'',
|
|
'True or False.'],
|
|
"swap_disk" : [
|
|
'Show swap as a disk.',
|
|
'',
|
|
'Ignores show_swap value above.',
|
|
'Inserts itself after first disk.'],
|
|
"show_disks" : [
|
|
'Split memory box to also show disks.',
|
|
'',
|
|
'True or False.'],
|
|
"net_download_min" : [
|
|
'Min value the network graphs scales down to.',
|
|
'',
|
|
'Default "10K" = 10 KibiBytes.',
|
|
'Possible units:',
|
|
'"K" (KiB), "M" (MiB), "G" (GiB),',
|
|
'no unit for bytes.',
|
|
'',
|
|
'Default values can be toggled with auto button.'],
|
|
"net_upload_min" : [
|
|
'Min value the network graphs scales down to.',
|
|
'',
|
|
'Default "10K" = 10 KibiBytes.',
|
|
'Possible units:',
|
|
'"K" (KiB), "M" (MiB), "G" (GiB),',
|
|
'no unit for bytes.',
|
|
'',
|
|
'Default values can be toggled with auto button.'],
|
|
"net_auto_min" : [
|
|
'Start in network graphs auto rescaling mode.',
|
|
'',
|
|
'Ignores any values set above at start and',
|
|
'rescale down to default value "10K".',
|
|
'',
|
|
'True or False.'],
|
|
"show_init" : [
|
|
'Show init screen at startup.',
|
|
'',
|
|
'The init screen is purely cosmetical and',
|
|
'slows down start to show status messages.'],
|
|
"update_check" : [
|
|
'Check for updates at start.',
|
|
'',
|
|
'Checks for latest version from:',
|
|
'https://github.com/aristocratos/bpytop'],
|
|
"log_level" : [
|
|
'Set loglevel for error.log',
|
|
'',
|
|
'Levels are: "ERROR" "WARNING" "INFO" "DEBUG".',
|
|
'The level set includes all lower levels,',
|
|
'i.e. "DEBUG" will show all logging info.']
|
|
}
|
|
option_len: int = len(option_items) * 2
|
|
sorting_i: int = CONFIG.sorting_options.index(CONFIG.proc_sorting)
|
|
loglevel_i: int = CONFIG.log_levels.index(CONFIG.log_level)
|
|
color_i: int
|
|
while not cls.close:
|
|
key = ""
|
|
if cls.resized:
|
|
y = 9 if Term.height < option_len + 10 else Term.height // 2 - option_len // 2 + 4
|
|
out_misc = (f'{Banner.draw(y-7, 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}')
|
|
x = Term.width//2-38
|
|
x2 = x + 27
|
|
h, w, w2 = Term.height-2-y, 26, 50
|
|
h -= h % 2
|
|
color_i = list(Theme.themes).index(THEME.current)
|
|
if option_len > h:
|
|
pages = ceil(option_len / h)
|
|
else:
|
|
h = option_len
|
|
pages = 0
|
|
page = 1
|
|
selected_int = 0
|
|
out_misc += create_box(x, y, w, h+2, "options", line_color=THEME.div_line)
|
|
redraw = True
|
|
cls.resized = False
|
|
|
|
if redraw:
|
|
out = ""
|
|
cy = 0
|
|
|
|
selected = list(option_items)[selected_int]
|
|
if pages:
|
|
out += (f'{Mv.to(y+h+1, x+11)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title("pg")}{Fx.ub}{THEME.main_fg(Symbol.up)} {Fx.b}{THEME.title}{page}/{pages} '
|
|
f'pg{Fx.ub}{THEME.main_fg(Symbol.down)}{THEME.div_line(Symbol.title_right)}')
|
|
#out += f'{Mv.to(y+1, x+1)}{THEME.title}{Fx.b}{"Keys:":^20}Description:{THEME.main_fg}'
|
|
for n, opt in enumerate(option_items):
|
|
if pages and n < (page - 1) * ceil(h / 2): continue
|
|
value = getattr(CONFIG, opt)
|
|
t_color = f'{THEME.selected_bg}{THEME.selected_fg}' if opt == selected else f'{THEME.title}'
|
|
v_color = "" if opt == selected else f'{THEME.title}'
|
|
d_quote = '"' if isinstance(value, str) else ""
|
|
if opt == "color_theme":
|
|
counter = f' {color_i + 1}/{len(Theme.themes)}'
|
|
elif opt == "proc_sorting":
|
|
counter = f' {sorting_i + 1}/{len(CONFIG.sorting_options)}'
|
|
elif opt == "log_level":
|
|
counter = f' {loglevel_i + 1}/{len(CONFIG.log_levels)}'
|
|
else:
|
|
counter = ""
|
|
out += f'{Mv.to(y+1+cy, x+1)}{t_color}{Fx.b}{opt.replace("_", " ").capitalize() + counter:^24.24}{Fx.ub}{Mv.to(y+2+cy, x+1)}{v_color}'
|
|
if opt == selected:
|
|
if isinstance(value, bool) or opt in ["color_theme", "proc_sorting", "log_level"]:
|
|
out += f'{t_color} {Symbol.left}{v_color}{d_quote + str(value) + d_quote:^20.20}{t_color}{Symbol.right} '
|
|
elif inputting:
|
|
out += f'{str(input_val)[-17:] + Fx.bl + "█" + Fx.ubl + "" + Symbol.enter:^33.33}'
|
|
else:
|
|
out += ((f'{t_color} {Symbol.left}{v_color}' if type(value) is int else " ") +
|
|
f'{str(value) + " " + Symbol.enter:^20.20}' + (f'{t_color}{Symbol.right} ' if type(value) is int else " "))
|
|
else:
|
|
out += f'{d_quote + str(value) + d_quote:^24.24}'
|
|
out += f'{Term.bg}'
|
|
if opt == selected:
|
|
h2 = len(option_items[opt]) + 2
|
|
y2 = y + (selected_int * 2) - ((page-1) * h)
|
|
if y2 + h2 > Term.height: y2 = Term.height - h2
|
|
out += f'{create_box(x2, y2, w2, h2, "description", line_color=THEME.div_line)}{THEME.main_fg}'
|
|
for n, desc in enumerate(option_items[opt]):
|
|
out += f'{Mv.to(y2+1+n, x2+2)}{desc:.48}'
|
|
cy += 2
|
|
if cy >= h: break
|
|
if cy < h:
|
|
for i in range(h-cy):
|
|
out += f'{Mv.to(y+1+cy+i, x+1)}{" " * (w-2)}'
|
|
|
|
|
|
if not skip or redraw:
|
|
Draw.now(f'{cls.background}{out_misc}{out}')
|
|
skip = redraw = False
|
|
|
|
if Key.input_wait(Timer.left()):
|
|
key = Key.get()
|
|
redraw = True
|
|
has_sel = False
|
|
if key == "mouse_click" and not inputting:
|
|
mx, my = Key.get_mouse()
|
|
if mx > x and mx < x + w and my > y and my < y + h + 2:
|
|
mouse_sel = ceil((my - y) / 2) - 1 + ceil((page-1) * (h / 2))
|
|
if pages and my == y+h+1 and mx > x+11 and mx < x+16:
|
|
key = "page_up"
|
|
elif pages and my == y+h+1 and mx > x+19 and mx < x+24:
|
|
key = "page_down"
|
|
elif my == y+h+1:
|
|
pass
|
|
elif mouse_sel == selected_int:
|
|
if mx < x + 6:
|
|
key = "left"
|
|
elif mx > x + 19:
|
|
key = "right"
|
|
else:
|
|
key = "enter"
|
|
elif mouse_sel < len(option_items):
|
|
selected_int = mouse_sel
|
|
has_sel = True
|
|
else:
|
|
key = "escape"
|
|
if inputting:
|
|
if key in ["escape", "mouse_click"]:
|
|
inputting = False
|
|
elif key == "enter":
|
|
inputting = False
|
|
if str(getattr(CONFIG, selected)) != input_val:
|
|
if selected == "update_ms":
|
|
if not input_val or int(input_val) < 100:
|
|
CONFIG.update_ms = 100
|
|
elif int(input_val) > 86399900:
|
|
CONFIG.update_ms = 86399900
|
|
else:
|
|
CONFIG.update_ms = int(input_val)
|
|
elif isinstance(getattr(CONFIG, selected), str):
|
|
setattr(CONFIG, selected, input_val)
|
|
if selected.startswith("net_"):
|
|
NetCollector.net_min = {"download" : -1, "upload" : -1}
|
|
Term.refresh(force=True)
|
|
cls.resized = False
|
|
elif key == "backspace" and len(input_val) > 0:
|
|
input_val = input_val[:-1]
|
|
elif key == "delete":
|
|
input_val = ""
|
|
elif isinstance(getattr(CONFIG, selected), str) and len(key) == 1:
|
|
input_val += key
|
|
elif isinstance(getattr(CONFIG, selected), int) and key.isdigit():
|
|
input_val += key
|
|
|
|
elif key == "q":
|
|
clean_quit()
|
|
elif key in ["escape", "o", "f2"]:
|
|
cls.close = True
|
|
break
|
|
elif key == "enter" and selected in ["update_ms", "disks_filter", "custom_cpu_name", "net_download_min", "net_upload_min"]:
|
|
inputting = True
|
|
input_val = str(getattr(CONFIG, selected))
|
|
elif key == "left" and selected == "update_ms" and CONFIG.update_ms - 100 >= 100:
|
|
CONFIG.update_ms -= 100
|
|
Box.draw_update_ms()
|
|
elif key == "right" and selected == "update_ms" and CONFIG.update_ms + 100 <= 86399900:
|
|
CONFIG.update_ms += 100
|
|
Box.draw_update_ms()
|
|
elif key in ["left", "right"] and isinstance(getattr(CONFIG, selected), bool):
|
|
setattr(CONFIG, selected, not getattr(CONFIG, selected))
|
|
if selected == "check_temp":
|
|
if CONFIG.check_temp:
|
|
CpuCollector.get_sensors()
|
|
else:
|
|
CpuCollector.sensor_method = ""
|
|
CpuCollector.got_sensors = False
|
|
Term.refresh(force=True)
|
|
cls.resized = False
|
|
elif key in ["left", "right"] and selected == "color_theme" and len(Theme.themes) > 1:
|
|
if key == "left":
|
|
color_i -= 1
|
|
if color_i < 0: color_i = len(Theme.themes) - 1
|
|
elif key == "right":
|
|
color_i += 1
|
|
if color_i > len(Theme.themes) - 1: color_i = 0
|
|
CONFIG.color_theme = list(Theme.themes)[color_i]
|
|
THEME(CONFIG.color_theme)
|
|
Term.refresh(force=True)
|
|
Timer.finish()
|
|
elif key in ["left", "right"] and selected == "proc_sorting":
|
|
ProcCollector.sorting(key)
|
|
elif key in ["left", "right"] and selected == "log_level":
|
|
if key == "left":
|
|
loglevel_i -= 1
|
|
if loglevel_i < 0: loglevel_i = len(CONFIG.log_levels) - 1
|
|
elif key == "right":
|
|
loglevel_i += 1
|
|
if loglevel_i > len(CONFIG.log_levels) - 1: loglevel_i = 0
|
|
CONFIG.log_level = CONFIG.log_levels[loglevel_i]
|
|
errlog.setLevel(getattr(logging, CONFIG.log_level))
|
|
errlog.info(f'Loglevel set to {CONFIG.log_level}')
|
|
elif key == "up":
|
|
selected_int -= 1
|
|
if selected_int < 0: selected_int = len(option_items) - 1
|
|
page = floor(selected_int * 2 / h) + 1
|
|
elif key == "down":
|
|
selected_int += 1
|
|
if selected_int > len(option_items) - 1: selected_int = 0
|
|
page = floor(selected_int * 2 / h) + 1
|
|
elif key in ["mouse_scroll_up", "page_up"] and pages:
|
|
page -= 1
|
|
if page < 1: page = pages
|
|
selected_int = (page-1) * ceil(h / 2)
|
|
elif key in ["mouse_scroll_down", "page_down"] and pages:
|
|
page += 1
|
|
if page > pages: page = 1
|
|
selected_int = (page-1) * ceil(h / 2)
|
|
elif has_sel:
|
|
pass
|
|
else:
|
|
redraw = False
|
|
|
|
if Timer.not_zero() and not cls.resized:
|
|
skip = True
|
|
else:
|
|
Collector.collect()
|
|
Collector.collect_done.wait(1)
|
|
if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}'
|
|
Timer.stamp()
|
|
|
|
if main_active:
|
|
cls.close = False
|
|
return
|
|
Draw.now(f'{Draw.saved_buffer()}')
|
|
cls.background = ""
|
|
cls.active = False
|
|
cls.close = False
|
|
|
|
class Timer:
|
|
timestamp: float
|
|
return_zero = False
|
|
|
|
@classmethod
|
|
def stamp(cls):
|
|
cls.timestamp = time()
|
|
|
|
@classmethod
|
|
def not_zero(cls) -> bool:
|
|
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):
|
|
cls.return_zero = True
|
|
cls.timestamp = time() - (CONFIG.update_ms / 1000)
|
|
Key.break_wait()
|
|
|
|
class UpdateChecker:
|
|
version: str = VERSION
|
|
thread: threading.Thread
|
|
|
|
@classmethod
|
|
def run(cls):
|
|
cls.thread = threading.Thread(target=cls._checker)
|
|
cls.thread.start()
|
|
|
|
@classmethod
|
|
def _checker(cls):
|
|
try:
|
|
with urllib.request.urlopen("https://github.com/aristocratos/bpytop/raw/master/bpytop.py", timeout=5) as source: # type: ignore
|
|
for line in source:
|
|
line = line.decode("utf-8")
|
|
if line.startswith("VERSION: str ="):
|
|
cls.version = line[(line.index("=")+1):].strip('" \n')
|
|
break
|
|
except Exception as e:
|
|
errlog.exception(f'{e}')
|
|
else:
|
|
if cls.version != VERSION and which("notify-send"):
|
|
try:
|
|
subprocess.run(["notify-send", "-u", "normal", "BpyTop Update!",
|
|
f'New version of BpyTop available!\nCurrent version: {VERSION}\nNew version: {cls.version}\nDownload at github.com/aristocratos/bpytop',
|
|
"-i", "update-notifier", "-t", "10000"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except Exception as e:
|
|
errlog.exception(f'{e}')
|
|
|
|
#? 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
|
|
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 and "CPU" 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 and not nlist[0] == "CPU":
|
|
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:
|
|
'''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
|
|
|
|
#* Get values from box class if given
|
|
if box:
|
|
x = box.x
|
|
y = box.y
|
|
width = box.width
|
|
height =box.height
|
|
title = box.name
|
|
hlines: Tuple[int, int] = (y, y + height - 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 and fill if enabled
|
|
for hpos in range(hlines[0]+1, hlines[1]):
|
|
out += f'{Mv.to(hpos, x)}{Symbol.v_line}{" " * (width-2) if fill else Mv.r(width-2)}{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}'
|
|
if title2:
|
|
out += f'{Mv.to(hlines[1], x + 2)}{Symbol.title_left}{title_color}{Fx.b}{title2}{Fx.ub}{line_color}{Symbol.title_right}'
|
|
|
|
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()
|
|
Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off, Term.title())
|
|
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, Term.title("BpyTOP"))
|
|
Term.echo(False)
|
|
Key.start()
|
|
Term.refresh()
|
|
Box.calc_sizes()
|
|
Box.draw_bg()
|
|
Collector.start()
|
|
|
|
#Draw.out()
|
|
|
|
def quit_sigint(signum, frame):
|
|
"""SIGINT redirection to clean_quit()"""
|
|
clean_quit()
|
|
|
|
def clean_quit(errcode: int = 0, errmsg: str = "", thread: bool = False):
|
|
"""Stop background input read, save current config and reset terminal settings before quitting"""
|
|
global THREAD_ERROR
|
|
if thread:
|
|
THREAD_ERROR = errcode
|
|
interrupt_main()
|
|
return
|
|
if THREAD_ERROR: errcode = THREAD_ERROR
|
|
Key.stop()
|
|
Collector.stop()
|
|
if not errcode: CONFIG.save_config()
|
|
Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off, Term.title())
|
|
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')
|
|
if not errmsg: errmsg = f'Bpytop exited with errorcode ({errcode}). See {CONFIG_DIR}/error.log for more information!'
|
|
if errmsg: print(errmsg)
|
|
|
|
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
|
|
else: value = 0
|
|
|
|
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}'
|
|
|
|
|
|
if short:
|
|
out = out.split(".")[0]
|
|
if len(out) > 3:
|
|
out = f'{int(out[0]) + 1}'
|
|
selector += 1
|
|
out += f'{"" if short else " "}{unit[selector][0] if short else unit[selector]}'
|
|
if per_second: out += "ps" if bit else "/s"
|
|
|
|
return out
|
|
|
|
def units_to_bytes(value: str) -> int:
|
|
if not value: return 0
|
|
out: int = 0
|
|
mult: int = 0
|
|
value_i: int = 0
|
|
units: Dict[str, int] = {"K" : 1, "M" : 2, "G" : 3}
|
|
try:
|
|
if value[-1].upper() in units:
|
|
mult = units[value[-1].upper()]
|
|
value = value[:-1]
|
|
|
|
if "." in value and value.replace(".", "").isdigit():
|
|
if mult > 0:
|
|
value_i = round(float(value) * 1024)
|
|
mult -= 1
|
|
else:
|
|
value_i = round(float(value))
|
|
elif value.isdigit():
|
|
value_i = int(value)
|
|
|
|
out = int(value_i) << 10 * mult
|
|
except ValueError:
|
|
out = 0
|
|
return out
|
|
|
|
def process_keys():
|
|
mouse_pos: Tuple[int, int] = (0, 0)
|
|
filtered: bool = False
|
|
while Key.has_key():
|
|
key = Key.get()
|
|
if key in ["mouse_scroll_up", "mouse_scroll_down", "mouse_click"]:
|
|
mouse_pos = Key.get_mouse()
|
|
if mouse_pos[0] >= ProcBox.x and mouse_pos[1] >= ProcBox.current_y + 1 and mouse_pos[1] < ProcBox.current_y + ProcBox.current_h - 1:
|
|
pass
|
|
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
|
|
|
|
|
|
if key == "_null":
|
|
continue
|
|
elif key == "q":
|
|
clean_quit()
|
|
elif key == "+" and CONFIG.update_ms + 100 <= 86399900:
|
|
CONFIG.update_ms += 100
|
|
Box.draw_update_ms()
|
|
elif key == "-" and CONFIG.update_ms - 100 >= 100:
|
|
CONFIG.update_ms -= 100
|
|
Box.draw_update_ms()
|
|
elif key in ["b", "n"]:
|
|
NetCollector.switch(key)
|
|
elif key in ["M", "escape"]:
|
|
Menu.main()
|
|
elif key in ["o", "f2"]:
|
|
Menu.options()
|
|
elif key in ["h", "f1"]:
|
|
Menu.help()
|
|
elif key == "z":
|
|
NetCollector.reset = not NetCollector.reset
|
|
Collector.collect(NetCollector, redraw=True)
|
|
elif key == "a":
|
|
NetCollector.auto_min = not NetCollector.auto_min
|
|
NetCollector.net_min = {"download" : -1, "upload" : -1}
|
|
Collector.collect(NetCollector, redraw=True)
|
|
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 == "c":
|
|
CONFIG.proc_per_core = not CONFIG.proc_per_core
|
|
Collector.collect(ProcCollector, interrupt=True, redraw=True)
|
|
elif key == "g":
|
|
CONFIG.mem_graphs = not CONFIG.mem_graphs
|
|
Collector.collect(MemCollector, interrupt=True, redraw=True)
|
|
elif key == "s":
|
|
CONFIG.swap_disk = not CONFIG.swap_disk
|
|
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 == "m":
|
|
Box.mini_mode = not Box.mini_mode
|
|
Draw.clear(saved=True)
|
|
Term.refresh(force=True)
|
|
elif key.lower() in ["t", "k", "i"] and (ProcBox.selected > 0 or ProcCollector.detailed):
|
|
pid: int = ProcBox.selected_pid if ProcBox.selected > 0 else ProcCollector.detailed_pid # type: ignore
|
|
if psutil.pid_exists(pid):
|
|
if key == "t": sig = signal.SIGTERM
|
|
elif key == "k": sig = signal.SIGKILL
|
|
elif key == "i": sig = signal.SIGINT
|
|
try:
|
|
os.kill(pid, sig)
|
|
except Exception as e:
|
|
errlog.error(f'Exception when sending signal {sig} to pid {pid}')
|
|
errlog.exception(f'{e}')
|
|
elif key == "delete" and ProcCollector.search_filter:
|
|
ProcCollector.search_filter = ""
|
|
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True)
|
|
elif key == "enter":
|
|
if ProcBox.selected > 0 and ProcCollector.detailed_pid != ProcBox.selected_pid and psutil.pid_exists(ProcBox.selected_pid):
|
|
ProcCollector.detailed = True
|
|
ProcBox.last_selection = ProcBox.selected
|
|
ProcBox.selected = 0
|
|
ProcCollector.detailed_pid = ProcBox.selected_pid
|
|
ProcBox.resized = True
|
|
elif ProcCollector.detailed:
|
|
ProcBox.selected = ProcBox.last_selection
|
|
ProcBox.last_selection = 0
|
|
ProcCollector.detailed = False
|
|
ProcCollector.detailed_pid = None
|
|
ProcBox.resized = True
|
|
else:
|
|
continue
|
|
ProcCollector.details = {}
|
|
ProcCollector.details_cpu = []
|
|
ProcCollector.details_mem = []
|
|
Graphs.detailed_cpu = NotImplemented
|
|
Graphs.detailed_mem = NotImplemented
|
|
Collector.collect(ProcCollector, proc_interrupt=True, redraw=True)
|
|
|
|
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)
|
|
|
|
|
|
#? Pre main -------------------------------------------------------------------------------------->
|
|
|
|
|
|
CPU_NAME: str = get_cpu_name()
|
|
|
|
|
|
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))
|
|
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}'
|
|
Draw.buffer("+banner", f'{Mv.to(Term.height // 2 - 2 + _i, Term.width // 2 - 28)}{Fx.trans(perc)}{Symbol.v_line}')
|
|
|
|
Draw.out("banner")
|
|
Draw.buffer("+init!", f'{Color.fg("#cc")}{Fx.b}{Mv.to(Term.height // 2 - 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
|
|
|
|
cls.draw_bg(10)
|
|
Draw.buffer("+init!", f'{Mv.restore}{Symbol.ok}\n{Mv.r(Term.width // 2 - 22)}{Mv.save}')
|
|
|
|
@classmethod
|
|
def draw_bg(cls, times: int = 10):
|
|
for _ in range(times):
|
|
sleep(0.05)
|
|
x = randint(0, 100)
|
|
Draw.buffer("initbg", f'{Fx.ub}{Mv.to(0, 0)}{cls.initbg_up(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:
|
|
cls.draw_bg(20)
|
|
Draw.clear("initbg", "banner", "init", saved=True)
|
|
del cls.initbg_up, cls.initbg_down, cls.initbg_data, cls.initbg_colors
|
|
|
|
|
|
#? 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, Term.title("BpyTOP"))
|
|
Term.echo(False)
|
|
Term.refresh()
|
|
if CONFIG.update_check: UpdateChecker.run()
|
|
|
|
#? Draw banner and init status
|
|
if CONFIG.show_init:
|
|
Init.success(start=True)
|
|
|
|
#? Load theme
|
|
if CONFIG.show_init:
|
|
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Loading theme and creating colors... ")}{Mv.save}')
|
|
try:
|
|
THEME: Theme = Theme(CONFIG.color_theme)
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
#? Setup boxes
|
|
if CONFIG.show_init:
|
|
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Doing some maths and drawing... ")}{Mv.save}')
|
|
try:
|
|
if CONFIG.check_temp: CpuCollector.get_sensors()
|
|
Box.calc_sizes()
|
|
Box.draw_bg(now=False)
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
#? 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}')
|
|
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)
|
|
else:
|
|
Init.success()
|
|
|
|
#? 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}')
|
|
try:
|
|
Key.start()
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
#? 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}')
|
|
try:
|
|
Collector.start()
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
#? Collect data and draw to buffer
|
|
if CONFIG.show_init:
|
|
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Collecting data and drawing... ")}{Mv.save}')
|
|
try:
|
|
Collector.collect(draw_now=False)
|
|
pass
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
#? Draw to screen
|
|
if CONFIG.show_init:
|
|
Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Finishing up... ")}{Mv.save}')
|
|
try:
|
|
Collector.collect_done.wait()
|
|
except Exception as e:
|
|
Init.fail(e)
|
|
else:
|
|
Init.success()
|
|
|
|
|
|
Init.done()
|
|
Term.refresh()
|
|
Draw.out(clear=True)
|
|
if CONFIG.draw_clock:
|
|
Box.clock_on = True
|
|
if DEBUG: TimeIt.stop("Init")
|
|
|
|
#? Main loop ------------------------------------------------------------------------------------->
|
|
|
|
def main():
|
|
while not False:
|
|
Term.refresh()
|
|
Timer.stamp()
|
|
|
|
while Timer.not_zero():
|
|
if Key.input_wait(Timer.left()):
|
|
process_keys()
|
|
|
|
Collector.collect()
|
|
|
|
#? Start main loop
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
errlog.exception(f'{e}')
|
|
clean_quit(1)
|
|
else:
|
|
#? Quit cleanly even if false starts being true...
|
|
clean_quit()
|