From 5e622476d97d90dd110671b84d9580af3224ab86 Mon Sep 17 00:00:00 2001 From: aristocratos Date: Thu, 30 Jul 2020 04:29:40 +0200 Subject: [PATCH] Added details box --- bpytop.py | 430 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 312 insertions(+), 118 deletions(-) diff --git a/bpytop.py b/bpytop.py index 4fa7dba..ba24235 100755 --- a/bpytop.py +++ b/bpytop.py @@ -23,7 +23,6 @@ from datetime import timedelta from _thread import interrupt_main from collections import defaultdict from select import select -from itertools import cycle from distutils.util import strtobool from string import Template from math import ceil, floor @@ -291,7 +290,8 @@ def timeit_decorator(func): class Config: '''Holds all config variables and functions for loading from and saving to disk''' - keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_colors", "proc_gradient", "proc_per_core", "disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "show_init"] + keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_colors", "proc_gradient", "proc_per_core", "disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "show_init", + "mini_mode"] conf_dict: Dict[str, Union[str, int, bool]] = {} color_theme: str = "Default" update_ms: int = 2000 @@ -312,6 +312,7 @@ class Config: swap_disk: bool = True show_disks: bool = True show_init: bool = True + mini_mode: bool = False log_level: str = "WARNING" warnings: List[str] = [] @@ -444,11 +445,11 @@ class Term: winch = threading.Event() @classmethod - def refresh(cls, *args): + 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): return + if (cls._w, cls._h) == (cls.width, cls.height) and not force: return while (cls._w, cls._h) != (cls.width, cls.height) or (cls._w < 80 or cls._h < 24): if Init.running: Init.resized = True cls.resized = True @@ -1141,6 +1142,11 @@ class Symbol: 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")}' @@ -1267,8 +1273,8 @@ class Graphs: cores: List[Graph] = [NotImplemented] * THREADS temps: List[Graph] = [NotImplemented] * (THREADS + 1) net: Dict[str, Graph] = {} - detailed_cpu: Graph - detailed_mem: Graph + detailed_cpu: Graph = NotImplemented + detailed_mem: Graph = NotImplemented pid_cpu: Dict[int, Graph] = {} class Meter: @@ -1340,6 +1346,7 @@ class Box: y: int width: int height: int + mini_mode: bool = CONFIG.mini_mode or False out: str bg: str _b_cpu_h: int @@ -1393,8 +1400,12 @@ class CpuBox(Box, SubBox): @classmethod def _calc_size(cls): + 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 * cls.height_p / 100) + cls.height = round(Term.height * height_p / 100) + if cls.height < 8: cls.height = 8 Box._b_cpu_h = cls.height #THREADS = 64 cls.box_columns = ceil((THREADS + 1) / (cls.height - 5)) @@ -1424,12 +1435,15 @@ class CpuBox(Box, SubBox): 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: + Key.mouse["i"] = [[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.title("min")}{THEME.hi_fg("i")}{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 - 9 - 13 if CONFIG.check_temp else bw - 9), "cpu") @@ -1441,6 +1455,7 @@ class CpuBox(Box, SubBox): 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 @@ -1487,7 +1502,7 @@ class CpuBox(Box, SubBox): out += f'{Mv.to(y + h - 1, x + 1)}{THEME.inactive_fg}up {cpu.uptime}' - Draw.buffer(cls.buffer, f'{out}{Term.fg}', only_save=Menu.active) + Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.resized = cls.redraw = False class MemBox(Box): @@ -1549,6 +1564,7 @@ class MemBox(Box): @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: @@ -1560,6 +1576,7 @@ class MemBox(Box): @classmethod def _draw_fg(cls): + if cls.mini_mode: return mem = MemCollector if mem.redraw: cls.redraw = True out: str = "" @@ -1700,11 +1717,13 @@ class NetBox(Box, SubBox): @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 @@ -1757,6 +1776,7 @@ class ProcBox(Box): x = 1 y = 1 current_y: int = 0 + current_h: int = 0 select_max: int = 0 selected: int = 0 selected_pid: int = 0 @@ -1764,6 +1784,7 @@ class ProcBox(Box): moved: bool = False start: int = 1 count: int = 0 + s_len: int = 0 detailed: bool = False detailed_x: int = 0 detailed_y: int = 0 @@ -1777,14 +1798,19 @@ class ProcBox(Box): @classmethod def _calc_size(cls): - cls.width = round(Term.width * cls.width_p / 100) - cls.height = round(Term.height * cls.height_p / 100) + 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.detailed_x = cls.x - cls.detailed_y = cls.y - cls.detailed_height = 8 - cls.detailed_width = cls.width + cls.current_y = cls.y + cls.current_h = cls.height + cls.select_max = cls.height - 3 cls.redraw = True cls.resized = True @@ -1795,6 +1821,7 @@ class ProcBox(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 @@ -1822,15 +1849,20 @@ class ProcBox(Box): 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 and mouse_pos[1] < cls.current_y + cls.select_max: - if mouse_pos[1] == cls.current_y + 1: + 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 + cls.select_max - 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 - 1) * ((ProcCollector.num_procs - cls.select_max - 2) / (cls.select_max - 2))) + cls.start = round((mouse_pos[1] - cls.current_y) * ((ProcCollector.num_procs - cls.select_max - 2) / (cls.select_max - 2))) else: - cls.selected = mouse_pos[1] - cls.current_y if mouse_pos[1] >= cls.current_y else cls.selected + 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: + cls.selected = new_sel elif key == "mouse_unselect": cls.selected = 0 @@ -1843,7 +1875,7 @@ class ProcBox(Box): if old != (cls.start, cls.selected): cls.moved = True - Collector.collect(ProcCollector, proc_interrupt=True, only_draw=True) + Collector.collect(ProcCollector, proc_interrupt=True, redraw=True, only_draw=True) @classmethod @@ -1854,19 +1886,26 @@ class ProcBox(Box): out: str = "" out_misc: str = "" n: int = 0 - x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2 + 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 - loc_string: str 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 + dx, dw = x + dgw + 2, w - dgw - 1 + dy = cls.y + 1 + if w > 67: arg_len = w - 53 - (1 if proc.num_procs > cls.select_max else 0) prog_len = 15 @@ -1877,49 +1916,156 @@ class ProcBox(Box): tree_len = arg_len + prog_len + 6 arg_len = 0 + #* Buttons and titles only redrawn if needed if cls.resized or cls.redraw: - cls.select_max = h - 1 - cls.current_y = y + 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", "o", "g", "c", "t", "k", "u", "enter"]: + 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 "u" in Key.mouse: Key.mouse["u"] = [[dx + 19 + i, dy-1] for i in range(9)] + out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{title}interr{hi}u{title}pt{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 Key.mouse["left"] = [[sort_pos + i, y-1] for i in range(3)] Key.mouse["right"] = [[sort_pos + len(CONFIG.proc_sorting) + 3 + i, y-1] for i in range(3)] - Key.mouse["e"] = [[sort_pos - 5 + i, y-1] for i in range(4)] - out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.h_line * (w - 9))}' + + 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 > 37 + s_len: + + 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 > 45 + s_len: - Key.mouse["r"] = [[sort_pos - 14 + i, y-1] for i in range(7)] + 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 > 55 + s_len: - Key.mouse["o"] = [[sort_pos - 24 + i, y-1] for i in range(8)] + if w > 47 + s_len: + if not "o" in Key.mouse: Key.mouse["o"] = [[sort_pos - 24 + i, y-1] for i in range(8)] out_misc += (f'{Mv.to(y-1, sort_pos - 25)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_per_core else ""}' f'{THEME.title("per-c")}{THEME.hi_fg("o")}{THEME.title("re")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') - if w > 65 + s_len: - Key.mouse["g"] = [[sort_pos - 34 + i, y-1] for i in range(8)] + 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 > 73 + s_len: - Key.mouse["c"] = [[sort_pos - 42 + i, y-1] for i in range(6)] + 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)}') - 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 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: - Key.mouse["delete"] = [[x+12 + len(proc.search_filter[-10:]) + i, y-1] for i in range(3)] + 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 "u" in Key.mouse: Key.mouse["u"] = [[x + 39 + i, y+h] for i in range(9)] + out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{title}interr{hi}u{title}pt{Fx.ub}{THEME.proc_box(Symbol.title_right)}' + Draw.buffer("proc_misc", out_misc, only_save=True) + #* Detailed box draw + if proc.detailed: + expand: bool = True if cls.width >= 115 and "nice" in proc.details else False + 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 = "" + iw = round((dw - 3) // (8 if expand else 4)) + 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)}C{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}}{"Elapsed:":^{iw}}{"Parent:":^{iw}}{"User:":^{iw}}' + + (f'{"Threads:":^{iw}}{"Nice:":^{iw}}{"IO Read:":^{iw}}{"IO Write:":^{iw}}{Fx.ub}' if expand else Fx.ub) + + f'{Mv.to(dy+1, dx+1)}{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}}{proc.details["username"]:^{iw}.{iw2}}' + + (f'{proc.details["threads"]:^{iw}.{iw2}}{proc.details["nice"]:^{iw}.{iw2}}{proc.details["io_read"]:^{iw}.{iw2}}{proc.details["io_write"]:^{iw}.{iw2}}' if expand else "") + + f'{Mv.to(dy+3, dx)}{THEME.title}{Fx.b}{"Memory: " + 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"]}{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 + + + #* Processes labels selected: str = CONFIG.proc_sorting if selected == "memory": selected = "mem" if selected == "threads" and not CONFIG.proc_tree and not arg_len: selected = "tr" @@ -1936,6 +2082,7 @@ class ProcBox(Box): out = out.replace(selected, f'{Fx.u}{selected}{Fx.uu}') cy = 1 + #* 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 @@ -1943,7 +2090,7 @@ class ProcBox(Box): 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 for n, (pid, items) in enumerate(proc.processes.items(), start=1): if n < cls.start: continue l_count += 1 @@ -1963,6 +2110,7 @@ class ProcBox(Box): else: cls.pid_counter[pid] = 0 + #* Move variables and sizes around for correct display if tree view is enabled if CONFIG.proc_tree: indent = name name = cmd @@ -1992,6 +2140,7 @@ class ProcBox(Box): 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 "") + @@ -2001,8 +2150,8 @@ class ProcBox(Box): 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: - #if is_selected: c_color = THEME.proc_misc out += f'{Mv.to(y+cy, x + w - 11)}{c_color if CONFIG.proc_colors else THEME.proc_misc}{Graphs.pid_cpu[pid](None if cls.moved else round(cpu))}{THEME.main_fg}' 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 ""}' @@ -2011,13 +2160,12 @@ class ProcBox(Box): if cy < h: for i in range(h-cy): out += f'{Mv.to(y+cy+i, x)}{" " * w}' - loc_string = f'{cls.start + cls.selected - 1}/{proc.num_procs}' - out += (f'{Mv.to(y+h, x + w - 15 - len(loc_string))}{THEME.proc_box}{Symbol.h_line*10}{Symbol.title_left}{THEME.title}' - f'{Fx.b}{loc_string}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') + #* Draw scrollbar if needed if proc.num_procs > cls.select_max: - Key.mouse["mouse_scroll_up"] = [[x+w-1, y]] - Key.mouse["mouse_scroll_down"] = [[x+w-1, y+h-1]] + 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 @@ -2025,14 +2173,20 @@ class ProcBox(Box): 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}{out_misc}{Term.fg}', only_save=Menu.active) + + Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.redraw = cls.resized = cls.moved = False class Collector: @@ -2571,8 +2725,10 @@ class ProcCollector(Collector): #! add interrupt on _collect and _draw processes: Dict = {} num_procs: int = 0 detailed: bool = False - details: Dict[str, Union[str, int, float, NamedTuple]] = {} + detailed_pid: Union[int, None] = None + details: Dict[str, Any] = {} details_cpu: List[int] = [] + details_mem: List[int] = [] proc_dict: Dict = {} p_values: List[str] = ["pid", "name", "cmdline", "num_threads", "username", "memory_percent", "cpu_percent", "cpu_times", "create_time"] sort_expr: Dict = {} @@ -2596,39 +2752,6 @@ class ProcCollector(Collector): #! add interrupt on _collect and _draw err: float = 0.0 n: int = 0 - if cls.detailed and not cls.details.get("killed", False): - try: - det = psutil.Process(ProcBox.selected_pid) - except (psutil.NoSuchProcess, psutil.ZombieProcess): - cls.details["killed"] = True - cls.details["status"] = psutil.STATUS_DEAD - else: - cls.details = det.as_dict(attrs=["pid", "name", "username", "status", "cmdline", "memory_info", "memory_percent", "create_time", "num_threads", "cpu_percent", "cpu_num"], ad_value="") - if det.parent() != None: cls.details["parent_name"] = det.parent().name() - else: cls.details["parent_name"] = "" - - cls.details["killed"] = False - - if isinstance(cls.details["cmdline"], list): cls.details["cmdline"] = " ".join(cls.details["cmdline"]) or f'[{cls.details["name"]}]' - - if hasattr(cls.details["memory_info"], "rss"): cls.details["memory_bytes"] = floating_humanizer(cls.details["memory_info"].rss) # type: ignore - else: cls.details["memory_bytes"] = "? Bytes" - - if isinstance(cls.details["create_time"], float): cls.details["uptime"] = f'{timedelta(seconds=round(time()-cls.details["create_time"],0))}' - else: cls.details["uptime"] = "??:??:??" - - for v in ["cpu_percent", "memory_percent"]: - if isinstance(cls.details[v], float): - if cls.details[v] >= 100: cls.details[v] = round(cls.details[v]) # type: ignore - elif cls.details[v] >= 10: cls.details[v] = round(cls.details[v], 1) # type: ignore - else: cls.details[v] = round(cls.details[v], 2) # type: ignore - else: - cls.details[v] = 0.0 - - cls.details_cpu.append(round(cls.details["cpu_percent"])) # type: ignore - if len(cls.details_cpu) > ProcBox.width: del cls.details_cpu[0] - - if CONFIG.proc_tree and sorting == "arguments": sorting = "program" @@ -2636,45 +2759,102 @@ class ProcCollector(Collector): #! add interrupt on _collect and _draw if CONFIG.proc_tree: cls._tree(sort_cmd=sort_cmd, reverse=reverse, proc_per_cpu=proc_per_cpu, search=search) - return - - for p in sorted(psutil.process_iter(cls.p_values, err), key=lambda p: eval(sort_cmd), reverse=reverse): - 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: - 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: + for p in sorted(psutil.process_iter(cls.p_values, 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: + 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 - 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"] + cpu = p.info["cpu_percent"] if proc_per_cpu else (p.info["cpu_percent"] / psutil.cpu_count()) + mem = p.info["memory_percent"] - cmd = " ".join(p.info["cmdline"]) or "[" + p.info["name"] + "]" + cmd = " ".join(p.info["cmdline"]) or "[" + p.info["name"] + "]" - out[p.info["pid"]] = { - "name" : p.info["name"], - "cmd" : cmd, - "threads" : p.info["num_threads"], - "username" : p.info["username"], - "mem" : mem, - "cpu" : cpu } + out[p.info["pid"]] = { + "name" : p.info["name"], + "cmd" : cmd, + "threads" : p.info["num_threads"], + "username" : p.info["username"], + "mem" : mem, + "cpu" : cpu } - n += 1 + n += 1 - cls.num_procs = n - cls.processes = out.copy() + cls.num_procs = n + cls.processes = out.copy() + + if cls.detailed and not cls.details.get("killed", False): + expand: bool = True if ProcBox.width >= 115 else 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: + cls.details = det.as_dict(attrs=["status", "memory_info", "create_time", "cpu_num"] + (["nice", "io_counters"] if expand else []), ad_value="") + if det.parent() != None: cls.details["parent_name"] = det.parent().name() + else: cls.details["parent_name"] = "" + + cls.details["pid"] = c_pid + 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)) + cls.details["killed"] = False + + + 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 expand: + if "nice" in cls.details: cls.details["nice"] = f'{cls.details["nice"]}' + if SYSTEM == "BSD": + 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 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 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 hasattr(cls.details["io_counters"], "write_bytes"): cls.details["io_write"] = floating_humanizer(cls.details["io_counters"].write_bytes) + else: cls.details["io_write"] = "?" + + 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): @@ -3087,7 +3267,7 @@ def process_keys(): 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: + 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" @@ -3158,18 +3338,32 @@ def process_keys(): ProcBox.filtering = True if not ProcCollector.search_filter: ProcBox.start = 0 Collector.collect(ProcCollector, redraw=True, only_draw=True) + elif key == "i": + Box.mini_mode = not Box.mini_mode + Draw.clear(saved=True) + Term.refresh(force=True) + elif key in ["t", "k", "u"]: + errlog.debug("kill action") elif key == "delete" and ProcCollector.search_filter: ProcCollector.search_filter = "" Collector.collect(ProcCollector, proc_interrupt=True, redraw=True) - elif key == "enter" and ProcBox.selected > 0: - if ProcCollector.details.get("pid", None) == ProcBox.selected_pid: - ProcCollector.detailed = False - elif psutil.pid_exists(ProcBox.selected_pid): + 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.selected = 0 + ProcCollector.detailed_pid = ProcBox.selected_pid + ProcBox.resized = True + elif ProcCollector.detailed: + 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"]: