diff --git a/.gitignore b/.gitignore index 1f957a4..1f7c0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ dmypy.json # nix build /result + +# vs code +.vscode/ diff --git a/auto_cpufreq/bin/auto_cpufreq.py b/auto_cpufreq/bin/auto_cpufreq.py index d9fcd37..5fc9c21 100755 --- a/auto_cpufreq/bin/auto_cpufreq.py +++ b/auto_cpufreq/bin/auto_cpufreq.py @@ -13,7 +13,9 @@ from auto_cpufreq.battery_scripts.battery import * from auto_cpufreq.config.config import config as conf, find_config_file from auto_cpufreq.core import * from auto_cpufreq.globals import GITHUB, IS_INSTALLED_WITH_AUR, IS_INSTALLED_WITH_SNAP +from auto_cpufreq.modules.system_monitor import ViewType, SystemMonitor from auto_cpufreq.power_helper import * +from threading import Thread @click.command() @click.option("--monitor", is_flag=True, help="Monitor and see suggestions for CPU optimizations") @@ -55,11 +57,8 @@ def main(monitor, live, daemon, install, update, remove, force, config, stats, g set_override(force) # Calling set override, only if force has some values if monitor: - config_info_dialog() root_check() - print('\nNote: You can quit monitor mode by pressing "ctrl+c"') battery_setup() - battery_get_thresholds() conf.notifier.start() if IS_INSTALLED_WITH_SNAP: gnome_power_detect_snap() @@ -67,26 +66,19 @@ def main(monitor, live, daemon, install, update, remove, force, config, stats, g else: gnome_power_detect() tlp_service_detect() - while True: + + if IS_INSTALLED_WITH_SNAP or tlp_stat_exists or (systemctl_exists and not bool(gnome_power_status)): try: - time.sleep(1) - running_daemon_check() - footer() - gov_check() - cpufreqctl() - distro_info() - sysinfo() - mon_autofreq() - countdown(2) - except KeyboardInterrupt: break - conf.notifier.stop() + input("press Enter to continue or Ctrl + c to exit...") + except KeyboardInterrupt: + conf.notifier.stop() + sys.exit(0) + + monitor = SystemMonitor(suggestion=True, type=ViewType.MONITOR) + monitor.run(on_quit=conf.notifier.stop) elif live: root_check() - config_info_dialog() - print('\nNote: You can quit live mode by pressing "ctrl+c"') - time.sleep(1) battery_setup() - battery_get_thresholds() conf.notifier.start() if IS_INSTALLED_WITH_SNAP: gnome_power_detect_snap() @@ -96,22 +88,40 @@ def main(monitor, live, daemon, install, update, remove, force, config, stats, g gnome_power_stop_live() tuned_stop_live() tlp_service_detect() - while True: + + if IS_INSTALLED_WITH_SNAP or tlp_stat_exists or (systemctl_exists and not bool(gnome_power_status)): try: - running_daemon_check() - footer() - gov_check() - cpufreqctl() - distro_info() - sysinfo() - set_autofreq() - countdown(2) + input("press Enter to continue or Ctrl + c to exit...") except KeyboardInterrupt: - gnome_power_start_live() - tuned_start_live() - print() - break - conf.notifier.stop() + conf.notifier.stop() + sys.exit(0) + + cpufreqctl() + def live_daemon(): + # Redirect stdout to suppress prints + class NullWriter: + def write(self, _): pass + def flush(self): pass + try: + sys.stdout = NullWriter() + + while True: + time.sleep(1) + set_autofreq() + except: + pass + + def live_daemon_off(): + gnome_power_start_live() + tuned_start_live() + cpufreqctl_restore() + conf.notifier.stop() + + thread = Thread(target=live_daemon, daemon=True) + thread.start() + + monitor = SystemMonitor(type=ViewType.LIVE) + monitor.run(on_quit=live_daemon_off) elif daemon: config_info_dialog() root_check() @@ -205,15 +215,22 @@ def main(monitor, live, daemon, install, update, remove, force, config, stats, g elif stats: not_running_daemon_check() config_info_dialog() - print('\nNote: You can quit stats mode by pressing "ctrl+c"') if IS_INSTALLED_WITH_SNAP: gnome_power_detect_snap() tlp_service_detect_snap() else: gnome_power_detect() tlp_service_detect() - battery_get_thresholds() - read_stats() + + if IS_INSTALLED_WITH_SNAP or tlp_stat_exists or (systemctl_exists and not bool(gnome_power_status)): + try: + input("press Enter to continue or Ctrl + c to exit...") + except KeyboardInterrupt: + conf.notifier.stop() + sys.exit(0) + + monitor = SystemMonitor(type=ViewType.STATS) + monitor.run() elif get_state: not_running_daemon_check() override = get_override() diff --git a/auto_cpufreq/core.py b/auto_cpufreq/core.py index f03e7de..6981d8a 100755 --- a/auto_cpufreq/core.py +++ b/auto_cpufreq/core.py @@ -435,7 +435,9 @@ def get_load(): print("\nTotal CPU usage:", cpuload, "%") print("Total system load: {:.2f}".format(load1m)) - print("Average temp. of all cores: {:.2f} °C \n".format(avg_all_core_temp)) + from auto_cpufreq.modules.system_info import SystemInfo + + print("Average temp. of all cores: {:.2f} °C \n".format(SystemInfo.avg_temp())) return cpuload, load1m @@ -574,7 +576,9 @@ def set_powersave(): if cpuload >= 20: set_turbo(True) # high cpu usage trigger else: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + from auto_cpufreq.modules.system_info import SystemInfo + + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") set_turbo(False) footer() @@ -591,7 +595,9 @@ def mon_powersave(): if cpuload >= 20: print("suggesting to set turbo boost: on") # high cpu usage trigger else: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + from auto_cpufreq.modules.system_info import SystemInfo + + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") print("suggesting to set turbo boost: off") get_turbo() @@ -672,32 +678,37 @@ def set_performance(): print("Configuration file disables turbo boost") set_turbo(False) else: + from auto_cpufreq.modules.system_info import SystemInfo + if ( psutil.cpu_percent(percpu=False, interval=0.01) >= 20.0 or max(psutil.cpu_percent(percpu=True, interval=0.01)) >= 75 ): print("High CPU load", end=""), display_system_load_avg() + if cpuload >= 20: set_turbo(True) # high cpu usage trigger - elif avg_all_core_temp >= 70: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + elif SystemInfo.avg_temp() >= 70: # set turbo state based on average of all core temperatures + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") set_turbo(False) else: set_turbo(True) elif load1m >= performance_load_threshold: + print("High system load", end=""), display_system_load_avg() if cpuload >= 20: set_turbo(True) # high cpu usage trigger - elif avg_all_core_temp >= 65: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + elif SystemInfo.avg_temp() >= 65: # set turbo state based on average of all core temperatures + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") set_turbo(False) else: set_turbo(True) else: print("Load optimal", end=""), display_system_load_avg() if cpuload >= 20: set_turbo(True) # high cpu usage trigger else: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") set_turbo(False) footer() def mon_performance(): + from auto_cpufreq.modules.system_info import SystemInfo cpuload, load1m = get_load() if ( @@ -705,12 +716,14 @@ def mon_performance(): or max(psutil.cpu_percent(percpu=True, interval=0.01)) >= 75 ): print("High CPU load", end=""), display_system_load_avg() + + if cpuload >= 20: # high cpu usage trigger print("suggesting to set turbo boost: on") get_turbo() # set turbo state based on average of all core temperatures - elif cpuload <= 25 and avg_all_core_temp >= 70: - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + elif cpuload <= 25 and SystemInfo.avg_temp() >= 70: + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") print("suggesting to set turbo boost: off") get_turbo() else: @@ -721,8 +734,8 @@ def mon_performance(): if cpuload >= 20: # high cpu usage trigger print("suggesting to set turbo boost: on") get_turbo() - elif cpuload <= 25 and avg_all_core_temp >= 65: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + elif cpuload <= 25 and SystemInfo.avg_temp() >= 65: # set turbo state based on average of all core temperatures + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") print("suggesting to set turbo boost: off") get_turbo() else: @@ -733,8 +746,8 @@ def mon_performance(): if cpuload >= 20: # high cpu usage trigger print("suggesting to set turbo boost: on") get_turbo() - elif cpuload <= 25 and avg_all_core_temp >= 60: # set turbo state based on average of all core temperatures - print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {avg_all_core_temp}°C") + elif cpuload <= 25 and SystemInfo.avg_temp() >= 60: # set turbo state based on average of all core temperatures + print(f"Optimal total CPU usage: {cpuload}%, high average core temp: {SystemInfo.avg_temp()}°C") print("suggesting to set turbo boost: off") get_turbo() else: @@ -895,11 +908,6 @@ def sysinfo(): if offline_cpus: print(f"\nDisabled CPUs: {','.join(offline_cpus)}") - # get average temperature of all cores - avg_cores_temp = sum(temp_per_cpu) - global avg_all_core_temp - avg_all_core_temp = float(avg_cores_temp / online_cpu_count) - # print current fan speed current_fans = list(psutil.sensors_fans()) for current_fan in current_fans: print("\nCPU fan speed:", psutil.sensors_fans()[current_fan][0].current, "RPM") diff --git a/auto_cpufreq/modules/system_info.py b/auto_cpufreq/modules/system_info.py new file mode 100644 index 0000000..d224297 --- /dev/null +++ b/auto_cpufreq/modules/system_info.py @@ -0,0 +1,330 @@ +from dataclasses import dataclass +import os +from pathlib import Path +import platform +from subprocess import getoutput +from typing import Tuple, List +import psutil +import distro +from pathlib import Path +from auto_cpufreq.config.config import config +from auto_cpufreq.core import get_power_supply_ignore_list +from auto_cpufreq.globals import ( + AVAILABLE_GOVERNORS_SORTED, + IS_INSTALLED_WITH_SNAP, + POWER_SUPPLY_DIR, +) + + +@dataclass +class CoreInfo: + id: int + usage: float + temperature: float + frequency: float + + +@dataclass +class BatteryInfo: + is_charging: bool | None + is_ac_plugged: bool | None + charging_start_threshold: int | None + charging_stop_threshold: int | None + battery_level: int | None + power_consumption: float | None + + def __repr__(self) -> str: + if self.is_charging: + return "charging" + elif not self.is_ac_plugged: + return f"discharging {('(' + '{:.2f}'.format(self.power_consumption) + ' W)') if self.power_consumption != None else ''}" + return "Not Charging" + + +@dataclass +class SystemReport: + distro_name: str + distro_ver: str + arch: str + processor_model: str + total_core: int | None + kernel_version: str + current_gov: str | None + current_epp: str | None + current_epb: str | None + cpu_driver: str + cpu_fan_speed: int | None + cpu_usage: float + cpu_max_freq: float | None + cpu_min_freq: float | None + load: float + avg_load: Tuple[float, float, float] | None + cores_info: list[CoreInfo] + battery_info: BatteryInfo + is_turbo_on: Tuple[bool | None, bool | None] + + +class SystemInfo: + """ + Provides system information related to CPU, distribution, and performance metrics. + """ + + def __init__(self): + self.distro_name: str = ( + distro.name(pretty=True) if not IS_INSTALLED_WITH_SNAP else "UNKNOWN" + ) + self.distro_version: str = ( + distro.version() if not IS_INSTALLED_WITH_SNAP else "UNKNOWN" + ) + self.architecture: str = platform.machine() + self.processor_model: str = ( + getoutput("grep -E 'model name' /proc/cpuinfo -m 1").split(":")[-1].strip() + ) + self.total_cores: int | None = psutil.cpu_count(logical=True) + self.cpu_driver: str = getoutput( + "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver" + ).strip() + self.kernel_version: str = platform.release() + + @staticmethod + def cpu_min_freq() -> float | None: + freqs = psutil.cpu_freq(percpu=True) + return min((freq.min for freq in freqs), default=None) + + @staticmethod + def cpu_max_freq() -> float | None: + freqs = psutil.cpu_freq(percpu=True) + return max((freq.max for freq in freqs), default=None) + + @staticmethod + def get_cpu_info() -> List[CoreInfo]: + """Returns detailed CPU information for each core.""" + cpu_usage = psutil.cpu_percent(percpu=True) + cpu_freqs = psutil.cpu_freq(percpu=True) + + try: + temps = psutil.sensors_temperatures() + core_temps = [temp.current for temp in temps.get("coretemp", [])] + except AttributeError: + core_temps = [] + + avg_temp = sum(core_temps) / len(core_temps) if core_temps else 0.0 + + return [ + CoreInfo( + id=i, + usage=cpu_usage[i], + temperature=core_temps[i] if i < len(core_temps) else avg_temp, + frequency=cpu_freqs[i].current, + ) + for i in range(len(cpu_usage)) + ] + + @staticmethod + def cpu_fan_speed() -> int | None: + fans = psutil.sensors_fans() + return next((fan[0].current for fan in fans.values() if fan), None) + + @staticmethod + def current_gov() -> str | None: + try: + with open( + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "r" + ) as f: + return f.read().strip() + except: + return None + + @staticmethod + def current_epp() -> str | None: + epp_path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference" + if not Path(epp_path).exists(): + return None + return config.get_config().get( + "battery", "energy_performance_preference", fallback="balance_power" + ) + + @staticmethod + def current_epb() -> str | None: + epb_path = "/sys/devices/system/cpu/intel_pstate" + if not Path(epb_path).exists(): + return None + return config.get_config().get( + "battery", "energy_perf_bias", fallback="balance_power" + ) + + @staticmethod + def cpu_usage() -> float: + return psutil.cpu_percent( + interval=0.5 + ) # Reduced interval for better responsiveness + + @staticmethod + def system_load() -> float: + return os.getloadavg()[0] + + @staticmethod + def avg_load() -> Tuple[float, float, float]: + return os.getloadavg() + + @staticmethod + def avg_temp() -> int: + temps: List[float] = [i.temperature for i in SystemInfo.get_cpu_info()] + return int(sum(temps) / len(temps)) + + @staticmethod + def turbo_on() -> Tuple[bool | None, bool | None]: + """Get CPU turbo mode status. + + Returns: Tuple[bool | None, bool | None]: + + The first value indicates whether turbo mode is enabled, None if unknown + + The second value indicates whether auto mode is enabled (amd_pstate only), None if unknown + """ + intel_pstate = Path("/sys/devices/system/cpu/intel_pstate/no_turbo") + cpu_freq = Path("/sys/devices/system/cpu/cpufreq/boost") + amd_pstate = Path("/sys/devices/system/cpu/amd_pstate/status") + + if intel_pstate.exists(): + control_file: Path = intel_pstate + inverse_logic = True + elif cpu_freq.exists(): + control_file = cpu_freq + inverse_logic = False + elif amd_pstate.exists(): + amd_status: str = amd_pstate.read_text().strip() + if amd_status == "active": + return None, True + return None, False + else: + return None, None + + try: + current_value = int(control_file.read_text().strip()) + return bool(current_value) ^ inverse_logic, False + except Exception as e: + return None, None + + @staticmethod + def battery_info() -> BatteryInfo: + def read_file(path: str) -> str | None: + try: + with open(path, "r") as f: + return f.read().strip() + except FileNotFoundError: + return None + + power_supplies: List[str] = sorted(os.listdir(POWER_SUPPLY_DIR)) + + if not power_supplies: + return BatteryInfo( + is_charging=None, + is_ac_plugged=True, + charging_start_threshold=None, + charging_stop_threshold=None, + battery_level=None, + power_consumption=None, + ) + + is_ac_plugged = None + is_charging = None + battery_level = None + power_consumption = None + charging_start_threshold = None + charging_stop_threshold = None + + for supply in power_supplies: + if any(item in supply for item in get_power_supply_ignore_list()): + continue + + supply_type: str | None = read_file(f"{POWER_SUPPLY_DIR}{supply}/type") + + if supply_type == "Mains": + power_supply_online: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/online" + ) + is_ac_plugged = power_supply_online == "1" + + elif supply_type == "Battery": + battery_status: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/status" + ) + battery_percentage: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/capacity" + ) + energy_rate: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/power_now" + ) + charge_start_threshold: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/charge_start_threshold" + ) + charge_stop_threshold: str | None = read_file( + f"{POWER_SUPPLY_DIR}{supply}/charge_stop_threshold" + ) + + is_charging: bool | None = ( + battery_status.lower() == "charging" if battery_status else None + ) + battery_level: int | None = ( + int(battery_percentage) if battery_percentage else None + ) + power_consumption: float | None = ( + float(energy_rate) / 1_000_000 if energy_rate else None + ) + charging_start_threshold: int | None = ( + int(charge_start_threshold) if charge_start_threshold else None + ) + charging_stop_threshold: int | None = ( + int(charge_stop_threshold) if charge_stop_threshold else None + ) + + return BatteryInfo( + is_charging=is_charging, + is_ac_plugged=is_ac_plugged, + charging_start_threshold=charging_start_threshold, + charging_stop_threshold=charging_stop_threshold, + battery_level=battery_level, + power_consumption=power_consumption, + ) + + @staticmethod + def turbo_on_suggestion() -> bool: + usage = SystemInfo.cpu_usage() + if usage >= 20.0: + return True + elif usage <= 25 and SystemInfo.avg_temp() >= 70: + return False + return False + + @staticmethod + def governor_suggestion() -> str: + if SystemInfo.battery_info().is_ac_plugged: + return AVAILABLE_GOVERNORS_SORTED[0] + return AVAILABLE_GOVERNORS_SORTED[-1] + + def generate_system_report(self) -> SystemReport: + return SystemReport( + distro_name=self.distro_name, + distro_ver=self.distro_version, + arch=self.architecture, + processor_model=self.processor_model, + total_core=self.total_cores, + cpu_driver=self.cpu_driver, + kernel_version=self.kernel_version, + current_gov=self.current_gov(), + current_epp=self.current_epp(), + current_epb=self.current_epb(), + cpu_fan_speed=self.cpu_fan_speed(), + cpu_usage=self.cpu_usage(), + cpu_max_freq=self.cpu_max_freq(), + cpu_min_freq=self.cpu_min_freq(), + load=self.system_load(), + avg_load=self.avg_load(), + cores_info=self.get_cpu_info(), + is_turbo_on=self.turbo_on(), + battery_info=self.battery_info(), + ) + + +system_info = SystemInfo() diff --git a/auto_cpufreq/modules/system_monitor.py b/auto_cpufreq/modules/system_monitor.py new file mode 100644 index 0000000..f608a41 --- /dev/null +++ b/auto_cpufreq/modules/system_monitor.py @@ -0,0 +1,301 @@ +import sys +from typing import Callable +import urwid +import time +from .system_info import SystemReport, system_info +from auto_cpufreq.config.config import config +from enum import Enum + + +class ViewType(str, Enum): + STATS = "Stats" + MONITOR = "Monitor" + LIVE = "Live" + + def __str__(self) -> str: + return self.value + + +class SystemMonitor: + def __init__(self, type: ViewType, suggestion: bool = False): + self.type: ViewType = type + self.title_header = urwid.Text(f"{type} Mode", align="center") + self.header = urwid.Columns( + [ + self.title_header, + ] + ) + + # Create separate content walkers for left and right columns + self.left_content = urwid.SimpleListWalker([]) + self.right_content = urwid.SimpleListWalker([]) + + # Create listboxes for both columns + self.left_listbox = urwid.ListBox(self.left_content) + self.right_listbox = urwid.ListBox(self.right_content) + + # Create a columns widget with a vertical line (using box drawing character) + self.columns = urwid.Columns( + [ + ("weight", 1, self.left_listbox), + ( + "fixed", + 1, + urwid.AttrMap(urwid.SolidFill("│"), "divider"), + ), # Vertical line # type: ignore + ("weight", 1, self.right_listbox), + ], + dividechars=0, + ) + + self.footer = urwid.AttrMap( + urwid.Text( + "Press Q or Ctrl+C to quit | Use ↑↓ or PageUp/PageDown to scroll", + align="center", + ), + "footer", + ) + + self.frame = urwid.Frame( + body=self.columns, + header=urwid.AttrMap(self.header, "header"), + footer=self.footer, + ) + + palette = [ + ("header", "white", "dark blue"), + ("footer", "white", "dark green"), + ("body", "white", "default"), + ("divider", "light gray", "default"), # Style for the vertical line + ] + + if suggestion: + palette.append(("suggestion", "yellow", "default")) + + self.loop = urwid.MainLoop( + self.frame, palette=palette, unhandled_input=self.handle_input + ) + + self.last_focus_left = 0 + self.last_focus_right = 0 + self.on_quit: Callable[[], None] | None = None + self.suggestion = suggestion + + def update(self, loop: urwid.MainLoop, user_data: dict) -> None: + # Store current focus positions + if len(self.left_content) > 0: + _, self.last_focus_left = self.left_listbox.get_focus() + if self.last_focus_left is None: + self.last_focus_left = 0 + if len(self.right_content) > 0: + _, self.last_focus_right = self.right_listbox.get_focus() + if self.last_focus_right is None: + self.last_focus_right = 0 + + current_time = time.strftime("%H:%M:%S") + self.title_header.set_text(f"{self.type} Mode - {current_time}") + + report: SystemReport = system_info.generate_system_report() + self.format_system_info(report) + + # Restore focus positions + if len(self.left_content) > 0: + self.left_listbox.set_focus(min(self.last_focus_left, len(self.left_content) - 1)) # type: ignore + if len(self.right_content) > 0: + self.right_listbox.set_focus(min(self.last_focus_right, len(self.right_content) - 1)) # type: ignore + + self.loop.set_alarm_in(2, self.update) # type: ignore + + def handle_input(self, key): + if key in ("q", "Q"): + if self.on_quit: + self.on_quit() + raise urwid.ExitMainLoop() + + def format_system_info(self, report: SystemReport): + self.left_content.clear() + self.right_content.clear() + + # Helper function to create centered text + def aligned_text(text: str) -> urwid.Text: + return urwid.Text(text, align="left") + + # Left Column - System Info and CPU Stats + self.left_content.extend( + [ + urwid.AttrMap(aligned_text("System Information"), "header"), + aligned_text(""), + aligned_text(f"Linux distro: {report.distro_name} {report.distro_ver}"), + aligned_text(f"Linux kernel: {report.kernel_version}"), + aligned_text(f"Processor: {report.processor_model}"), + aligned_text(f"Cores: {report.total_core}"), + aligned_text(f"Architecture: {report.arch}"), + aligned_text(f"Driver: {report.cpu_driver}"), + aligned_text(""), + ] + ) + + if config.has_config(): + self.left_content.append( + aligned_text(f"Using settings defined in {config.path} file") + ) + self.left_content.append(aligned_text("")) + + # CPU Stats + self.left_content.extend( + [ + urwid.AttrMap(aligned_text("Current CPU Stats"), "header"), + aligned_text(""), + aligned_text(f"CPU max frequency: {report.cpu_max_freq} MHz"), + aligned_text(f"CPU min frequency: {report.cpu_min_freq} MHz"), + aligned_text(""), + aligned_text("Core Usage Temperature Frequency"), + ] + ) + + for core in report.cores_info: + self.left_content.append( + aligned_text( + f"CPU{core.id:<2} {core.usage:>4.1f}% {core.temperature:>6.0f} °C {core.frequency:>6.0f} MHz" + ) + ) + + if report.cpu_fan_speed: + self.left_content.append(aligned_text("")) + self.left_content.append( + aligned_text(f"CPU fan speed: {report.cpu_fan_speed} RPM") + ) + + # Right Column - Battery, Frequency Scaling, and System Stats + if report.battery_info != None: + self.right_content.extend( + [ + urwid.AttrMap(aligned_text("Battery Stats"), "header"), + aligned_text(""), + aligned_text(f"Battery status: {str(report.battery_info)}"), + aligned_text( + f"Battery precentage: {(str(report.battery_info.battery_level) + '%') if report.battery_info.battery_level != None else 'Unknown'}" + ), + aligned_text( + f'AC plugged: {("Yes" if report.battery_info.is_ac_plugged else "No") if report.battery_info.is_ac_plugged != None else "Unknown"}' + ), + aligned_text( + f'Charging start threshold: {report.battery_info.charging_start_threshold if report.battery_info.is_ac_plugged != None else "Unknown"}' + ), + aligned_text( + f'Charging stop threshold: {report.battery_info.charging_stop_threshold if report.battery_info.is_ac_plugged != None else "Unknown"}' + ), + aligned_text(""), + ] + ) + + # CPU Frequency Scaling + self.right_content.extend( + [ + urwid.AttrMap(aligned_text("CPU Frequency Scaling"), "header"), + aligned_text(""), + aligned_text( + f'Setting to use: "{report.current_gov if report.current_gov != None else "Unknown"}" governor' + ), + ] + ) + + if ( + self.suggestion + and report.current_gov != None + and system_info.governor_suggestion() != report.current_gov + ): + self.right_content.append( + urwid.AttrMap( + aligned_text( + f'Suggesting use of: "{system_info.governor_suggestion()}" governor' + ), + "suggestion", + ) + ) + + if report.current_epp: + self.right_content.append( + aligned_text(f"EPP setting: {report.current_epp}") + ) + else: + self.right_content.append( + aligned_text("Not setting EPP (not supported by system)") + ) + + if report.current_epb: + self.right_content.append( + aligned_text(f'Setting to use: "{report.current_epb}" EPB') + ) + + self.right_content.append(aligned_text("")) + + # System Statistics + self.right_content.extend( + [ + urwid.AttrMap(aligned_text("System Statistics"), "header"), + aligned_text(""), + aligned_text(f"Total CPU usage: {report.cpu_usage:.1f} %"), + aligned_text(f"Total system load: {report.load:.2f}"), + ] + ) + + if report.cores_info: + avg_temp = sum(core.temperature for core in report.cores_info) / len( + report.cores_info + ) + self.right_content.append( + aligned_text(f"Average temp. of all cores: {avg_temp:.2f} °C") + ) + + if report.avg_load: + load_status = "Load optimal" if report.load < 1.0 else "Load high" + self.right_content.append( + aligned_text( + f"{load_status} (load average: {report.avg_load[0]:.2f}, {report.avg_load[1]:.2f}, {report.avg_load[2]:.2f})" + ) + ) + + if report.cores_info: + usage_status = "Optimal" if report.cpu_usage < 70 else "High" + temp_status = "high" if avg_temp > 75 else "normal" # type: ignore + self.right_content.append( + aligned_text( + f"{usage_status} total CPU usage: {report.cpu_usage:.1f}%, {temp_status} average core temp: {avg_temp:.1f}°C" # type: ignore + ) + ) + + turbo_status: str + if report.is_turbo_on[0] != None: + turbo_status = "On" if report.is_turbo_on[0] else "Off" + elif report.is_turbo_on[1] != None: + turbo_status = ( + f"Auto mode {'enabled' if report.is_turbo_on[1] else 'disabled'}" + ) + else: + turbo_status = "Unknown" + self.right_content.append(aligned_text(f"Setting turbo boost: {turbo_status}")) + if ( + self.suggestion + and report.is_turbo_on[0] != None + and system_info.turbo_on_suggestion() != report.is_turbo_on[0] + ): + self.right_content.append( + urwid.AttrMap( + aligned_text( + f'Suggesting to set turbo boost: {"on" if system_info.turbo_on_suggestion() else "off"}' + ), + "suggestion", + ) + ) + + def run(self, on_quit: Callable[[], None] | None = None): + try: + if on_quit: + self.on_quit = on_quit + self.loop.set_alarm_in(0, self.update) # type: ignore + self.loop.run() + except KeyboardInterrupt: + if on_quit: + on_quit() + sys.exit(0) diff --git a/pyproject.toml b/pyproject.toml index 6d0babf..a5e7c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ click = "^8.1.0" distro = "^1.8.0" requests = "^2.32.3" PyGObject = "^3.46.0" +urwid = "^2.6.16" pyinotify = {git = "https://github.com/shadeyg56/pyinotify-3.12"} [tool.poetry.group.dev.dependencies]