Fix display error
This commit is contained in:
@@ -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"}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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,13 +64,20 @@ 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
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
self.monitor.update_history()
|
||||||
|
|
||||||
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:
|
|
||||||
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())
|
|
||||||
|
self.layout["processes"].update(self._create_processes_visual())
|
||||||
def render(self) -> Layout:
|
|
||||||
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
|
||||||
Reference in New Issue
Block a user