gardesk/garterm / e04ff32

Browse files

feat: add search mode handling in app

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
e04ff329ac346515eebd52eb83c055853e47e0b6
Parents
b8188e3
Tree
cadc5b8

1 changed file

StatusFile+-
M garterm/src/app.rs 155 8
garterm/src/app.rsmodified
@@ -1,8 +1,8 @@
11
 use crate::config::{Action, Config, KeybindSet, LuaRuntime, Modifiers as ConfigModifiers, TerminalCommand, expand_tilde};
2
-use crate::input::{Clipboard, KeyboardHandler, MouseButton, MouseEvent, MouseHandler, Selection, SelectionMode};
2
+use crate::input::{Clipboard, KeyboardHandler, MouseButton, MouseEvent, MouseHandler, SearchState, Selection, SelectionMode};
33
 use crate::ipc::IpcServer;
44
 use crate::pty::{PtySize, ReceivedSignal, SignalHandler};
5
-use crate::render::{PaneRenderInfo, Renderer, SelectionBounds};
5
+use crate::render::{PaneRenderInfo, Renderer, SearchOverlay, SelectionBounds};
66
 use crate::ui::{Direction, PaneId, TabManager};
77
 use anyhow::Result;
88
 use garterm_ipc::{Command, Response};
@@ -47,6 +47,8 @@ pub struct App {
4747
     fullscreen: bool,
4848
     /// Original font size (for reset)
4949
     original_font_size: f32,
50
+    /// Search state for the focused pane
51
+    search: SearchState,
5052
 }
5153
 
5254
 impl App {
@@ -196,6 +198,7 @@ impl App {
196198
             net_wm_state_fullscreen,
197199
             fullscreen: false,
198200
             original_font_size: config.font.size,
201
+            search: SearchState::new(),
199202
         })
200203
     }
201204
 
@@ -319,12 +322,25 @@ impl App {
319322
                             focused: pane.focused,
320323
                             // Only show selection on focused pane
321324
                             selection: if pane.focused { selection_bounds } else { None },
325
+                            search: None, // Search rendering handled separately
322326
                         }).collect()
323327
                     })
324328
                     .unwrap_or_default();
325329
 
326330
                 if !pane_infos.is_empty() {
327
-                    self.renderer.render_scene(&tab_bar_data, &pane_infos)?;
331
+                    // Build search overlay if search is active or has matches
332
+                    let match_count = self.search.match_count_text();
333
+                    let search_overlay = if self.search.active || !self.search.matches.is_empty() {
334
+                        Some(SearchOverlay {
335
+                            query: &self.search.query,
336
+                            match_count: &match_count,
337
+                            active: self.search.active,
338
+                            case_insensitive: self.search.case_insensitive,
339
+                        })
340
+                    } else {
341
+                        None
342
+                    };
343
+                    self.renderer.render_scene_with_search(&tab_bar_data, &pane_infos, search_overlay.as_ref())?;
328344
                 }
329345
                 self.window.connection().flush()?;
330346
             }
@@ -497,10 +513,20 @@ impl App {
497513
                 Ok(true)
498514
             }
499515
 
500
-            // Search (TODO: implement search)
501
-            Action::SearchForward | Action::SearchBackward => {
502
-                // TODO: Implement search
503
-                Ok(false)
516
+            // Search
517
+            Action::SearchForward => {
518
+                info!("Starting forward search");
519
+                self.search.start();
520
+                self.tabs.mark_all_dirty();
521
+                Ok(true)
522
+            }
523
+            Action::SearchBackward => {
524
+                info!("Starting backward search");
525
+                self.search.start();
526
+                // For backward search, we could track direction
527
+                // but for now just start search mode
528
+                self.tabs.mark_all_dirty();
529
+                Ok(true)
504530
             }
505531
 
506532
             // Misc
@@ -1115,12 +1141,25 @@ impl App {
11151141
                                     height: pane.height,
11161142
                                     focused: pane.focused,
11171143
                                     selection: if pane.focused { selection_bounds } else { None },
1144
+                                    search: None, // Search rendering handled separately
11181145
                                 }).collect()
11191146
                             })
11201147
                             .unwrap_or_default();
11211148
 
11221149
                         if !pane_infos.is_empty() {
1123
-                            self.renderer.render_scene(&tab_bar_data, &pane_infos)?;
1150
+                            // Build search overlay if search is active or has matches
1151
+                            let match_count = self.search.match_count_text();
1152
+                            let search_overlay = if self.search.active || !self.search.matches.is_empty() {
1153
+                                Some(SearchOverlay {
1154
+                                    query: &self.search.query,
1155
+                                    match_count: &match_count,
1156
+                                    active: self.search.active,
1157
+                                    case_insensitive: self.search.case_insensitive,
1158
+                                })
1159
+                            } else {
1160
+                                None
1161
+                            };
1162
+                            self.renderer.render_scene_with_search(&tab_bar_data, &pane_infos, search_overlay.as_ref())?;
11241163
                         }
11251164
                     }
11261165
                 }
@@ -1200,6 +1239,11 @@ impl App {
12001239
         tracing::debug!("Key press: {:?}, modifiers: ctrl={}, shift={}, alt={}",
12011240
             key, modifiers.ctrl, modifiers.shift, modifiers.alt);
12021241
 
1242
+        // Handle search mode input
1243
+        if self.search.active {
1244
+            return self.handle_search_key(key, &modifiers);
1245
+        }
1246
+
12031247
         // Convert to config modifiers and key string for lookup
12041248
         let config_mods = Self::modifiers_to_config(&modifiers);
12051249
         let key_str = Self::key_to_string(&key);
@@ -1245,6 +1289,109 @@ impl App {
12451289
         Ok(())
12461290
     }
12471291
 
1292
+    /// Handle key events in search mode
1293
+    fn handle_search_key(&mut self, key: gartk_core::Key, modifiers: &gartk_core::Modifiers) -> Result<()> {
1294
+        use gartk_core::Key;
1295
+
1296
+        match key {
1297
+            // Escape or Ctrl+C cancels search
1298
+            Key::Escape => {
1299
+                self.search.cancel();
1300
+                self.tabs.mark_all_dirty();
1301
+            }
1302
+            // Enter confirms search (exits input mode but keeps highlights)
1303
+            Key::Return => {
1304
+                self.search.confirm();
1305
+                self.tabs.mark_all_dirty();
1306
+            }
1307
+            // Backspace removes last character
1308
+            Key::Backspace => {
1309
+                self.search.pop_char();
1310
+                self.update_search_matches();
1311
+                self.tabs.mark_all_dirty();
1312
+            }
1313
+            // n/N for next/previous match (when not typing)
1314
+            Key::Char('n') if modifiers.ctrl => {
1315
+                self.search.next_match();
1316
+                self.scroll_to_current_match();
1317
+                self.tabs.mark_all_dirty();
1318
+            }
1319
+            Key::Char('n') if modifiers.shift => {
1320
+                self.search.prev_match();
1321
+                self.scroll_to_current_match();
1322
+                self.tabs.mark_all_dirty();
1323
+            }
1324
+            Key::Char('n') if !modifiers.ctrl && !modifiers.shift && !modifiers.alt => {
1325
+                if self.search.query.is_empty() {
1326
+                    self.search.push_char('n');
1327
+                    self.update_search_matches();
1328
+                } else {
1329
+                    self.search.next_match();
1330
+                    self.scroll_to_current_match();
1331
+                }
1332
+                self.tabs.mark_all_dirty();
1333
+            }
1334
+            Key::Char('N') => {
1335
+                self.search.prev_match();
1336
+                self.scroll_to_current_match();
1337
+                self.tabs.mark_all_dirty();
1338
+            }
1339
+            // Ctrl+I toggles case sensitivity
1340
+            Key::Char('i') if modifiers.ctrl => {
1341
+                self.search.toggle_case_sensitive();
1342
+                self.update_search_matches();
1343
+                self.tabs.mark_all_dirty();
1344
+            }
1345
+            // Regular characters add to query
1346
+            Key::Char(c) => {
1347
+                self.search.push_char(c);
1348
+                self.update_search_matches();
1349
+                self.tabs.mark_all_dirty();
1350
+            }
1351
+            _ => {}
1352
+        }
1353
+
1354
+        Ok(())
1355
+    }
1356
+
1357
+    /// Update search matches based on current query
1358
+    fn update_search_matches(&mut self) {
1359
+        if let Some(pane) = self.tabs.focused_pane() {
1360
+            let matches = pane.terminal.grid().search(
1361
+                &self.search.query,
1362
+                self.search.case_insensitive,
1363
+            );
1364
+            self.search.set_matches(matches);
1365
+
1366
+            // Auto-scroll to first match
1367
+            if !self.search.matches.is_empty() {
1368
+                self.scroll_to_current_match();
1369
+            }
1370
+        }
1371
+    }
1372
+
1373
+    /// Scroll viewport to show the current match
1374
+    fn scroll_to_current_match(&mut self) {
1375
+        if let Some(m) = self.search.current() {
1376
+            if let Some(pane) = self.tabs.focused_pane_mut() {
1377
+                let grid = pane.terminal.grid();
1378
+                let scrollback_len = grid.scrollback_len();
1379
+
1380
+                // Calculate the scroll offset needed to show this match
1381
+                // Match row is absolute (0 = start of scrollback)
1382
+                if m.row < scrollback_len {
1383
+                    // Match is in scrollback
1384
+                    let offset = scrollback_len - m.row;
1385
+                    pane.terminal.grid_mut().set_scroll_offset(offset);
1386
+                } else {
1387
+                    // Match is in active display - scroll to bottom
1388
+                    pane.terminal.reset_viewport();
1389
+                }
1390
+                pane.mark_dirty();
1391
+            }
1392
+        }
1393
+    }
1394
+
12481395
     fn handle_button_press(&mut self, event: xproto::ButtonPressEvent) -> Result<()> {
12491396
         let (cell_w, cell_h) = self.renderer.cell_size();
12501397