Fix display error

This commit is contained in:
Exil Productions
2025-12-04 15:59:47 +01:00
parent b583417640
commit 2820893b4f
5 changed files with 116 additions and 108 deletions

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "statsman" name = "statsman"
version = "0.1.2" version = "0.1.3"
description = "A real-time terminal-based system monitoring tool with ASCII visualizations" description = "A real-time terminal-based system monitoring tool with ASCII visualizations"
authors = [{name = "Exil Productions", email = "exil.productions.business@gmail.com"}] authors = [{name = "Exil Productions", email = "exil.productions.business@gmail.com"}]
license = {text = "MIT"} license = {text = "MIT"}

View File

@@ -1,3 +1,3 @@
__version__ = "0.1.2" __version__ = "0.1.3"
__author__ = "ExilProductions" __author__ = "ExilProductions"
__email__ = "exil.productions.business@gmail.com" __email__ = "exil.productions.business@gmail.com"

View File

@@ -31,6 +31,8 @@ class StatsManApp:
self.live.stop() self.live.stop()
def _handle_keyboard_input(self): def _handle_keyboard_input(self):
old_settings = None
termios = None
try: try:
import select import select
import termios import termios
@@ -62,7 +64,7 @@ class StatsManApp:
finally: finally:
try: try:
if 'old_settings' in locals(): if old_settings is not None and termios is not None:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
except: except:
pass pass
@@ -70,6 +72,13 @@ class StatsManApp:
def run(self): def run(self):
self.running = True self.running = True
def on_resize(sig, frame):
self.console.clear()
if self.live:
self.live.update(self.dashboard.render(), refresh=True)
signal.signal(signal.SIGWINCH, on_resize)
keyboard_thread = threading.Thread(target=self._handle_keyboard_input, daemon=True) keyboard_thread = threading.Thread(target=self._handle_keyboard_input, daemon=True)
keyboard_thread.start() keyboard_thread.start()

View File

@@ -75,19 +75,24 @@ class ChartRenderer:
return Panel("\n".join(lines), border_style="green") return Panel("\n".join(lines), border_style="green")
def create_mini_process_table(self, processes: List[Any], limit: int = 12) -> Panel: def create_mini_process_table(self, processes: List[Any], limit: int = 12, console_width: int = 80) -> Panel:
if not processes: if not processes:
return Panel("No processes", border_style="red") return Panel("No processes", border_style="red")
sorted_processes = sorted(processes, key=lambda p: p.cpu_percent, reverse=True) sorted_processes = sorted(processes, key=lambda p: p.cpu_percent, reverse=True)
lines = ["PID Name CPU MEM ", "=" * 55] max_name_width = max(10, min(20, (console_width - 35) // 2))
bar_width = max(5, min(10, (console_width - 25) // 4))
header = f"{'PID':<7} {'PROCESS':<20} {'CPU':<12} {'MEM':<12}"
separator = "=" * min(console_width - 4, 75)
lines = [header, separator]
for proc in sorted_processes[:limit]: for proc in sorted_processes[:limit]:
cpu_bar = self._create_mini_bar(proc.cpu_percent, 10) cpu_bar = self._create_mini_bar(proc.cpu_percent, bar_width)
mem_bar = self._create_mini_bar(proc.memory_percent, 10) mem_bar = self._create_mini_bar(proc.memory_percent, bar_width)
name = proc.name[:14] + ".." if len(proc.name) > 16 else proc.name.ljust(16) name = (proc.name[:18] + "..") if len(proc.name) > 20 else proc.name.ljust(20)
lines.append(f"{proc.pid:<7} {name} {cpu_bar} {mem_bar}") lines.append(f"{proc.pid:<7} {name} {cpu_bar} {mem_bar}")
return Panel("\n".join(lines), title="Top Processes", border_style="magenta") return Panel("\n".join(lines), title="Top Processes", border_style="magenta")
@@ -97,16 +102,17 @@ class ChartRenderer:
bar = "" * filled + "" * (width - filled) bar = "" * filled + "" * (width - filled)
return bar return bar
def create_system_gauges(self, cpu_info: Any, memory_info: Any, disk_info: Any) -> Panel: def create_system_gauges(self, cpu_info: Any, memory_info: Any, disk_info: Any, console_width: int = 80) -> Panel:
gauges = [] gauges = []
gauge_width = max(15, min(30, console_width // 4))
cpu_gauge = self._create_gauge(cpu_info.percent, "CPU") cpu_gauge = self._create_gauge(cpu_info.percent, "CPU", gauge_width)
gauges.append(cpu_gauge) gauges.append(cpu_gauge)
mem_gauge = self._create_gauge(memory_info.percent, "MEM") mem_gauge = self._create_gauge(memory_info.percent, "MEM", gauge_width)
gauges.append(mem_gauge) gauges.append(mem_gauge)
disk_gauge = self._create_gauge(disk_info.percent, "DSK") disk_gauge = self._create_gauge(disk_info.percent, "DSK", gauge_width)
gauges.append(disk_gauge) gauges.append(disk_gauge)
return Panel(Group(*gauges), border_style="cyan") return Panel(Group(*gauges), border_style="cyan")
@@ -116,7 +122,7 @@ class ChartRenderer:
bar = "" * filled + "" * (width - filled) bar = "" * filled + "" * (width - filled)
return Text.from_markup(f"[bold]{label}:[/bold] {bar} {percentage:5.1f}%") return Text.from_markup(f"[bold]{label}:[/bold] {bar} {percentage:5.1f}%")
def create_network_visualization(self, network_info: Any) -> Panel: def create_network_visualization(self, network_info: Any, console_width: int = 80) -> Panel:
sent_mb = network_info.bytes_sent / (1024 * 1024) sent_mb = network_info.bytes_sent / (1024 * 1024)
recv_mb = network_info.bytes_recv / (1024 * 1024) recv_mb = network_info.bytes_recv / (1024 * 1024)
@@ -125,9 +131,10 @@ class ChartRenderer:
"DOWNLOAD": min(recv_mb * 10, 100), "DOWNLOAD": min(recv_mb * 10, 100),
} }
return self.create_horizontal_bars(network_data) bar_width = max(15, min(70, console_width // 2))
return self.create_horizontal_bars(network_data, max_width=bar_width)
def create_cpu_core_visualization(self, cpu_info: Any) -> Panel: def create_cpu_core_visualization(self, cpu_info: Any, console_width: int = 80) -> Panel:
if not cpu_info.percent_per_core: if not cpu_info.percent_per_core:
return Panel("No core data", border_style="red") return Panel("No core data", border_style="red")
@@ -135,9 +142,10 @@ class ChartRenderer:
for i, core_percent in enumerate(cpu_info.percent_per_core): for i, core_percent in enumerate(cpu_info.percent_per_core):
core_data[f"C{i:02d}"] = core_percent core_data[f"C{i:02d}"] = core_percent
return self.create_vertical_bars(core_data, height=8, width=40) bar_width = max(20, min(40, console_width // 8))
return self.create_vertical_bars(core_data, height=8, width=bar_width)
def create_memory_breakdown(self, memory_info: Any) -> Panel: def create_memory_breakdown(self, memory_info: Any, console_width: int = 80) -> Panel:
used_gb = memory_info.used / (1024**3) used_gb = memory_info.used / (1024**3)
total_gb = memory_info.total / (1024**3) total_gb = memory_info.total / (1024**3)
@@ -146,7 +154,8 @@ class ChartRenderer:
"FREE": ((total_gb - used_gb) / total_gb) * 100, "FREE": ((total_gb - used_gb) / total_gb) * 100,
} }
return self.create_horizontal_bars(memory_data) bar_width = max(15, min(70, console_width // 2))
return self.create_horizontal_bars(memory_data, max_width=bar_width)
def format_bytes(self, bytes_value: int) -> str: def format_bytes(self, bytes_value: int) -> str:
bytes_float = float(bytes_value) bytes_float = float(bytes_value)

View File

@@ -1,11 +1,9 @@
from rich.console import Console, Group from rich.console import Console, Group
from rich.layout import Layout from rich.layout import Layout
from rich.panel import Panel from rich.panel import Panel
from rich.live import Live
from rich.text import Text from rich.text import Text
from rich.align import Align from rich.align import Align
from typing import Optional from typing import Optional
import time
from ..system_monitor import SystemMonitor from ..system_monitor import SystemMonitor
from .charts import ChartRenderer from .charts import ChartRenderer
@@ -14,120 +12,112 @@ from .charts import ChartRenderer
class Dashboard: class Dashboard:
def __init__(self, console: Optional[Console] = None, no_color: bool = False): def __init__(self, console: Optional[Console] = None, no_color: bool = False):
self.console = console or Console(color_system=None if no_color else "auto") self.console = console or Console(color_system=None if no_color else "auto")
self.monitor = SystemMonitor() self.monitor = SystemMonitor(history_size=120)
self.charts = ChartRenderer(self.console) self.charts = ChartRenderer(self.console)
self.layout = Layout() self.layout = Layout()
self.sort_processes_by = "cpu" self.sort_processes_by = "cpu"
self._setup_visual_layout() def _make_layout(self) -> Layout:
"""Create responsive layout based on current terminal size"""
def _setup_visual_layout(self) -> None: layout = Layout()
self.layout.split( layout.split_column(
Layout(name="header", size=3), Layout(name="header", size=3),
Layout(name="main"), Layout(name="body", ratio=1),
Layout(name="footer", size=3), Layout(name="footer", size=3),
) )
self.layout["main"].split_column( layout["body"].split_column(
Layout(name="top", size=16), Layout(name="top", ratio=2),
Layout(name="middle", size=14), Layout(name="middle", ratio=1),
Layout(name="bottom", ratio=1), Layout(name="processes", ratio=2),
) )
self.layout["top"].split_row( layout["top"].split_row(
Layout(name="gauges", ratio=1), Layout(name="gauges", ratio=1),
Layout(name="cores", ratio=1), Layout(name="cores", ratio=1),
) )
self.layout["middle"].split_row( layout["middle"].split_row(
Layout(name="memory", ratio=1), Layout(name="memory", ratio=1),
Layout(name="network", ratio=1), Layout(name="network", ratio=1),
) )
self.layout["bottom"].split_row( return layout
Layout(name="processes"),
)
def _create_header(self) -> Panel: def _create_header(self) -> Panel:
header_text = Text.from_markup(
"[bold blue]StatsMan[/bold blue] - System Monitor",
justify="center"
)
return Panel( return Panel(
Align.center(header_text), Align.center(Text("StatsMan - System Monitor", style="bold blue")),
border_style="blue" border_style="bright_blue",
height=3
) )
def _create_footer(self) -> Panel: def _create_footer(self) -> Panel:
controls = Text.from_markup( footer_text = (
"[cyan]q:quit p:pause c:cpu m:mem r:reset[/cyan]", "[bold cyan]q[/] quit "
justify="center" "[bold cyan]p[/] pause │ "
"[bold cyan]c[/] sort CPU │ "
"[bold cyan]m[/] sort MEM"
) )
return Panel( return Panel(
Align.center(controls), Align.center(Text.from_markup(footer_text)),
border_style="cyan" border_style="bright_black",
height=3
) )
def _create_system_gauges(self) -> Panel: def render(self) -> Layout:
cpu_info = self.monitor.get_cpu_info() self.layout = self._make_layout()
memory_info = self.monitor.get_memory_info()
disk_info = self.monitor.get_disk_info()
return self.charts.create_system_gauges(cpu_info, memory_info, disk_info) self.monitor.update_history()
def _create_cpu_cores(self) -> Panel:
cpu_info = self.monitor.get_cpu_info()
cpu_history = self.monitor.get_cpu_history()
sparkline = self.charts.create_sparkline(cpu_history, width=60, height=8)
sparkline_text = Text.from_markup(f"[cyan]CPU History:[/cyan] {sparkline}")
cores_panel = self.charts.create_cpu_core_visualization(cpu_info)
return Panel(
Group(sparkline_text, cores_panel),
title=f"CPU: {cpu_info.percent:.1f}%",
border_style="red"
)
def _create_memory_visual(self) -> Panel:
memory_info = self.monitor.get_memory_info()
memory_history = self.monitor.get_memory_history()
sparkline = self.charts.create_sparkline(memory_history, width=50, height=6)
sparkline_text = Text.from_markup(f"[green]Memory History:[/green] {sparkline}")
breakdown_panel = self.charts.create_memory_breakdown(memory_info)
return Panel(
Group(sparkline_text, breakdown_panel),
title=f"Memory: {memory_info.percent:.1f}%",
border_style="green"
)
def _create_network_visual(self) -> Panel:
network_info = self.monitor.get_network_info()
return self.charts.create_network_visualization(network_info)
def _create_processes_visual(self) -> Panel:
processes = self.monitor.get_process_info(limit=20)
return self.charts.create_mini_process_table(processes, limit=16)
def update_layout(self) -> None:
self.layout["header"].update(self._create_header()) self.layout["header"].update(self._create_header())
self.layout["footer"].update(self._create_footer()) self.layout["footer"].update(self._create_footer())
self.layout["top"]["gauges"].update(self._create_system_gauges()) self.layout["top"]["gauges"].update(self._create_system_gauges())
self.layout["top"]["cores"].update(self._create_cpu_cores()) self.layout["top"]["cores"].update(self._create_cpu_cores())
self.layout["middle"]["memory"].update(self._create_memory_visual()) self.layout["middle"]["memory"].update(self._create_memory_visual())
self.layout["middle"]["network"].update(self._create_network_visual()) self.layout["middle"]["network"].update(self._create_network_visual())
self.layout["bottom"]["processes"].update(self._create_processes_visual())
def render(self) -> Layout: self.layout["processes"].update(self._create_processes_visual())
self.monitor.update_history()
self.update_layout()
return self.layout return self.layout
def _create_system_gauges(self) -> Panel:
cpu = self.monitor.get_cpu_info()
mem = self.monitor.get_memory_info()
disk = self.monitor.get_disk_info()
return self.charts.create_system_gauges(cpu, mem, disk)
def _create_cpu_cores(self) -> Panel:
cpu = self.monitor.get_cpu_info()
history = self.monitor.get_cpu_history()
spark = self.charts.create_sparkline(history, height=6)
cores = self.charts.create_cpu_core_visualization(cpu)
return Panel(Group(Text(f"CPU Usage: {cpu.percent:.1f}%"), spark, cores),
title="CPU Cores", border_style="red")
def _create_memory_visual(self) -> Panel:
mem = self.monitor.get_memory_info()
history = self.monitor.get_memory_history()
spark = self.charts.create_sparkline(history, height=5)
breakdown = self.charts.create_memory_breakdown(mem)
return Panel(Group(Text(f"Memory: {mem.percent:.1f}%"), spark, breakdown),
title="Memory & Swap", border_style="green")
def _create_network_visual(self) -> Panel:
net = self.monitor.get_network_info()
return self.charts.create_network_visualization(net)
def _create_processes_visual(self) -> Panel:
height = self.console.size.height
limit = max(8, height // 3, 20)
procs = self.monitor.get_process_info(limit=limit + 5)
if self.sort_processes_by == "memory":
procs.sort(key=lambda p: p.memory_percent, reverse=True)
return self.charts.create_mini_process_table(procs[:limit])
def set_process_sort(self, sort_by: str) -> None: def set_process_sort(self, sort_by: str) -> None:
if sort_by in ['cpu', 'memory']: if sort_by in ("cpu", "memory"):
self.sort_processes_by = sort_by self.sort_processes_by = sort_by