gardesk/gartop / 4567ee9

Browse files

Fix cursor tracking: grace period, jump-to-bottom, magenta flash

- Add 400ms grace period after navigation to prevent auto-scroll fighting
- When tracked process exits, jump to bottom of visible list (not top)
- Magenta highlight when cursor loses its target process
- Track last_nav_time and cursor_lost state
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4567ee9f1e5e7c73c48fbbadd06eea34351bb099
Parents
c5d25d6
Tree
2a8d41a

1 changed file

StatusFile+-
M gartop/src/gui/process_list.rs 54 13
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::time::Instant;
67
 use super::theme::Theme;
78
 
89
 /// Format rate (bytes/second) to human-readable compact string.
@@ -30,6 +31,9 @@ const ROW_HEIGHT: u32 = 22;
3031
 /// Header row height.
3132
 const HEADER_HEIGHT: u32 = 24;
3233
 
34
+/// Grace period after user navigation before auto-scrolling resumes (ms)
35
+const NAVIGATION_GRACE_MS: u64 = 400;
36
+
3337
 /// Process list component - renders process data without owning it.
3438
 /// Tracks selection by PID so cursor follows the process when list reorders.
3539
 pub struct ProcessList {
@@ -42,6 +46,10 @@ pub struct ProcessList {
4246
     sort_field: SortField,
4347
     visible_rows: usize,
4448
     process_count: usize,
49
+    /// Last time user navigated (for grace period)
50
+    last_nav_time: Option<Instant>,
51
+    /// Whether cursor lost its target and needs visual indicator
52
+    cursor_lost: bool,
4553
 }
4654
 
4755
 impl ProcessList {
@@ -56,6 +64,8 @@ impl ProcessList {
5664
             sort_field: SortField::Cpu,
5765
             visible_rows,
5866
             process_count: 0,
67
+            last_nav_time: None,
68
+            cursor_lost: false,
5969
         }
6070
     }
6171
 
@@ -67,40 +77,48 @@ impl ProcessList {
6777
 
6878
     /// Sync selection with updated process list.
6979
     /// Call this after refreshing process data to update the cursor position.
70
-    /// Returns true if selection is still valid and visible.
80
+    /// Returns true if selection is still valid.
7181
     pub fn sync_selection(&mut self, processes: &[ProcessInfo]) -> bool {
7282
         self.process_count = processes.len();
83
+        self.cursor_lost = false;
7384
 
7485
         // Clamp scroll offset
7586
         if self.scroll_offset > self.max_scroll() {
7687
             self.scroll_offset = self.max_scroll();
7788
         }
7889
 
90
+        // Check if we're in grace period after user navigation
91
+        let in_grace_period = self.last_nav_time
92
+            .map(|t| t.elapsed().as_millis() < NAVIGATION_GRACE_MS as u128)
93
+            .unwrap_or(false);
94
+
7995
         // Find the selected PID in the new list
8096
         if let Some(pid) = self.selected_pid {
8197
             if let Some(idx) = processes.iter().position(|p| p.pid == pid) {
98
+                // Process found - update index
8299
                 self.selected_index = Some(idx);
83100
 
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
101
+                // Only auto-scroll if NOT in grace period
102
+                if !in_grace_period {
103
+                    // Keep cursor visible if it moved
88104
                     if idx < self.scroll_offset {
89105
                         self.scroll_offset = idx;
90106
                     } else if idx >= self.scroll_offset + self.visible_rows {
91107
                         self.scroll_offset = idx.saturating_sub(self.visible_rows - 1);
92108
                     }
93
-                    return true;
109
+                }
110
+                return true;
111
+            } else {
112
+                // Process no longer exists - jump to last visible row
113
+                self.cursor_lost = true;
114
+                let last_visible = (self.scroll_offset + self.visible_rows - 1).min(processes.len().saturating_sub(1));
115
+                if !processes.is_empty() {
116
+                    self.selected_index = Some(last_visible);
117
+                    self.selected_pid = Some(processes[last_visible].pid);
94118
                 } else {
95
-                    // Process moved too far down, clear selection
96119
                     self.selected_pid = None;
97120
                     self.selected_index = None;
98
-                    return false;
99121
                 }
100
-            } else {
101
-                // Process no longer exists, clear selection
102
-                self.selected_pid = None;
103
-                self.selected_index = None;
104122
                 return false;
105123
             }
106124
         }
@@ -159,6 +177,8 @@ impl ProcessList {
159177
         let process_idx = self.scroll_offset + row;
160178
 
161179
         if process_idx < processes.len() {
180
+            self.last_nav_time = Some(Instant::now());
181
+            self.cursor_lost = false;
162182
             let pid = processes[process_idx].pid;
163183
             self.selected_pid = Some(pid);
164184
             self.selected_index = Some(process_idx);
@@ -184,6 +204,9 @@ impl ProcessList {
184204
         if processes.is_empty() {
185205
             return;
186206
         }
207
+        self.last_nav_time = Some(Instant::now());
208
+        self.cursor_lost = false;
209
+
187210
         match self.selected_index {
188211
             None => {
189212
                 // Select first visible item
@@ -210,6 +233,9 @@ impl ProcessList {
210233
         if processes.is_empty() {
211234
             return;
212235
         }
236
+        self.last_nav_time = Some(Instant::now());
237
+        self.cursor_lost = false;
238
+
213239
         match self.selected_index {
214240
             None => {
215241
                 // Select first visible item
@@ -234,6 +260,8 @@ impl ProcessList {
234260
     /// Select first item (Home key).
235261
     pub fn select_first(&mut self, processes: &[ProcessInfo]) {
236262
         if !processes.is_empty() {
263
+            self.last_nav_time = Some(Instant::now());
264
+            self.cursor_lost = false;
237265
             self.selected_index = Some(0);
238266
             self.selected_pid = Some(processes[0].pid);
239267
             self.scroll_offset = 0;
@@ -243,6 +271,8 @@ impl ProcessList {
243271
     /// Select last item (End key).
244272
     pub fn select_last(&mut self, processes: &[ProcessInfo]) {
245273
         if !processes.is_empty() {
274
+            self.last_nav_time = Some(Instant::now());
275
+            self.cursor_lost = false;
246276
             let last_idx = processes.len() - 1;
247277
             self.selected_index = Some(last_idx);
248278
             self.selected_pid = Some(processes[last_idx].pid);
@@ -255,6 +285,11 @@ impl ProcessList {
255285
         self.selected_index
256286
     }
257287
 
288
+    /// Check if cursor just lost its target (for visual feedback).
289
+    pub fn is_cursor_lost(&self) -> bool {
290
+        self.cursor_lost
291
+    }
292
+
258293
     /// Render the process list.
259294
     pub fn render(&self, renderer: &Renderer, theme: &Theme, processes: &[ProcessInfo]) -> anyhow::Result<()> {
260295
         // Background
@@ -358,7 +393,13 @@ impl ProcessList {
358393
                     self.bounds.width - 4,
359394
                     ROW_HEIGHT - 4,
360395
                 );
361
-                renderer.fill_rounded_rect(row_rect, 2.0, theme.header_bg)?;
396
+                // Use magenta highlight when cursor lost its target
397
+                let highlight_color = if self.cursor_lost {
398
+                    gartk_core::Color::new(0.6, 0.2, 0.6, 1.0) // Magenta
399
+                } else {
400
+                    theme.header_bg
401
+                };
402
+                renderer.fill_rounded_rect(row_rect, 2.0, highlight_color)?;
362403
             }
363404
 
364405
             // PID