gardesk/gartop / e0f19d1

Browse files

Add tree view rendering with parent-child hierarchy

Authored by espadonne
SHA
e0f19d184d18152fe21b812db9c5e30837102f0f
Parents
c0f88e8
Tree
81310a2

1 changed file

StatusFile+-
M gartop/src/gui/process_list.rs 107 14
gartop/src/gui/process_list.rsmodified
@@ -3,6 +3,7 @@
33
 use gartk_core::{Point, Rect};
44
 use gartk_render::{Renderer, TextStyle};
55
 use gartop_ipc::{ProcessInfo, SortField};
6
+use std::collections::HashMap;
67
 use std::time::Instant;
78
 use super::theme::Theme;
89
 
@@ -25,6 +26,70 @@ fn format_rate(bytes_per_sec: f64) -> String {
2526
     }
2627
 }
2728
 
29
+/// Build tree-ordered list of processes with indent levels.
30
+/// Returns Vec of (process_index_in_original, indent_level, is_last_sibling).
31
+fn build_tree_order(processes: &[ProcessInfo]) -> Vec<(usize, usize, bool)> {
32
+    // Build parent -> children map
33
+    let mut children: HashMap<i32, Vec<usize>> = HashMap::new();
34
+    let pid_to_idx: HashMap<i32, usize> = processes.iter()
35
+        .enumerate()
36
+        .map(|(i, p)| (p.pid, i))
37
+        .collect();
38
+
39
+    // Group processes by parent
40
+    for (idx, proc) in processes.iter().enumerate() {
41
+        children.entry(proc.ppid).or_default().push(idx);
42
+    }
43
+
44
+    // Find root processes (parent not in our list, or ppid=0/1)
45
+    let mut roots: Vec<usize> = Vec::new();
46
+    for (idx, proc) in processes.iter().enumerate() {
47
+        if proc.ppid == 0 || proc.ppid == 1 || !pid_to_idx.contains_key(&proc.ppid) {
48
+            roots.push(idx);
49
+        }
50
+    }
51
+
52
+    // Sort roots by CPU usage (descending)
53
+    roots.sort_by(|&a, &b| {
54
+        processes[b].cpu_percent
55
+            .partial_cmp(&processes[a].cpu_percent)
56
+            .unwrap_or(std::cmp::Ordering::Equal)
57
+    });
58
+
59
+    // DFS to build ordered list
60
+    let mut result = Vec::new();
61
+    fn visit(
62
+        idx: usize,
63
+        depth: usize,
64
+        is_last: bool,
65
+        children: &HashMap<i32, Vec<usize>>,
66
+        processes: &[ProcessInfo],
67
+        result: &mut Vec<(usize, usize, bool)>,
68
+    ) {
69
+        result.push((idx, depth, is_last));
70
+        if let Some(child_indices) = children.get(&processes[idx].pid) {
71
+            let mut sorted_children = child_indices.clone();
72
+            // Sort children by CPU
73
+            sorted_children.sort_by(|&a, &b| {
74
+                processes[b].cpu_percent
75
+                    .partial_cmp(&processes[a].cpu_percent)
76
+                    .unwrap_or(std::cmp::Ordering::Equal)
77
+            });
78
+            let len = sorted_children.len();
79
+            for (i, &child_idx) in sorted_children.iter().enumerate() {
80
+                visit(child_idx, depth + 1, i == len - 1, children, processes, result);
81
+            }
82
+        }
83
+    }
84
+
85
+    let root_count = roots.len();
86
+    for (i, root_idx) in roots.into_iter().enumerate() {
87
+        visit(root_idx, 0, i == root_count - 1, &children, processes, &mut result);
88
+    }
89
+
90
+    result
91
+}
92
+
2893
 /// Row height for process list.
2994
 const ROW_HEIGHT: u32 = 22;
3095
 
@@ -306,7 +371,7 @@ impl ProcessList {
306371
     }
307372
 
308373
     /// Render the process list.
309
-    pub fn render(&self, renderer: &Renderer, theme: &Theme, processes: &[ProcessInfo]) -> anyhow::Result<()> {
374
+    pub fn render(&self, renderer: &Renderer, theme: &Theme, processes: &[ProcessInfo], tree_view: bool) -> anyhow::Result<()> {
310375
         // Background
311376
         renderer.fill_rect(self.bounds, theme.panel_bg)?;
312377
 
@@ -389,28 +454,48 @@ impl ProcessList {
389454
             1.0,
390455
         )?;
391456
 
457
+        // Build tree order if needed
458
+        let tree_order = if tree_view {
459
+            Some(build_tree_order(processes))
460
+        } else {
461
+            None
462
+        };
463
+
392464
         // Process rows
393465
         let start_y = self.bounds.y + HEADER_HEIGHT as i32;
394
-        for (i, process) in processes.iter()
395
-            .skip(self.scroll_offset)
396
-            .take(self.visible_rows)
397
-            .enumerate()
398
-        {
466
+        let row_count = if tree_view {
467
+            tree_order.as_ref().map(|t| t.len()).unwrap_or(0)
468
+        } else {
469
+            processes.len()
470
+        };
471
+
472
+        for i in 0..self.visible_rows {
473
+            let display_idx = self.scroll_offset + i;
474
+            if display_idx >= row_count {
475
+                break;
476
+            }
477
+
478
+            // Get process and tree info
479
+            let (process, indent, _is_last) = if let Some(ref tree) = tree_order {
480
+                let (orig_idx, indent, is_last) = tree[display_idx];
481
+                (&processes[orig_idx], indent, is_last)
482
+            } else {
483
+                (&processes[display_idx], 0, false)
484
+            };
485
+
399486
             let row_y = start_y + (i as i32 * ROW_HEIGHT as i32);
400
-            let text_y = row_y as f64 + 4.0; // Pango uses top-left positioning
401
-            let process_idx = self.scroll_offset + i;
487
+            let text_y = row_y as f64 + 4.0;
402488
 
403489
             // Selection highlight
404
-            if self.selected_index == Some(process_idx) {
490
+            if self.selected_index == Some(display_idx) {
405491
                 let row_rect = Rect::new(
406492
                     self.bounds.x + 2,
407493
                     row_y + 2,
408494
                     self.bounds.width - 4,
409495
                     ROW_HEIGHT - 4,
410496
                 );
411
-                // Use magenta highlight when cursor lost its target
412497
                 let highlight_color = if self.cursor_lost {
413
-                    gartk_core::Color::new(0.6, 0.2, 0.6, 1.0) // Magenta
498
+                    gartk_core::Color::new(0.6, 0.2, 0.6, 1.0)
414499
                 } else {
415500
                     theme.header_bg
416501
                 };
@@ -420,13 +505,21 @@ impl ProcessList {
420505
             // PID
421506
             renderer.text(&process.pid.to_string(), col_pid, text_y, &dim_style)?;
422507
 
423
-            // Name (truncate if too long)
424
-            let name = if process.name.len() > 18 {
508
+            // Name with tree prefix
509
+            let name_with_prefix = if tree_view && indent > 0 {
510
+                let prefix = "  ".repeat(indent.saturating_sub(1)) + "├─";
511
+                let max_name_len = 18usize.saturating_sub(prefix.len());
512
+                if process.name.len() > max_name_len {
513
+                    format!("{}{:.width$}..", prefix, process.name, width = max_name_len.saturating_sub(2))
514
+                } else {
515
+                    format!("{}{}", prefix, process.name)
516
+                }
517
+            } else if process.name.len() > 18 {
425518
                 format!("{}...", &process.name[..15])
426519
             } else {
427520
                 process.name.clone()
428521
             };
429
-            renderer.text(&name, col_name, text_y, &text_style)?;
522
+            renderer.text(&name_with_prefix, col_name, text_y, &text_style)?;
430523
 
431524
             // Show CPU/Memory or I/O or Network depending on sort field
432525
             if is_disk_sort {