diff --git a/bpytop.py b/bpytop.py index 53a2171..a34ec64 100755 --- a/bpytop.py +++ b/bpytop.py @@ -133,6 +133,25 @@ proc_per_core=$proc_per_core #* Show process memory as bytes instead of percent proc_mem_bytes=$proc_mem_bytes +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available, see: +#* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. +#* Select from a list of detected attributes from the options menu +cpu_graph_upper="$cpu_graph_upper" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available, see: +#* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. +#* Select from a list of detected attributes from the options menu +cpu_graph_lower="$cpu_graph_lower" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower=$cpu_invert_lower + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph=$cpu_single_graph + +#* Shows the system uptime in the CPU box. +show_uptime=$show_uptime + #* Check cpu temperature, needs "osx-cpu-temp" on MacOS X. check_temp=$check_temp @@ -385,7 +404,8 @@ class Config: "proc_colors", "proc_gradient", "proc_per_core", "proc_mem_bytes", "disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "use_fstab", "net_download", "net_upload", "net_auto", "net_color_fixed", "show_init", "theme_background", "net_sync", "show_battery", "tree_depth", "cpu_sensor", "show_coretemp", "proc_update_mult", "shown_boxes", "net_iface", "only_physical", - "truecolor", "io_mode", "io_graph_combined", "io_graph_speeds", "show_io_stat"] + "truecolor", "io_mode", "io_graph_combined", "io_graph_speeds", "show_io_stat", "cpu_graph_upper", "cpu_graph_lower", "cpu_invert_lower", + "cpu_single_graph", "show_uptime"] conf_dict: Dict[str, Union[str, int, bool]] = {} color_theme: str = "Default" theme_background: bool = True @@ -401,6 +421,11 @@ class Config: proc_gradient: bool = True proc_per_core: bool = False proc_mem_bytes: bool = True + cpu_graph_upper: str = "total" + cpu_graph_lower: str = "total" + cpu_invert_lower: bool = True + cpu_single_graph: bool = False + show_uptime: bool = True check_temp: bool = True cpu_sensor: str = "Auto" show_coretemp: bool = True @@ -434,6 +459,8 @@ class Config: sorting_options: List[str] = ["pid", "program", "arguments", "threads", "user", "memory", "cpu lazy", "cpu responsive"] log_levels: List[str] = ["ERROR", "WARNING", "INFO", "DEBUG"] + cpu_percent_fields: List = ["total"] + cpu_percent_fields.extend(getattr(psutil.cpu_times_percent(), "_fields", [])) cpu_sensors: List[str] = [ "Auto" ] @@ -536,6 +563,10 @@ class Config: new_config["shown_boxes"] = "_error_" self.warnings.append(f'Config key "shown_boxes" contains invalid box names!') break + for cpu_graph in ["cpu_graph_upper", "cpu_graph_lower"]: + if cpu_graph in new_config and not new_config[cpu_graph] in self.cpu_percent_fields: + new_config[cpu_graph] = "_error_" + self.warnings.append(f'Config key "{cpu_graph}" does not contain an available cpu stat attribute!') return new_config def save_config(self): @@ -1600,6 +1631,7 @@ class Box: clock_custom_format: Dict[str, Any] = { "/host" : os.uname()[1], "/user" : os.environ.get("USER") or pwd.getpwuid(os.getuid())[0], + "/uptime" : "", } if clock_custom_format["/host"].endswith(".local"): clock_custom_format["/host"] = clock_custom_format["/host"].replace(".local", "") @@ -1637,6 +1669,7 @@ class Box: clock_string = cls.clock = strftime(CONFIG.draw_clock) for custom in cls.clock_custom_format: if custom in clock_string: + if custom == "/uptime": cls.clock_custom_format["/uptime"] = CpuCollector.uptime clock_string = clock_string.replace(custom, cls.clock_custom_format[custom]) clock_len = len(clock_string[:(CpuBox.width-56)]) if cls.clock_len != clock_len and not CpuBox.resized: @@ -1805,6 +1838,13 @@ class CpuBox(Box, SubBox): 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) + hh2: int = h - hh + mid_line: bool = False + if not CONFIG.cpu_single_graph and CONFIG.cpu_graph_upper != CONFIG.cpu_graph_lower: + mid_line = True + if h % 2: hh = floor(h / 2) + else: hh2 -= 1 + hide_cores: bool = (cpu.cpu_temp_only or not CONFIG.show_coretemp) and cpu.got_sensors ct_width: int = (max(6, 6 * cls.column_size)) * hide_cores @@ -1812,8 +1852,9 @@ class CpuBox(Box, SubBox): if not "m" in Key.mouse: Key.mouse["m"] = [[cls.x + 16 + i, cls.y] for i in range(12)] out_misc += f'{Mv.to(cls.y, cls.x + 16)}{THEME.cpu_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("m")}{THEME.title}ode:{Box.view_mode}{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) + Graphs.cpu["up"] = Graph(w - bw - 3, (h if CONFIG.cpu_single_graph else hh), THEME.gradient["cpu"], cpu.cpu_upper) + if not CONFIG.cpu_single_graph: + Graphs.cpu["down"] = Graph(w - bw - 3, hh2, THEME.gradient["cpu"], cpu.cpu_lower, invert=CONFIG.cpu_invert_lower) Meters.cpu = Meter(cpu.cpu_usage[0][-1], bw - (21 if cpu.got_sensors else 9), "cpu") if cls.column_size > 0 or ct_width > 0: for n in range(THREADS): @@ -1861,8 +1902,13 @@ class CpuBox(Box, SubBox): 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])}' + out += f'{Mv.to(y, x)}{Graphs.cpu["up"](None if cls.resized else cpu.cpu_upper[-1])}' + if mid_line: + out += (f'{Mv.to(y+hh, x-1)}{THEME.cpu_box(Symbol.title_right)}{THEME.div_line}{Symbol.h_line * (w - bw - 3)}{THEME.div_line(Symbol.title_left)}' + f'{Mv.to(y+hh, x+((w-bw)//2)-((len(CONFIG.cpu_graph_upper)+len(CONFIG.cpu_graph_lower))//2))}{THEME.main_fg}{CONFIG.cpu_graph_upper}{Mv.r(1)}▲▼{Mv.r(1)}{CONFIG.cpu_graph_lower}') + if not CONFIG.cpu_single_graph and Graphs.cpu.get("down"): + out += f'{Mv.to(y + hh + (1 * mid_line), x)}{Graphs.cpu["down"](None if cls.resized else cpu.cpu_lower[-1])}' + out += (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: try: @@ -1909,7 +1955,8 @@ class CpuBox(Box, SubBox): 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.graph_text}up {cpu.uptime}' + if CONFIG.show_uptime: + out += f'{Mv.to(y + (0 if not CONFIG.cpu_invert_lower or CONFIG.cpu_single_graph else h - 1), x + 1)}{THEME.graph_text}{Fx.trans("up " + cpu.uptime)}' Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) @@ -2877,6 +2924,8 @@ class Collector: class CpuCollector(Collector): '''Collects cpu usage for cpu and cores, cpu frequency, load_avg, uptime and cpu temps''' cpu_usage: List[List[int]] = [] + cpu_upper: List[int] = [] + cpu_lower: List[int] = [] cpu_temp: List[List[int]] = [] cpu_temp_high: int = 0 cpu_temp_crit: int = 0 @@ -2932,6 +2981,15 @@ class CpuCollector(Collector): if len(cls.cpu_usage[0]) > Term.width * 4: del cls.cpu_usage[0][0] + cpu_times_percent = psutil.cpu_times_percent() + for x in ["upper", "lower"]: + if getattr(CONFIG, "cpu_graph_" + x) == "total": + setattr(cls, "cpu_" + x, cls.cpu_usage[0]) + else: + getattr(cls, "cpu_" + x).append(round(getattr(cpu_times_percent, getattr(CONFIG, "cpu_graph_" + x)))) + if len(getattr(cls, "cpu_" + x)) > Term.width * 4: + del getattr(cls, "cpu_" + x)[0] + 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: @@ -2947,7 +3005,7 @@ class CpuCollector(Collector): else: pass cls.load_avg = [round(lavg, 2) for lavg in psutil.getloadavg()] - cls.uptime = str(timedelta(seconds=round(time()-psutil.boot_time(),0)))[:-3] + cls.uptime = str(timedelta(seconds=round(time()-psutil.boot_time(),0)))[:-3].replace(" days,", "d").replace(" day,", "d") if CONFIG.check_temp and cls.got_sensors: cls._collect_temps() @@ -4176,6 +4234,7 @@ class Menu: 'Custom formatting options:', '"/host" = hostname', '"/user" = username', + '"/uptime" = system uptime', '', 'Examples of strftime formats:', '"%X" = locale HH:MM:SS', @@ -4213,6 +4272,39 @@ class Menu: 'i.e. "DEBUG" will show all logging info.'] }, "cpu" : { + "cpu_graph_upper" : [ + 'Sets the CPU stat shown in upper half of', + 'the CPU graph.', + '', + '"total" = Total cpu usage.', + '"user" = User mode cpu usage.', + '"system" = Kernel mode cpu usage.', + 'See:', + 'https://psutil.readthedocs.io/en/latest/', + '#psutil.cpu_times', + 'for attributes available on specific platforms.'], + "cpu_graph_lower" : [ + 'Sets the CPU stat shown in lower half of', + 'the CPU graph.', + '', + '"total" = Total cpu usage.', + '"user" = User mode cpu usage.', + '"system" = Kernel mode cpu usage.', + 'See:', + 'https://psutil.readthedocs.io/en/latest/', + '#psutil.cpu_times', + 'for attributes available on specific platforms.'], + "cpu_invert_lower" : [ + 'Toggles orientation of the lower CPU graph.', + '', + 'True or False.'], + "cpu_single_graph" : [ + 'Completely disable the lower CPU graph.', + '', + 'Shows only upper CPU graph and resizes it', + 'to fit to box height.', + '', + 'True or False.'], "check_temp" : [ 'Enable cpu temperature reporting.', '', @@ -4233,6 +4325,13 @@ class Menu: 'Custom cpu model name in cpu percentage box.', '', 'Empty string to disable.'], + "show_uptime" : [ + 'Shows the system uptime in the CPU box.', + '', + 'Can also be shown in the clock by using', + '"/uptime" in the formatting.', + '', + 'True or False.'], }, "mem" : { "mem_graphs" : [ @@ -4423,6 +4522,8 @@ class Menu: loglevel_i: int = CONFIG.log_levels.index(CONFIG.log_level) cpu_sensor_i: int = CONFIG.cpu_sensors.index(CONFIG.cpu_sensor) + cpu_graph_i: Dict[str, int] = { "cpu_graph_upper" : CONFIG.cpu_percent_fields.index(CONFIG.cpu_graph_upper), + "cpu_graph_lower" : CONFIG.cpu_percent_fields.index(CONFIG.cpu_graph_lower)} color_i: int max_opt_len: int = max([len(categories[x]) for x in categories]) * 2 cat_list = list(categories) @@ -4484,11 +4585,13 @@ class Menu: counter = f' {loglevel_i + 1}/{len(CONFIG.log_levels)}' elif opt == "cpu_sensor": counter = f' {cpu_sensor_i + 1}/{len(CONFIG.cpu_sensors)}' + elif opt in ["cpu_graph_upper", "cpu_graph_lower"]: + counter = f' {cpu_graph_i[opt] + 1}/{len(CONFIG.cpu_percent_fields)}' 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", "cpu_sensor"]: + if isinstance(value, bool) or opt in ["color_theme", "proc_sorting", "log_level", "cpu_sensor", "cpu_graph_upper", "cpu_graph_lower"]: 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}' @@ -4687,6 +4790,17 @@ class Menu: 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 in ["left", "right"] and selected in ["cpu_graph_upper", "cpu_graph_lower"]: + if key == "left": + cpu_graph_i[selected] -= 1 + if cpu_graph_i[selected] < 0: cpu_graph_i[selected] = len(CONFIG.cpu_percent_fields) - 1 + if key == "right": + cpu_graph_i[selected] += 1 + if cpu_graph_i[selected] > len(CONFIG.cpu_percent_fields) - 1: cpu_graph_i[selected] = 0 + setattr(CONFIG, selected, CONFIG.cpu_percent_fields[cpu_graph_i[selected]]) + setattr(CpuCollector, selected.replace("_graph", ""), []) + Term.refresh(force=True) + cls.resized = False elif key in ["left", "right"] and selected == "cpu_sensor" and len(CONFIG.cpu_sensors) > 1: if key == "left": cpu_sensor_i -= 1