diff --git a/hanatui/publish/hanatui b/hanatui/publish/hanatui index c406661..d02e393 100755 Binary files a/hanatui/publish/hanatui and b/hanatui/publish/hanatui differ diff --git a/hanatui/src/Tui/Components/StatsPanel.cs b/hanatui/src/Tui/Components/StatsPanel.cs index 335f695..3c2a94b 100644 --- a/hanatui/src/Tui/Components/StatsPanel.cs +++ b/hanatui/src/Tui/Components/StatsPanel.cs @@ -10,133 +10,138 @@ namespace HanaTui.Tui.Components; /// public static class StatsPanel { - public static IRenderable Build(SystemSnapshot snap, TimeSpan elapsed, int panelWidth) + public static IRenderable Build(SystemSnapshot snap, TimeSpan elapsed, int panelWidth) + { + var barWidth = Math.Max(10, panelWidth - 16); + + var grid = new Grid(); + grid.AddColumn(new GridColumn().NoWrap()); + + // --- CPU --- + grid.AddRow(new Markup("[bold yellow]CPU[/]")); + + if (snap.CpuPercents.Length == 0) { - var barWidth = Math.Max(10, panelWidth - 16); - - var grid = new Grid(); - grid.AddColumn(new GridColumn().NoWrap()); - - // --- CPU --- - grid.AddRow(new Markup("[bold yellow]CPU[/]")); - - if (snap.CpuPercents.Length == 0) - { - grid.AddRow(new Markup("[grey]No data[/]")); - } - else - { - for (int i = 0; i < snap.CpuPercents.Length; i++) - { - var label = snap.CpuLabels.Length > i ? snap.CpuLabels[i] : $"cpu{i}"; - var displayLabel = label == "cpu" ? "Total" : label.Replace("cpu", "Core"); - var pct = snap.CpuPercents[i]; - // Build as Columns: label | bar | pct — no interpolation of dynamic values into markup - grid.AddRow(BuildBarRow(displayLabel, pct, barWidth)); - } - } - - grid.AddRow(new Text("")); - - // --- Memory --- - grid.AddRow(new Markup("[bold cyan]MEMORY[/]")); - if (snap.MemTotalKb > 0) - { - var memPct = snap.MemUsedPercent; - grid.AddRow(BuildBarRow("Used", memPct, barWidth)); - grid.AddRow(new Text($" {SystemSnapshot.FormatGb(snap.MemUsedKb)} / {SystemSnapshot.FormatGb(snap.MemTotalKb)}", - new Style(Color.Grey))); - } - else - { - grid.AddRow(new Markup("[grey]No data[/]")); - } - - grid.AddRow(new Text("")); - - // --- Swap --- - grid.AddRow(new Markup("[bold cyan]SWAP[/]")); - if (snap.SwapTotalKb > 0) - { - var swapPct = snap.SwapUsedPercent; - grid.AddRow(BuildBarRow("Used", swapPct, barWidth)); - grid.AddRow(new Text($" {SystemSnapshot.FormatGb(snap.SwapUsedKb)} / {SystemSnapshot.FormatGb(snap.SwapTotalKb)}", - new Style(Color.Grey))); - } - else - { - grid.AddRow(new Markup("[grey]No swap[/]")); - } - - grid.AddRow(new Text("")); - - // --- Elapsed --- - // Two separate renderables composed — no interpolation of elapsed into markup - var elapsedGrid = new Grid(); - elapsedGrid.AddColumn(new GridColumn().NoWrap()); - elapsedGrid.AddColumn(new GridColumn().NoWrap()); - elapsedGrid.AddRow( - new Markup("[bold]Elapsed:[/]"), - new Text(" " + FormatElapsed(elapsed), new Style(Color.Yellow))); - grid.AddRow(elapsedGrid); - - return new Panel(grid) - { - Header = new PanelHeader("[bold] SYSTEM STATS [/]"), - Border = BoxBorder.Rounded, - Padding = new Padding(1, 0), - }; + grid.AddRow(new Markup("[grey]No data[/]")); + } + else + { + for (int i = 0; i < snap.CpuPercents.Length; i++) + { + var label = snap.CpuLabels.Length > i ? snap.CpuLabels[i] : $"cpu{i}"; + var displayLabel = label == "cpu" ? "Total" : label.Replace("cpu", "Core"); + var pct = snap.CpuPercents[i]; + // Build as Columns: label | bar | pct — no interpolation of dynamic values into markup + grid.AddRow(BuildBarRow(displayLabel, pct, barWidth)); + } } - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- + grid.AddRow(new Text("")); - /// - /// Builds a single bar row as a Grid with three columns: - /// label (dim) | bar (block chars, plain Text with color) | pct (bold) - /// Nothing here goes through markup parsing with dynamic content. - /// - private static IRenderable BuildBarRow(string label, double pct, int barWidth) + // --- Memory --- + grid.AddRow(new Markup("[bold cyan]MEMORY[/]")); + if (snap.MemTotalKb > 0) { - var filled = (int)Math.Round(pct / 100.0 * barWidth); - filled = Math.Clamp(filled, 0, barWidth); - var empty = barWidth - filled; - - var filledColor = CpuColor(pct); - var filledStr = new string('\u2588', filled); - var emptyStr = new string('\u2591', empty); - - var row = new Grid(); - row.AddColumn(new GridColumn().NoWrap()); - var colorMarkup = filledColor.ToMarkup(); - - // Construct a seamless markup string - var barMarkup = $"[grey][[[/][{colorMarkup}]{filledStr}[/][grey dim]{emptyStr}[/][grey]]][/]"; - - row.AddRow( - new Text($" {label,-5}", new Style(Color.Grey, decoration: Decoration.Dim)), - new Markup(barMarkup), // Replace Columns with a single Markup object - new Text($" {pct,5:F1}%", new Style(Color.White, decoration: Decoration.Bold)) - ); - - return row; + var memPct = snap.MemUsedPercent; + grid.AddRow(BuildBarRow("Used", memPct, barWidth)); + grid.AddRow(new Text($" {SystemSnapshot.FormatGb(snap.MemUsedKb)} / {SystemSnapshot.FormatGb(snap.MemTotalKb)}", + new Style(Color.Grey))); + } + else + { + grid.AddRow(new Markup("[grey]No data[/]")); } - private static Color CpuColor(double pct) => pct switch + grid.AddRow(new Text("")); + + // --- Swap --- + grid.AddRow(new Markup("[bold cyan]SWAP[/]")); + if (snap.SwapTotalKb > 0) { - > 90 => Color.Red, - > 70 => Color.Yellow, - > 40 => Color.Green, - _ => Color.Blue, + var swapPct = snap.SwapUsedPercent; + grid.AddRow(BuildBarRow("Used", swapPct, barWidth)); + grid.AddRow(new Text($" {SystemSnapshot.FormatGb(snap.SwapUsedKb)} / {SystemSnapshot.FormatGb(snap.SwapTotalKb)}", + new Style(Color.Grey))); + } + else + { + grid.AddRow(new Markup("[grey]No swap[/]")); + } + + grid.AddRow(new Text("")); + + // --- Elapsed --- + // Two separate renderables composed — no interpolation of elapsed into markup + var elapsedGrid = new Grid(); + elapsedGrid.AddColumn(new GridColumn().NoWrap()); + elapsedGrid.AddColumn(new GridColumn().NoWrap()); + elapsedGrid.AddRow( + new Markup("[bold]Elapsed:[/]"), + new Text(" " + FormatElapsed(elapsed), new Style(Color.Yellow))); + grid.AddRow(elapsedGrid); + + return new Panel(grid) + { + Header = new PanelHeader("[bold] SYSTEM STATS [/]"), + Border = BoxBorder.Rounded, + Padding = new Padding(1, 0), }; + } - private static string FormatElapsed(TimeSpan t) - { - if (t.TotalHours >= 1) - return $"{(int)t.TotalHours}h {t.Minutes:D2}m {t.Seconds:D2}s"; - if (t.TotalMinutes >= 1) - return $"{t.Minutes}m {t.Seconds:D2}s"; - return $"{t.Seconds}s"; - } + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /// + /// Builds a single bar row as a Grid with three columns: + /// label (dim) | bar (block chars, plain Text with color) | pct (bold) + /// Nothing here goes through markup parsing with dynamic content. + /// + private static IRenderable BuildBarRow(string label, double pct, int barWidth) + { + var filled = (int)Math.Round(pct / 100.0 * barWidth); + filled = Math.Clamp(filled, 0, barWidth); + var empty = barWidth - filled; + + var filledColor = CpuColor(pct); + var filledStr = new string('\u2588', filled); + var emptyStr = new string('\u2591', empty); + + var row = new Grid(); + + // FIX: Add all three columns back! + row.AddColumn(new GridColumn().NoWrap().Width(9)); // Column 1: Label + row.AddColumn(new GridColumn().NoWrap()); // Column 2: Bar + row.AddColumn(new GridColumn().NoWrap().Width(7)); // Column 3: Pct + + var colorMarkup = filledColor.ToMarkup(); + + // Construct a seamless markup string + var barMarkup = $"[grey][[[/][{colorMarkup}]{filledStr}[/][grey dim]{emptyStr}[/][grey]]][/]"; + + row.AddRow( + new Text($" {label,-5}", new Style(Color.Grey, decoration: Decoration.Dim)), + new Markup(barMarkup), + new Text($" {pct,5:F1}%", new Style(Color.White, decoration: Decoration.Bold)) + ); + + return row; + } + + private static Color CpuColor(double pct) => pct switch + { + > 90 => Color.Red, + > 70 => Color.Yellow, + > 40 => Color.Green, + _ => Color.Blue, + }; + + private static string FormatElapsed(TimeSpan t) + { + if (t.TotalHours >= 1) + return $"{(int)t.TotalHours}h {t.Minutes:D2}m {t.Seconds:D2}s"; + if (t.TotalMinutes >= 1) + return $"{t.Minutes}m {t.Seconds:D2}s"; + return $"{t.Seconds}s"; + } }