gardesk/gartop / c5d25d6

Browse files

Add keyboard navigation, freeze mode, search filter, and fix legend

- Move graph legend to top-right inside graph area with semi-transparent bg
- Add keyboard navigation: j/k/arrows, Home/End, PageUp/PageDown
- Add kill process: K (SIGTERM), X (SIGKILL)
- Add freeze mode (f) to pause process list updates
- Add search filter (/) to filter by process name/cmdline
- Track selection by PID instead of index (cursor follows process)
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c5d25d61924736f31a63b7e9e524cbcd8d946851
Parents
6149ec6
Tree
157493c

3 changed files

StatusFile+-
M gartop/src/gui/app.rs 219 60
M gartop/src/gui/graph.rs 31 18
M gartop/src/gui/process_list.rs 136 9
gartop/src/gui/app.rsmodified
@@ -46,6 +46,12 @@ pub struct App {
4646
     last_refresh: Instant,
4747
     refresh_interval: f64,
4848
     show_legend: bool,
49
+    /// Freeze mode - pause process list updates for navigation
50
+    frozen: bool,
51
+    /// Search mode - filter processes by name
52
+    search_mode: bool,
53
+    /// Search query string
54
+    search_query: String,
4955
     status: Option<StatusInfo>,
5056
     cpu_stats: Option<CpuStats>,
5157
     memory_stats: Option<MemoryStats>,
@@ -118,6 +124,9 @@ impl App {
118124
             last_refresh: Instant::now() - std::time::Duration::from_secs(10),
119125
             refresh_interval,
120126
             show_legend,
127
+            frozen: false,
128
+            search_mode: false,
129
+            search_query: String::new(),
121130
             status: None,
122131
             cpu_stats: None,
123132
             memory_stats: None,
@@ -241,21 +250,24 @@ impl App {
241250
             }
242251
         }
243252
 
244
-        // Get processes sorted by current tab's resource
245
-        let sort_field = match self.tab_bar.active() {
246
-            Tab::Cpu => SortField::Cpu,
247
-            Tab::Memory => SortField::Memory,
248
-            Tab::Network => SortField::NetConnections,
249
-            Tab::Disk => SortField::DiskTotal,
250
-        };
251
-        if let Some(resp) = self.send_command(&Command::GetProcesses {
252
-            sort_by: Some(sort_field),
253
-            limit: Some(100),
254
-        }) {
255
-            if resp.success {
256
-                self.processes = resp.data.and_then(|d| serde_json::from_value(d).ok()).unwrap_or_default();
257
-                self.process_list.set_process_count(self.processes.len());
258
-                self.process_list.set_sort(sort_field);
253
+        // Get processes sorted by current tab's resource (skip when frozen)
254
+        if !self.frozen {
255
+            let sort_field = match self.tab_bar.active() {
256
+                Tab::Cpu => SortField::Cpu,
257
+                Tab::Memory => SortField::Memory,
258
+                Tab::Network => SortField::NetConnections,
259
+                Tab::Disk => SortField::DiskTotal,
260
+            };
261
+            if let Some(resp) = self.send_command(&Command::GetProcesses {
262
+                sort_by: Some(sort_field),
263
+                limit: Some(100),
264
+            }) {
265
+                if resp.success {
266
+                    self.processes = resp.data.and_then(|d| serde_json::from_value(d).ok()).unwrap_or_default();
267
+                    // Sync selection - tracks process by PID across list reorders
268
+                    self.process_list.sync_selection(&self.processes);
269
+                    self.process_list.set_sort(sort_field);
270
+                }
259271
             }
260272
         }
261273
 
@@ -293,6 +305,17 @@ impl App {
293305
         // Render header
294306
         self.header.render(&self.renderer, &self.theme)?;
295307
 
308
+        // Freeze indicator
309
+        if self.frozen {
310
+            let freeze_style = TextStyle {
311
+                font_family: "monospace".to_string(),
312
+                font_size: 10.0,
313
+                color: self.theme.swap_color,
314
+                ..Default::default()
315
+            };
316
+            self.renderer.text("PAUSED", 80.0, (HEADER_HEIGHT as f64 * 0.4) + 6.0, &freeze_style)?;
317
+        }
318
+
296319
         // Render tab bar
297320
         self.tab_bar.render(&self.renderer, &self.theme)?;
298321
 
@@ -583,8 +606,34 @@ impl App {
583606
             }
584607
         }
585608
 
586
-        // Render process list (pass reference, no clone)
587
-        self.process_list.render(&self.renderer, &self.theme, &self.processes)?;
609
+        // Search bar (above process list when active)
610
+        if self.search_mode || !self.search_query.is_empty() {
611
+            let search_y = (HEADER_HEIGHT + TAB_BAR_HEIGHT + SECTION_GAP + 20 + GRAPH_HEIGHT + SECTION_GAP) as i32 - 20;
612
+            let search_style = TextStyle {
613
+                font_family: "monospace".to_string(),
614
+                font_size: 11.0,
615
+                color: if self.search_mode { self.theme.text } else { self.theme.text_secondary },
616
+                ..Default::default()
617
+            };
618
+            let search_text = format!("/{}{}", self.search_query, if self.search_mode { "_" } else { "" });
619
+            self.renderer.text(&search_text, CONTENT_PADDING as f64, search_y as f64, &search_style)?;
620
+        }
621
+
622
+        // Filter processes if search query is active
623
+        let display_processes: Vec<ProcessInfo> = if self.search_query.is_empty() {
624
+            self.processes.clone()
625
+        } else {
626
+            let query = self.search_query.to_lowercase();
627
+            self.processes
628
+                .iter()
629
+                .filter(|p| p.name.to_lowercase().contains(&query) ||
630
+                           p.cmdline.to_lowercase().contains(&query))
631
+                .cloned()
632
+                .collect()
633
+        };
634
+
635
+        // Render process list with filtered processes
636
+        self.process_list.render(&self.renderer, &self.theme, &display_processes)?;
588637
 
589638
         Ok(())
590639
     }
@@ -705,6 +754,24 @@ impl App {
705754
         true
706755
     }
707756
 
757
+    /// Kill a process by PID.
758
+    fn kill_process(&mut self, pid: i32, signal: i32) {
759
+        tracing::info!("Sending signal {} to process {}", signal, pid);
760
+        if let Some(resp) = self.send_command(&Command::KillProcess {
761
+            pid,
762
+            signal: Some(signal),
763
+        }) {
764
+            if resp.success {
765
+                tracing::info!("Process {} killed", pid);
766
+                // Clear selection and trigger refresh
767
+                self.process_list.clear_selection();
768
+                self.last_refresh = Instant::now() - std::time::Duration::from_secs(10);
769
+            } else {
770
+                tracing::warn!("Failed to kill process {}: {:?}", pid, resp.error);
771
+            }
772
+        }
773
+    }
774
+
708775
     /// Run the GUI event loop.
709776
     pub fn run(mut self) -> Result<()> {
710777
         let config = EventLoopConfig {
@@ -748,51 +815,143 @@ impl App {
748815
                 }
749816
 
750817
                 InputEvent::Key(key_event) if key_event.pressed => {
751
-                    match key_event.key {
752
-                        Key::Escape | Key::Char('q') => {
753
-                            self.should_quit = true;
754
-                        }
755
-                        Key::Char('r') => {
756
-                            self.last_refresh = Instant::now() - std::time::Duration::from_secs(10);
757
-                        }
758
-                        Key::Char('1') => {
759
-                            self.tab_bar.set_active(Tab::Cpu);
760
-                            self.sort_processes(SortField::Cpu);
761
-                            ev_loop.request_redraw();
762
-                        }
763
-                        Key::Char('2') => {
764
-                            self.tab_bar.set_active(Tab::Memory);
765
-                            self.sort_processes(SortField::Memory);
766
-                            ev_loop.request_redraw();
767
-                        }
768
-                        Key::Char('3') => {
769
-                            self.tab_bar.set_active(Tab::Network);
770
-                            self.sort_processes(SortField::NetConnections);
771
-                            ev_loop.request_redraw();
772
-                        }
773
-                        Key::Char('4') => {
774
-                            self.tab_bar.set_active(Tab::Disk);
775
-                            self.sort_processes(SortField::DiskTotal);
776
-                            ev_loop.request_redraw();
818
+                    // Search mode input handling
819
+                    if self.search_mode {
820
+                        match key_event.key {
821
+                            Key::Escape => {
822
+                                if self.search_query.is_empty() {
823
+                                    self.search_mode = false;
824
+                                } else {
825
+                                    self.search_query.clear();
826
+                                }
827
+                                ev_loop.request_redraw();
828
+                            }
829
+                            Key::Return => {
830
+                                self.search_mode = false;
831
+                                ev_loop.request_redraw();
832
+                            }
833
+                            Key::Backspace => {
834
+                                self.search_query.pop();
835
+                                ev_loop.request_redraw();
836
+                            }
837
+                            Key::Char(c) => {
838
+                                if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
839
+                                    self.search_query.push(c);
840
+                                    ev_loop.request_redraw();
841
+                                }
842
+                            }
843
+                            _ => {}
777844
                         }
778
-                        Key::Tab => {
779
-                            let new_tab = match self.tab_bar.active() {
780
-                                Tab::Cpu => Tab::Memory,
781
-                                Tab::Memory => Tab::Network,
782
-                                Tab::Network => Tab::Disk,
783
-                                Tab::Disk => Tab::Cpu,
784
-                            };
785
-                            self.tab_bar.set_active(new_tab);
786
-                            let sort_field = match new_tab {
787
-                                Tab::Cpu => SortField::Cpu,
788
-                                Tab::Memory => SortField::Memory,
789
-                                Tab::Network => SortField::NetConnections,
790
-                                Tab::Disk => SortField::DiskTotal,
791
-                            };
792
-                            self.sort_processes(sort_field);
793
-                            ev_loop.request_redraw();
845
+                    } else {
846
+                        // Normal mode key handling
847
+                        match key_event.key {
848
+                            Key::Escape => {
849
+                                if !self.search_query.is_empty() {
850
+                                    // Clear search filter first
851
+                                    self.search_query.clear();
852
+                                    ev_loop.request_redraw();
853
+                                } else {
854
+                                    self.should_quit = true;
855
+                                }
856
+                            }
857
+                            Key::Char('q') => {
858
+                                self.should_quit = true;
859
+                            }
860
+                            Key::Char('/') => {
861
+                                self.search_mode = true;
862
+                                ev_loop.request_redraw();
863
+                            }
864
+                            Key::Char('r') => {
865
+                                self.last_refresh = Instant::now() - std::time::Duration::from_secs(10);
866
+                            }
867
+                            Key::Char('1') => {
868
+                                self.tab_bar.set_active(Tab::Cpu);
869
+                                self.sort_processes(SortField::Cpu);
870
+                                ev_loop.request_redraw();
871
+                            }
872
+                            Key::Char('2') => {
873
+                                self.tab_bar.set_active(Tab::Memory);
874
+                                self.sort_processes(SortField::Memory);
875
+                                ev_loop.request_redraw();
876
+                            }
877
+                            Key::Char('3') => {
878
+                                self.tab_bar.set_active(Tab::Network);
879
+                                self.sort_processes(SortField::NetConnections);
880
+                                ev_loop.request_redraw();
881
+                            }
882
+                            Key::Char('4') => {
883
+                                self.tab_bar.set_active(Tab::Disk);
884
+                                self.sort_processes(SortField::DiskTotal);
885
+                                ev_loop.request_redraw();
886
+                            }
887
+                            Key::Tab => {
888
+                                let new_tab = match self.tab_bar.active() {
889
+                                    Tab::Cpu => Tab::Memory,
890
+                                    Tab::Memory => Tab::Network,
891
+                                    Tab::Network => Tab::Disk,
892
+                                    Tab::Disk => Tab::Cpu,
893
+                                };
894
+                                self.tab_bar.set_active(new_tab);
895
+                                let sort_field = match new_tab {
896
+                                    Tab::Cpu => SortField::Cpu,
897
+                                    Tab::Memory => SortField::Memory,
898
+                                    Tab::Network => SortField::NetConnections,
899
+                                    Tab::Disk => SortField::DiskTotal,
900
+                                };
901
+                                self.sort_processes(sort_field);
902
+                                ev_loop.request_redraw();
903
+                            }
904
+                            // Freeze mode toggle (pause process list updates)
905
+                            Key::Char('f') => {
906
+                                self.frozen = !self.frozen;
907
+                                ev_loop.request_redraw();
908
+                            }
909
+                            // Process list navigation
910
+                            Key::Down | Key::Char('j') => {
911
+                                self.process_list.select_next(&self.processes);
912
+                                ev_loop.request_redraw();
913
+                            }
914
+                            Key::Up | Key::Char('k') => {
915
+                                self.process_list.select_prev(&self.processes);
916
+                                ev_loop.request_redraw();
917
+                            }
918
+                            Key::Home => {
919
+                                self.process_list.select_first(&self.processes);
920
+                                ev_loop.request_redraw();
921
+                            }
922
+                            Key::End => {
923
+                                self.process_list.select_last(&self.processes);
924
+                                ev_loop.request_redraw();
925
+                            }
926
+                            Key::PageDown => {
927
+                                // Move selection down by visible rows
928
+                                for _ in 0..10 {
929
+                                    self.process_list.select_next(&self.processes);
930
+                                }
931
+                                ev_loop.request_redraw();
932
+                            }
933
+                            Key::PageUp => {
934
+                                // Move selection up by visible rows
935
+                                for _ in 0..10 {
936
+                                    self.process_list.select_prev(&self.processes);
937
+                                }
938
+                                ev_loop.request_redraw();
939
+                            }
940
+                            // Kill selected process (K = SIGTERM, X = SIGKILL)
941
+                            Key::Char('K') => {
942
+                                if let Some(pid) = self.process_list.selected_pid() {
943
+                                    self.kill_process(pid, 15); // SIGTERM
944
+                                    ev_loop.request_redraw();
945
+                                }
946
+                            }
947
+                            Key::Char('X') => {
948
+                                if let Some(pid) = self.process_list.selected_pid() {
949
+                                    self.kill_process(pid, 9); // SIGKILL
950
+                                    ev_loop.request_redraw();
951
+                                }
952
+                            }
953
+                            _ => {}
794954
                         }
795
-                        _ => {}
796955
                     }
797956
                 }
798957
 
gartop/src/gui/graph.rsmodified
@@ -74,12 +74,8 @@ impl LineGraph {
7474
         // Background
7575
         fill_rounded_rect(ctx, rect, 4.0, theme.graph_bg);
7676
 
77
-        // Calculate graph area with margins
78
-        let (ml, mr, mt, mb) = if self.show_legend {
79
-            (36.0, 8.0, 8.0, 22.0)
80
-        } else {
81
-            (8.0, 8.0, 8.0, 8.0)
82
-        };
77
+        // Calculate graph area with margins (legend now inside graph, no extra bottom margin)
78
+        let (ml, mr, mt, mb) = (36.0, 8.0, 8.0, 8.0);
8379
 
8480
         let gx = x + ml;
8581
         let gy = y + mt;
@@ -100,9 +96,9 @@ impl LineGraph {
10096
             }
10197
         }
10298
 
103
-        // Draw legend at bottom
99
+        // Draw legend inside graph area (top-right corner)
104100
         if self.show_legend && !series.is_empty() {
105
-            self.draw_legend(ctx, series, x, y + h - 18.0, w, theme);
101
+            self.draw_legend(ctx, series, gx, gy, gw, theme);
106102
         }
107103
     }
108104
 
@@ -209,7 +205,7 @@ impl LineGraph {
209205
         let _ = ctx.stroke();
210206
     }
211207
 
212
-    fn draw_legend(&self, ctx: &Context, series: &[DataSeries], x: f64, y: f64, w: f64, theme: &Theme) {
208
+    fn draw_legend(&self, ctx: &Context, series: &[DataSeries], gx: f64, gy: f64, gw: f64, theme: &Theme) {
213209
         let text_renderer = TextRenderer::new();
214210
         let style = TextStyle {
215211
             font_family: "monospace".to_string(),
@@ -218,11 +214,33 @@ impl LineGraph {
218214
             ..Default::default()
219215
         };
220216
 
221
-        let mut lx = x + 38.0; // Start after Y-axis labels
217
+        // Calculate total legend width first (right-align)
218
+        let mut total_width = 0.0;
219
+        for data in series {
220
+            let size = text_renderer.measure(ctx, &data.label, &style);
221
+            total_width += 12.0 + size.width as f64 + 12.0; // box + gap + text + spacing
222
+        }
223
+        total_width -= 12.0; // Remove trailing spacing
224
+
225
+        // Position at top-right inside graph area
226
+        let legend_x = gx + gw - total_width - 8.0;
227
+        let legend_y = gy + 4.0;
222228
 
229
+        // Draw semi-transparent background for readability
230
+        ctx.rectangle(legend_x - 4.0, legend_y - 2.0, total_width + 8.0, 16.0);
231
+        ctx.set_source_rgba(
232
+            theme.graph_bg.r,
233
+            theme.graph_bg.g,
234
+            theme.graph_bg.b,
235
+            0.85,
236
+        );
237
+        let _ = ctx.fill();
238
+
239
+        // Draw legend entries
240
+        let mut lx = legend_x;
223241
         for data in series {
224242
             // Color box
225
-            ctx.rectangle(lx, y + 3.0, 8.0, 8.0);
243
+            ctx.rectangle(lx, legend_y + 3.0, 8.0, 8.0);
226244
             ctx.set_source_rgba(
227245
                 data.color.r,
228246
                 data.color.g,
@@ -234,14 +252,9 @@ impl LineGraph {
234252
             lx += 12.0;
235253
 
236254
             // Label
237
-            text_renderer.draw(ctx, &data.label, lx, y, &style);
255
+            text_renderer.draw(ctx, &data.label, lx, legend_y, &style);
238256
             let size = text_renderer.measure(ctx, &data.label, &style);
239
-            lx += size.width as f64 + 16.0;
240
-
241
-            // Stop if we run out of space
242
-            if lx > x + w - 40.0 {
243
-                break;
244
-            }
257
+            lx += size.width as f64 + 12.0;
245258
         }
246259
     }
247260
 }
gartop/src/gui/process_list.rsmodified
@@ -31,10 +31,14 @@ const ROW_HEIGHT: u32 = 22;
3131
 const HEADER_HEIGHT: u32 = 24;
3232
 
3333
 /// Process list component - renders process data without owning it.
34
+/// Tracks selection by PID so cursor follows the process when list reorders.
3435
 pub struct ProcessList {
3536
     bounds: Rect,
3637
     scroll_offset: usize,
37
-    selected: Option<usize>,
38
+    /// Selected process PID (tracks process across reorders)
39
+    selected_pid: Option<i32>,
40
+    /// Cached index of selected PID (updated on sync)
41
+    selected_index: Option<usize>,
3842
     sort_field: SortField,
3943
     visible_rows: usize,
4044
     process_count: usize,
@@ -47,7 +51,8 @@ impl ProcessList {
4751
         Self {
4852
             bounds,
4953
             scroll_offset: 0,
50
-            selected: None,
54
+            selected_pid: None,
55
+            selected_index: None,
5156
             sort_field: SortField::Cpu,
5257
             visible_rows,
5358
             process_count: 0,
@@ -60,7 +65,50 @@ impl ProcessList {
6065
         self.visible_rows = ((bounds.height.saturating_sub(HEADER_HEIGHT)) / ROW_HEIGHT) as usize;
6166
     }
6267
 
63
-    /// Update process count (for scroll calculations).
68
+    /// Sync selection with updated process list.
69
+    /// Call this after refreshing process data to update the cursor position.
70
+    /// Returns true if selection is still valid and visible.
71
+    pub fn sync_selection(&mut self, processes: &[ProcessInfo]) -> bool {
72
+        self.process_count = processes.len();
73
+
74
+        // Clamp scroll offset
75
+        if self.scroll_offset > self.max_scroll() {
76
+            self.scroll_offset = self.max_scroll();
77
+        }
78
+
79
+        // Find the selected PID in the new list
80
+        if let Some(pid) = self.selected_pid {
81
+            if let Some(idx) = processes.iter().position(|p| p.pid == pid) {
82
+                self.selected_index = Some(idx);
83
+
84
+                // If process moved out of visible area, scroll to keep it visible
85
+                // but only if it's still in the first "page" of results
86
+                if idx < self.visible_rows * 2 {
87
+                    // Process is near the top, keep tracking
88
+                    if idx < self.scroll_offset {
89
+                        self.scroll_offset = idx;
90
+                    } else if idx >= self.scroll_offset + self.visible_rows {
91
+                        self.scroll_offset = idx.saturating_sub(self.visible_rows - 1);
92
+                    }
93
+                    return true;
94
+                } else {
95
+                    // Process moved too far down, clear selection
96
+                    self.selected_pid = None;
97
+                    self.selected_index = None;
98
+                    return false;
99
+                }
100
+            } else {
101
+                // Process no longer exists, clear selection
102
+                self.selected_pid = None;
103
+                self.selected_index = None;
104
+                return false;
105
+            }
106
+        }
107
+
108
+        true
109
+    }
110
+
111
+    /// Update process count (for scroll calculations) - legacy method.
64112
     pub fn set_process_count(&mut self, count: usize) {
65113
         self.process_count = count;
66114
         // Clamp scroll offset
@@ -111,21 +159,100 @@ impl ProcessList {
111159
         let process_idx = self.scroll_offset + row;
112160
 
113161
         if process_idx < processes.len() {
114
-            self.selected = Some(process_idx);
115
-            return Some(processes[process_idx].pid);
162
+            let pid = processes[process_idx].pid;
163
+            self.selected_pid = Some(pid);
164
+            self.selected_index = Some(process_idx);
165
+            return Some(pid);
116166
         }
117167
 
118168
         None
119169
     }
120170
 
121171
     /// Get selected process PID.
122
-    pub fn selected_pid(&self, processes: &[ProcessInfo]) -> Option<i32> {
123
-        self.selected.and_then(|idx| processes.get(idx).map(|p| p.pid))
172
+    pub fn selected_pid(&self) -> Option<i32> {
173
+        self.selected_pid
124174
     }
125175
 
126176
     /// Clear selection.
127177
     pub fn clear_selection(&mut self) {
128
-        self.selected = None;
178
+        self.selected_pid = None;
179
+        self.selected_index = None;
180
+    }
181
+
182
+    /// Move selection down by one row.
183
+    pub fn select_next(&mut self, processes: &[ProcessInfo]) {
184
+        if processes.is_empty() {
185
+            return;
186
+        }
187
+        match self.selected_index {
188
+            None => {
189
+                // Select first visible item
190
+                let idx = self.scroll_offset.min(processes.len() - 1);
191
+                self.selected_index = Some(idx);
192
+                self.selected_pid = Some(processes[idx].pid);
193
+            }
194
+            Some(idx) => {
195
+                if idx + 1 < processes.len() {
196
+                    let new_idx = idx + 1;
197
+                    self.selected_index = Some(new_idx);
198
+                    self.selected_pid = Some(processes[new_idx].pid);
199
+                    // Auto-scroll if selection goes below visible area
200
+                    if new_idx >= self.scroll_offset + self.visible_rows {
201
+                        self.scroll_offset = (new_idx + 1).saturating_sub(self.visible_rows);
202
+                    }
203
+                }
204
+            }
205
+        }
206
+    }
207
+
208
+    /// Move selection up by one row.
209
+    pub fn select_prev(&mut self, processes: &[ProcessInfo]) {
210
+        if processes.is_empty() {
211
+            return;
212
+        }
213
+        match self.selected_index {
214
+            None => {
215
+                // Select first visible item
216
+                let idx = self.scroll_offset.min(processes.len() - 1);
217
+                self.selected_index = Some(idx);
218
+                self.selected_pid = Some(processes[idx].pid);
219
+            }
220
+            Some(idx) => {
221
+                if idx > 0 {
222
+                    let new_idx = idx - 1;
223
+                    self.selected_index = Some(new_idx);
224
+                    self.selected_pid = Some(processes[new_idx].pid);
225
+                    // Auto-scroll if selection goes above visible area
226
+                    if new_idx < self.scroll_offset {
227
+                        self.scroll_offset = new_idx;
228
+                    }
229
+                }
230
+            }
231
+        }
232
+    }
233
+
234
+    /// Select first item (Home key).
235
+    pub fn select_first(&mut self, processes: &[ProcessInfo]) {
236
+        if !processes.is_empty() {
237
+            self.selected_index = Some(0);
238
+            self.selected_pid = Some(processes[0].pid);
239
+            self.scroll_offset = 0;
240
+        }
241
+    }
242
+
243
+    /// Select last item (End key).
244
+    pub fn select_last(&mut self, processes: &[ProcessInfo]) {
245
+        if !processes.is_empty() {
246
+            let last_idx = processes.len() - 1;
247
+            self.selected_index = Some(last_idx);
248
+            self.selected_pid = Some(processes[last_idx].pid);
249
+            self.scroll_offset = self.max_scroll();
250
+        }
251
+    }
252
+
253
+    /// Get the currently selected index.
254
+    pub fn selected_index(&self) -> Option<usize> {
255
+        self.selected_index
129256
     }
130257
 
131258
     /// Render the process list.
@@ -224,7 +351,7 @@ impl ProcessList {
224351
             let process_idx = self.scroll_offset + i;
225352
 
226353
             // Selection highlight
227
-            if self.selected == Some(process_idx) {
354
+            if self.selected_index == Some(process_idx) {
228355
                 let row_rect = Rect::new(
229356
                     self.bounds.x + 2,
230357
                     row_y + 2,