gardesk/gartop / e505293

Browse files

Add type-to-jump fuzzy matching, change freeze to Alt+f

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
e505293a0a9e6a98c4b0eecac33d0d15b64568ef
Parents
a0dda54
Tree
580eb3f

2 changed files

StatusFile+-
M gartop/src/gui/app.rs 119 14
M gartop/src/gui/process_list.rs 15 0
gartop/src/gui/app.rsmodified
@@ -54,6 +54,10 @@ pub struct App {
5454
     search_query: String,
5555
     /// Help overlay visible
5656
     show_help: bool,
57
+    /// Type-to-jump fuzzy pattern
58
+    jump_pattern: String,
59
+    /// Last time a jump character was typed (for timeout)
60
+    jump_time: Option<Instant>,
5761
     status: Option<StatusInfo>,
5862
     cpu_stats: Option<CpuStats>,
5963
     memory_stats: Option<MemoryStats>,
@@ -130,6 +134,8 @@ impl App {
130134
             search_mode: false,
131135
             search_query: String::new(),
132136
             show_help: false,
137
+            jump_pattern: String::new(),
138
+            jump_time: None,
133139
             status: None,
134140
             cpu_stats: None,
135141
             memory_stats: None,
@@ -410,19 +416,19 @@ impl App {
410416
 
411417
         let bindings = [
412418
             ("?", "Toggle this help"),
413
-            ("q / Esc", "Quit / clear search"),
419
+            ("q / Esc", "Quit / clear"),
414420
             ("", ""),
415421
             ("1-4", "Switch tab"),
416422
             ("Tab", "Cycle tabs"),
417423
             ("", ""),
418
-            ("j / \u{2193}", "Next process"),
419
-            ("k / \u{2191}", "Previous process"),
420
-            ("Home", "First process"),
421
-            ("End", "Last process"),
424
+            ("\u{2191} / \u{2193}", "Navigate list"),
425
+            ("Home / End", "First / last"),
422426
             ("PgUp/PgDn", "Jump 10 rows"),
423427
             ("", ""),
424
-            ("f", "Freeze list"),
425
-            ("/", "Search by name"),
428
+            ("Alt+f", "Freeze list"),
429
+            ("/", "Search filter"),
430
+            ("a-z", "Fuzzy jump"),
431
+            ("Backspace", "Delete jump char"),
426432
             ("", ""),
427433
             ("K", "Kill (SIGTERM)"),
428434
             ("X", "Kill (SIGKILL)"),
@@ -719,6 +725,22 @@ impl App {
719725
             self.renderer.text(&search_text, CONTENT_PADDING as f64, search_y as f64, &search_style)?;
720726
         }
721727
 
728
+        // Jump pattern indicator (right side, above process list)
729
+        if !self.jump_pattern.is_empty() {
730
+            let jump_y = (HEADER_HEIGHT + TAB_BAR_HEIGHT + SECTION_GAP + 20 + GRAPH_HEIGHT + SECTION_GAP) as i32 - 20;
731
+            let jump_style = TextStyle {
732
+                font_family: "monospace".to_string(),
733
+                font_size: 11.0,
734
+                color: self.theme.network_color,
735
+                ..Default::default()
736
+            };
737
+            let jump_text = format!("jump: {}", self.jump_pattern);
738
+            // Position on right side
739
+            let text_width = self.renderer.measure_text(&jump_text, &jump_style)?.width as f64;
740
+            let jump_x = (self.width as f64) - CONTENT_PADDING as f64 - text_width;
741
+            self.renderer.text(&jump_text, jump_x, jump_y as f64, &jump_style)?;
742
+        }
743
+
722744
         // Filter processes if search query is active
723745
         let display_processes: Vec<ProcessInfo> = if self.search_query.is_empty() {
724746
             self.processes.clone()
@@ -872,6 +894,35 @@ impl App {
872894
         }
873895
     }
874896
 
897
+    /// Handle type-to-jump fuzzy matching.
898
+    fn handle_fuzzy_jump(&mut self, c: char) {
899
+        // Add character to pattern
900
+        self.jump_pattern.push(c);
901
+        self.jump_time = Some(Instant::now());
902
+
903
+        // Find first fuzzy match
904
+        if let Some(idx) = self.find_fuzzy_match(&self.jump_pattern) {
905
+            // Select and scroll to the match
906
+            if idx < self.processes.len() {
907
+                let pid = self.processes[idx].pid;
908
+                self.process_list.select_by_index(idx, pid);
909
+            }
910
+        }
911
+    }
912
+
913
+    /// Find first process matching fuzzy pattern.
914
+    /// Returns index of first match or None.
915
+    fn find_fuzzy_match(&self, pattern: &str) -> Option<usize> {
916
+        let pattern_lower = pattern.to_lowercase();
917
+
918
+        for (idx, proc) in self.processes.iter().enumerate() {
919
+            if fuzzy_match(&proc.name.to_lowercase(), &pattern_lower) {
920
+                return Some(idx);
921
+            }
922
+        }
923
+        None
924
+    }
925
+
875926
     /// Run the GUI event loop.
876927
     pub fn run(mut self) -> Result<()> {
877928
         let config = EventLoopConfig {
@@ -956,8 +1007,13 @@ impl App {
9561007
                         // Normal mode key handling
9571008
                         match key_event.key {
9581009
                             Key::Escape => {
959
-                                if !self.search_query.is_empty() {
960
-                                    // Clear search filter first
1010
+                                if !self.jump_pattern.is_empty() {
1011
+                                    // Clear jump pattern first
1012
+                                    self.jump_pattern.clear();
1013
+                                    self.jump_time = None;
1014
+                                    ev_loop.request_redraw();
1015
+                                } else if !self.search_query.is_empty() {
1016
+                                    // Clear search filter second
9611017
                                     self.search_query.clear();
9621018
                                     ev_loop.request_redraw();
9631019
                                 } else {
@@ -1015,20 +1071,35 @@ impl App {
10151071
                                 self.sort_processes(sort_field);
10161072
                                 ev_loop.request_redraw();
10171073
                             }
1018
-                            // Freeze mode toggle (pause process list updates)
1019
-                            Key::Char('f') => {
1074
+                            // Freeze mode toggle (Alt+f)
1075
+                            Key::Char('f') if key_event.modifiers.alt => {
10201076
                                 self.frozen = !self.frozen;
10211077
                                 ev_loop.request_redraw();
10221078
                             }
1023
-                            // Process list navigation
1024
-                            Key::Down | Key::Char('j') => {
1079
+                            // Process list navigation (arrow keys only - j/k go to fuzzy jump)
1080
+                            Key::Down => {
10251081
                                 self.process_list.select_next(&self.processes);
10261082
                                 ev_loop.request_redraw();
10271083
                             }
1028
-                            Key::Up | Key::Char('k') => {
1084
+                            Key::Up => {
10291085
                                 self.process_list.select_prev(&self.processes);
10301086
                                 ev_loop.request_redraw();
10311087
                             }
1088
+                            // Backspace - delete last character from jump pattern
1089
+                            Key::Backspace if !self.jump_pattern.is_empty() => {
1090
+                                self.jump_pattern.pop();
1091
+                                self.jump_time = Some(Instant::now());
1092
+                                // Re-run match with shorter pattern
1093
+                                if !self.jump_pattern.is_empty() {
1094
+                                    if let Some(idx) = self.find_fuzzy_match(&self.jump_pattern.clone()) {
1095
+                                        if idx < self.processes.len() {
1096
+                                            let pid = self.processes[idx].pid;
1097
+                                            self.process_list.select_by_index(idx, pid);
1098
+                                        }
1099
+                                    }
1100
+                                }
1101
+                                ev_loop.request_redraw();
1102
+                            }
10321103
                             Key::Home => {
10331104
                                 self.process_list.select_first(&self.processes);
10341105
                                 ev_loop.request_redraw();
@@ -1064,6 +1135,11 @@ impl App {
10641135
                                     ev_loop.request_redraw();
10651136
                                 }
10661137
                             }
1138
+                            // Type-to-jump fuzzy matching (lowercase letters only, no modifiers)
1139
+                            Key::Char(c) if c.is_ascii_lowercase() && !key_event.modifiers.alt && !key_event.modifiers.ctrl => {
1140
+                                self.handle_fuzzy_jump(c);
1141
+                                ev_loop.request_redraw();
1142
+                            }
10671143
                             _ => {}
10681144
                         }
10691145
                     }
@@ -1074,6 +1150,15 @@ impl App {
10741150
                 }
10751151
 
10761152
                 InputEvent::Idle => {
1153
+                    // Clear stale jump pattern (1.5s timeout)
1154
+                    if let Some(t) = self.jump_time {
1155
+                        if t.elapsed().as_millis() > 1500 && !self.jump_pattern.is_empty() {
1156
+                            self.jump_pattern.clear();
1157
+                            self.jump_time = None;
1158
+                            ev_loop.request_redraw();
1159
+                        }
1160
+                    }
1161
+
10771162
                     if self.last_refresh.elapsed().as_secs_f64() >= self.refresh_interval {
10781163
                         if self.daemon_available {
10791164
                             self.refresh_data();
@@ -1148,3 +1233,23 @@ fn format_rate(bytes_per_sec: f64) -> String {
11481233
         format!("{:.0} B/s", bytes_per_sec)
11491234
     }
11501235
 }
1236
+
1237
+/// Fuzzy match: check if all characters of pattern appear in target in order.
1238
+/// Example: "grtrm" matches "garterm" because g-a-r-t-e-r-m contains g-r-t-r-m in order.
1239
+fn fuzzy_match(target: &str, pattern: &str) -> bool {
1240
+    let mut pattern_chars = pattern.chars().peekable();
1241
+
1242
+    for c in target.chars() {
1243
+        if let Some(&p) = pattern_chars.peek() {
1244
+            if c == p {
1245
+                pattern_chars.next();
1246
+            }
1247
+        } else {
1248
+            // All pattern chars matched
1249
+            return true;
1250
+        }
1251
+    }
1252
+
1253
+    // Check if all pattern chars were consumed
1254
+    pattern_chars.peek().is_none()
1255
+}
gartop/src/gui/process_list.rsmodified
@@ -199,6 +199,21 @@ impl ProcessList {
199199
         self.selected_index = None;
200200
     }
201201
 
202
+    /// Select by index and PID (for fuzzy jump).
203
+    pub fn select_by_index(&mut self, idx: usize, pid: i32) {
204
+        self.last_nav_time = Some(Instant::now());
205
+        self.cursor_lost = false;
206
+        self.selected_index = Some(idx);
207
+        self.selected_pid = Some(pid);
208
+
209
+        // Scroll to make selection visible
210
+        if idx < self.scroll_offset {
211
+            self.scroll_offset = idx;
212
+        } else if idx >= self.scroll_offset + self.visible_rows {
213
+            self.scroll_offset = idx.saturating_sub(self.visible_rows - 1);
214
+        }
215
+    }
216
+
202217
     /// Move selection down by one row.
203218
     pub fn select_next(&mut self, processes: &[ProcessInfo]) {
204219
         if processes.is_empty() {