From 2820893b4faa26feb63314fd74c6423cf65418eb Mon Sep 17 00:00:00 2001 From: Exil Productions Date: Thu, 4 Dec 2025 15:59:47 +0100 Subject: [PATCH] Fix display error --- pyproject.toml | 2 +- src/statsman/__init__.py | 2 +- src/statsman/app.py | 11 ++- src/statsman/ui/charts.py | 39 ++++---- src/statsman/ui/dashboard.py | 170 +++++++++++++++++------------------ 5 files changed, 116 insertions(+), 108 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4bb9547..13138b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "statsman" -version = "0.1.2" +version = "0.1.3" description = "A real-time terminal-based system monitoring tool with ASCII visualizations" authors = [{name = "Exil Productions", email = "exil.productions.business@gmail.com"}] license = {text = "MIT"} diff --git a/src/statsman/__init__.py b/src/statsman/__init__.py index 0734435..882cf8f 100644 --- a/src/statsman/__init__.py +++ b/src/statsman/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" __author__ = "ExilProductions" __email__ = "exil.productions.business@gmail.com" \ No newline at end of file diff --git a/src/statsman/app.py b/src/statsman/app.py index bff69ff..8445183 100644 --- a/src/statsman/app.py +++ b/src/statsman/app.py @@ -31,6 +31,8 @@ class StatsManApp: self.live.stop() def _handle_keyboard_input(self): + old_settings = None + termios = None try: import select import termios @@ -62,13 +64,20 @@ class StatsManApp: finally: 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) except: pass def run(self): 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.start() diff --git a/src/statsman/ui/charts.py b/src/statsman/ui/charts.py index ab42475..5e2c5d3 100644 --- a/src/statsman/ui/charts.py +++ b/src/statsman/ui/charts.py @@ -75,19 +75,24 @@ class ChartRenderer: 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: return Panel("No processes", border_style="red") 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]: - cpu_bar = self._create_mini_bar(proc.cpu_percent, 10) - mem_bar = self._create_mini_bar(proc.memory_percent, 10) + cpu_bar = self._create_mini_bar(proc.cpu_percent, bar_width) + 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}") return Panel("\n".join(lines), title="Top Processes", border_style="magenta") @@ -97,16 +102,17 @@ class ChartRenderer: bar = "█" * filled + "░" * (width - filled) 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 = [] + 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) - mem_gauge = self._create_gauge(memory_info.percent, "MEM") + mem_gauge = self._create_gauge(memory_info.percent, "MEM", gauge_width) 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) return Panel(Group(*gauges), border_style="cyan") @@ -116,7 +122,7 @@ class ChartRenderer: bar = "█" * filled + "░" * (width - filled) 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) recv_mb = network_info.bytes_recv / (1024 * 1024) @@ -125,9 +131,10 @@ class ChartRenderer: "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: 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): 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) total_gb = memory_info.total / (1024**3) @@ -146,7 +154,8 @@ class ChartRenderer: "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: bytes_float = float(bytes_value) diff --git a/src/statsman/ui/dashboard.py b/src/statsman/ui/dashboard.py index dd828d9..1c721a8 100644 --- a/src/statsman/ui/dashboard.py +++ b/src/statsman/ui/dashboard.py @@ -1,11 +1,9 @@ from rich.console import Console, Group from rich.layout import Layout from rich.panel import Panel -from rich.live import Live from rich.text import Text from rich.align import Align from typing import Optional -import time from ..system_monitor import SystemMonitor from .charts import ChartRenderer @@ -14,120 +12,112 @@ from .charts import ChartRenderer class Dashboard: 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.monitor = SystemMonitor() + self.monitor = SystemMonitor(history_size=120) self.charts = ChartRenderer(self.console) self.layout = Layout() self.sort_processes_by = "cpu" - - self._setup_visual_layout() - - def _setup_visual_layout(self) -> None: - self.layout.split( + + def _make_layout(self) -> Layout: + """Create responsive layout based on current terminal size""" + layout = Layout() + layout.split_column( Layout(name="header", size=3), - Layout(name="main"), + Layout(name="body", ratio=1), Layout(name="footer", size=3), ) - - self.layout["main"].split_column( - Layout(name="top", size=16), - Layout(name="middle", size=14), - Layout(name="bottom", ratio=1), + + layout["body"].split_column( + Layout(name="top", ratio=2), + Layout(name="middle", ratio=1), + Layout(name="processes", ratio=2), ) - - self.layout["top"].split_row( + + layout["top"].split_row( Layout(name="gauges", ratio=1), Layout(name="cores", ratio=1), ) - - self.layout["middle"].split_row( + + layout["middle"].split_row( Layout(name="memory", ratio=1), Layout(name="network", ratio=1), ) - - self.layout["bottom"].split_row( - Layout(name="processes"), - ) - + + return layout + def _create_header(self) -> Panel: - header_text = Text.from_markup( - "[bold blue]StatsMan[/bold blue] - System Monitor", - justify="center" - ) return Panel( - Align.center(header_text), - border_style="blue" + Align.center(Text("StatsMan - System Monitor", style="bold blue")), + border_style="bright_blue", + height=3 ) - + def _create_footer(self) -> Panel: - controls = Text.from_markup( - "[cyan]q:quit p:pause c:cpu m:mem r:reset[/cyan]", - justify="center" + footer_text = ( + "[bold cyan]q[/] quit │ " + "[bold cyan]p[/] pause │ " + "[bold cyan]c[/] sort CPU │ " + "[bold cyan]m[/] sort MEM" ) return Panel( - Align.center(controls), - border_style="cyan" + Align.center(Text.from_markup(footer_text)), + border_style="bright_black", + height=3 ) - - def _create_system_gauges(self) -> Panel: - cpu_info = self.monitor.get_cpu_info() - 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) - - 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: + + def render(self) -> Layout: + self.layout = self._make_layout() + + self.monitor.update_history() + self.layout["header"].update(self._create_header()) self.layout["footer"].update(self._create_footer()) - + self.layout["top"]["gauges"].update(self._create_system_gauges()) self.layout["top"]["cores"].update(self._create_cpu_cores()) + self.layout["middle"]["memory"].update(self._create_memory_visual()) self.layout["middle"]["network"].update(self._create_network_visual()) - self.layout["bottom"]["processes"].update(self._create_processes_visual()) - - def render(self) -> Layout: - self.monitor.update_history() - self.update_layout() + + self.layout["processes"].update(self._create_processes_visual()) + 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: - if sort_by in ['cpu', 'memory']: + if sort_by in ("cpu", "memory"): self.sort_processes_by = sort_by \ No newline at end of file