gardesk/gartop / ae028c0

Browse files

Add per-process CPU/memory history sparklines in detail view

Authored by espadonne
SHA
ae028c0e3e5383a84841b609500901708c31e89b
Parents
5c84a22
Tree
3a25ba6

1 changed file

StatusFile+-
M gartop/src/gui/app.rs 122 0
gartop/src/gui/app.rsmodified
@@ -13,6 +13,7 @@ use gartk_core::{InputEvent, Key, Point, Rect};
1313
 use gartk_render::{Renderer, TextStyle};
1414
 use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
1515
 use gartop_ipc::{Command, CpuStats, DiskStats, MemoryStats, NetworkStats, ProcessInfo, Response, SortField, StatusInfo, TempStats};
16
+use std::collections::HashMap;
1617
 use std::io::{BufRead, BufReader, Write};
1718
 use std::os::unix::net::UnixStream;
1819
 use std::time::Instant;
@@ -30,6 +31,33 @@ const CONTENT_PADDING: u32 = 16;
3031
 /// Vertical gap between sections.
3132
 const SECTION_GAP: u32 = 12;
3233
 
34
+/// Max history points per process
35
+const PROCESS_HISTORY_LEN: usize = 60;
36
+
37
+/// Per-process CPU/memory history for sparkline graphs.
38
+struct ProcessHistory {
39
+    cpu: Vec<f64>,
40
+    memory: Vec<f64>,
41
+}
42
+
43
+impl ProcessHistory {
44
+    fn new() -> Self {
45
+        Self {
46
+            cpu: Vec::with_capacity(PROCESS_HISTORY_LEN),
47
+            memory: Vec::with_capacity(PROCESS_HISTORY_LEN),
48
+        }
49
+    }
50
+
51
+    fn push(&mut self, cpu: f64, memory: f64) {
52
+        if self.cpu.len() >= PROCESS_HISTORY_LEN {
53
+            self.cpu.remove(0);
54
+            self.memory.remove(0);
55
+        }
56
+        self.cpu.push(cpu);
57
+        self.memory.push(memory);
58
+    }
59
+}
60
+
3361
 /// GUI application.
3462
 pub struct App {
3563
     window: Window,
@@ -75,6 +103,8 @@ pub struct App {
75103
     network_history: Vec<Vec<NetworkStats>>,
76104
     disk_history: Vec<Vec<DiskStats>>,
77105
     processes: Vec<ProcessInfo>,
106
+    /// Per-process CPU/memory history for sparkline graphs
107
+    process_history: HashMap<i32, ProcessHistory>,
78108
 }
79109
 
80110
 impl App {
@@ -170,6 +200,7 @@ impl App {
170200
             network_history: Vec::new(),
171201
             disk_history: Vec::new(),
172202
             processes: Vec::new(),
203
+            process_history: HashMap::new(),
173204
         })
174205
     }
175206
 
@@ -307,6 +338,18 @@ impl App {
307338
                     // Sync selection - tracks process by PID across list reorders
308339
                     self.process_list.sync_selection(&self.processes);
309340
                     self.process_list.set_sort(sort_field);
341
+
342
+                    // Record per-process history for sparklines
343
+                    for proc in &self.processes {
344
+                        self.process_history
345
+                            .entry(proc.pid)
346
+                            .or_insert_with(ProcessHistory::new)
347
+                            .push(proc.cpu_percent, proc.memory_percent);
348
+                    }
349
+                    // Clean up history for dead processes
350
+                    let active_pids: std::collections::HashSet<i32> =
351
+                        self.processes.iter().map(|p| p.pid).collect();
352
+                    self.process_history.retain(|pid, _| active_pids.contains(pid));
310353
                 }
311354
             }
312355
         }
@@ -737,6 +780,85 @@ impl App {
737780
         };
738781
         self.renderer.text("[K] Kill (SIGTERM)   [X] Kill (SIGKILL)", x, footer_y, &action_style)?;
739782
 
783
+        // History sparklines (if we have history for this process)
784
+        if let Some(history) = self.process_history.get(&process.pid) {
785
+            if history.cpu.len() >= 2 {
786
+                let spark_y = footer_y - 90.0;
787
+                let spark_height = 50.0;
788
+                let spark_width = ((self.width - margin * 2) as f64 - 40.0) / 2.0 - 10.0;
789
+
790
+                // CPU sparkline
791
+                self.renderer.text("CPU History:", x, spark_y - 14.0, &label_style)?;
792
+                self.render_sparkline(
793
+                    x, spark_y, spark_width, spark_height,
794
+                    &history.cpu, self.theme.cpu_color,
795
+                )?;
796
+
797
+                // Memory sparkline
798
+                let mem_x = x + spark_width + 20.0;
799
+                self.renderer.text("Memory History:", mem_x, spark_y - 14.0, &label_style)?;
800
+                self.render_sparkline(
801
+                    mem_x, spark_y, spark_width, spark_height,
802
+                    &history.memory, self.theme.memory_color,
803
+                )?;
804
+            }
805
+        }
806
+
807
+        Ok(())
808
+    }
809
+
810
+    /// Render a sparkline graph (small inline chart).
811
+    fn render_sparkline(&self, x: f64, y: f64, w: f64, h: f64, values: &[f64], color: gartk_core::Color) -> Result<()> {
812
+        if values.is_empty() || w <= 0.0 || h <= 0.0 {
813
+            return Ok(());
814
+        }
815
+
816
+        let ctx = self.renderer.context()?;
817
+
818
+        // Background
819
+        ctx.set_source_rgba(
820
+            self.theme.graph_bg.r,
821
+            self.theme.graph_bg.g,
822
+            self.theme.graph_bg.b,
823
+            self.theme.graph_bg.a,
824
+        );
825
+        ctx.rectangle(x, y, w, h);
826
+        let _ = ctx.fill();
827
+
828
+        // Find max for scaling (at least 1% to avoid division by zero)
829
+        let max_val = values.iter().cloned().fold(1.0_f64, f64::max);
830
+        let scale = h / max_val.max(1.0);
831
+
832
+        let step = w / (values.len().saturating_sub(1).max(1)) as f64;
833
+
834
+        // Draw filled area
835
+        ctx.set_source_rgba(color.r, color.g, color.b, 0.3);
836
+        ctx.move_to(x, y + h);
837
+        for (i, &val) in values.iter().enumerate() {
838
+            let px = x + i as f64 * step;
839
+            let py = y + h - (val * scale);
840
+            ctx.line_to(px, py);
841
+        }
842
+        ctx.line_to(x + (values.len() - 1) as f64 * step, y + h);
843
+        ctx.close_path();
844
+        let _ = ctx.fill();
845
+
846
+        // Draw line
847
+        ctx.set_source_rgba(color.r, color.g, color.b, 1.0);
848
+        ctx.set_line_width(1.5);
849
+        let mut first = true;
850
+        for (i, &val) in values.iter().enumerate() {
851
+            let px = x + i as f64 * step;
852
+            let py = y + h - (val * scale);
853
+            if first {
854
+                ctx.move_to(px, py);
855
+                first = false;
856
+            } else {
857
+                ctx.line_to(px, py);
858
+            }
859
+        }
860
+        let _ = ctx.stroke();
861
+
740862
         Ok(())
741863
     }
742864