auto-cpufreq/auto_cpufreq/modules/system_monitor.py
LAITH343 081dbda79c
Enhance CLI System Monitoring with a TUI (#810)
* create a cli for better visual monitor, live and stats

- created a system info module
- created system monitor module
- fixed avg_all_core_temp not defined

* fixed snap package error and added missing implementations
2025-03-01 06:43:55 +01:00

302 lines
11 KiB
Python

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)