Skip to main content

codetether_agent/tui/app/
navigation.rs

1use crossterm::event::KeyModifiers;
2
3use crate::tui::app::model_picker::close_model_picker;
4use crate::tui::app::session_sync::return_to_chat;
5use crate::tui::app::state::App;
6use crate::tui::app::symbols::symbol_search_active;
7use crate::tui::models::{InputMode, ViewMode};
8
9pub fn handle_escape(app: &mut App) {
10    if symbol_search_active(app) {
11        app.state.symbol_search.close();
12        app.state.status = "Closed symbol search".to_string();
13    } else if app.state.show_help {
14        app.state.show_help = false;
15        app.state.status = "Closed help".to_string();
16    } else if app.state.model_picker_active {
17        close_model_picker(app);
18        app.state.status = "Closed model picker".to_string();
19    } else {
20        match app.state.view_mode {
21            ViewMode::Sessions => {
22                app.state.clear_session_filter();
23                return_to_chat(app);
24            }
25            ViewMode::FilePicker => {
26                app.state.file_picker_active = false;
27                return_to_chat(app);
28            }
29            ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.exit_detail(),
30            ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.exit_detail(),
31            ViewMode::Bus if app.state.bus_log.filter_input_mode => {
32                app.state.bus_log.exit_filter_mode();
33                app.state.status = "Protocol filter closed".to_string();
34            }
35            ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.exit_detail(),
36            ViewMode::Chat => app.state.input_mode = InputMode::Normal,
37            _ => return_to_chat(app),
38        }
39    }
40}
41
42pub fn handle_tab(app: &mut App) {
43    if app.state.apply_selected_slash_suggestion() {
44        app.state.status = "Command autocompleted".to_string();
45    }
46}
47
48pub fn toggle_help(app: &mut App) {
49    app.state.show_help = !app.state.show_help;
50    app.state.help_scroll.offset = 0;
51    app.state.status = if app.state.show_help {
52        "Help".to_string()
53    } else {
54        "Closed help".to_string()
55    };
56}
57
58pub fn handle_up(app: &mut App, modifiers: KeyModifiers) {
59    if app.state.show_help {
60        app.state.help_scroll.scroll_up(1);
61        return;
62    }
63    if symbol_search_active(app) {
64        app.state.symbol_search.select_prev();
65        return;
66    }
67    if app.state.view_mode == ViewMode::Sessions {
68        app.state.sessions_select_prev();
69        return;
70    }
71    if app.state.view_mode == ViewMode::Model {
72        app.state.model_select_prev();
73        return;
74    }
75    if app.state.view_mode == ViewMode::Settings {
76        app.state.settings_select_prev();
77        return;
78    }
79    if app.state.slash_suggestions_visible() {
80        app.state.select_prev_slash_suggestion();
81        return;
82    }
83    if app.state.view_mode == ViewMode::Chat {
84        if modifiers.contains(KeyModifiers::CONTROL) {
85            let _ = app.state.history_prev();
86        } else if modifiers.contains(KeyModifiers::SHIFT) {
87            app.state.scroll_tool_preview_up(1);
88        } else {
89            app.state.scroll_up(1);
90        }
91        return;
92    }
93
94    match app.state.view_mode {
95        ViewMode::Swarm => {
96            if app.state.swarm.detail_mode {
97                app.state.swarm.detail_scroll_up(1);
98            } else {
99                app.state.swarm.select_prev();
100            }
101        }
102        ViewMode::Ralph => {
103            if app.state.ralph.detail_mode {
104                app.state.ralph.detail_scroll_up(1);
105            } else {
106                app.state.ralph.select_prev();
107            }
108        }
109        ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
110            if app.state.bus_log.detail_mode {
111                app.state.bus_log.detail_scroll_up(1);
112            } else {
113                app.state.bus_log.select_prev();
114            }
115        }
116        _ => app.state.scroll_up(1),
117    }
118}
119
120pub fn handle_down(app: &mut App, modifiers: KeyModifiers) {
121    if app.state.show_help {
122        app.state.help_scroll.scroll_down(1, 200);
123        return;
124    }
125    if symbol_search_active(app) {
126        app.state.symbol_search.select_next();
127        return;
128    }
129    if app.state.view_mode == ViewMode::Sessions {
130        app.state.sessions_select_next();
131        return;
132    }
133    if app.state.view_mode == ViewMode::Model {
134        app.state.model_select_next();
135        return;
136    }
137    if app.state.view_mode == ViewMode::Settings {
138        app.state.settings_select_next();
139        return;
140    }
141    if app.state.slash_suggestions_visible() {
142        app.state.select_next_slash_suggestion();
143        return;
144    }
145    if app.state.view_mode == ViewMode::Chat {
146        if modifiers.contains(KeyModifiers::CONTROL) {
147            let _ = app.state.history_next();
148        } else if modifiers.contains(KeyModifiers::SHIFT) {
149            app.state.scroll_tool_preview_down(1);
150        } else {
151            app.state.scroll_down(1);
152        }
153        return;
154    }
155
156    match app.state.view_mode {
157        ViewMode::Swarm => {
158            if app.state.swarm.detail_mode {
159                app.state.swarm.detail_scroll_down(1);
160            } else {
161                app.state.swarm.select_next();
162            }
163        }
164        ViewMode::Ralph => {
165            if app.state.ralph.detail_mode {
166                app.state.ralph.detail_scroll_down(1);
167            } else {
168                app.state.ralph.select_next();
169            }
170        }
171        ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
172            if app.state.bus_log.detail_mode {
173                app.state.bus_log.detail_scroll_down(1);
174            } else {
175                app.state.bus_log.select_next();
176            }
177        }
178        _ => app.state.scroll_down(1),
179    }
180}
181
182pub fn handle_page_up(app: &mut App) {
183    if app.state.show_help {
184        app.state.help_scroll.scroll_up(10);
185        return;
186    }
187
188    match app.state.view_mode {
189        ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_up(10),
190        ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_up(10),
191        ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_up(10),
192        ViewMode::Chat => app.state.scroll_up(10),
193        _ => {}
194    }
195}
196
197pub fn handle_page_down(app: &mut App) {
198    if app.state.show_help {
199        app.state.help_scroll.scroll_down(10, 200);
200        return;
201    }
202
203    match app.state.view_mode {
204        ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_down(10),
205        ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_down(10),
206        ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_down(10),
207        ViewMode::Chat => app.state.scroll_down(10),
208        _ => {}
209    }
210}
211
212pub fn handle_home(app: &mut App) {
213    if app.state.view_mode == ViewMode::Chat {
214        app.state.move_cursor_home();
215    }
216}
217
218pub fn handle_end(app: &mut App) {
219    if app.state.view_mode == ViewMode::Chat {
220        app.state.move_cursor_end();
221    }
222}
223
224pub fn handle_left(app: &mut App, modifiers: KeyModifiers) {
225    if app.state.view_mode == ViewMode::Chat {
226        if modifiers.contains(KeyModifiers::CONTROL) {
227            app.state.move_cursor_word_left();
228        } else {
229            app.state.move_cursor_left();
230        }
231    }
232}
233
234pub fn handle_right(app: &mut App, modifiers: KeyModifiers) {
235    if app.state.view_mode == ViewMode::Chat {
236        if modifiers.contains(KeyModifiers::CONTROL) {
237            app.state.move_cursor_word_right();
238        } else {
239            app.state.move_cursor_right();
240        }
241    }
242}
243
244pub fn handle_delete(app: &mut App) {
245    if app.state.view_mode == ViewMode::Chat {
246        app.state.delete_forward();
247    }
248}
249
250pub fn handle_symbol_enter(app: &mut App) {
251    if let Some(symbol) = app.state.symbol_search.selected_symbol() {
252        app.state.status = format!(
253            "Selected symbol {} {}",
254            symbol.name,
255            symbol
256                .line
257                .map(|line| format!("at line {line}"))
258                .unwrap_or_default()
259        );
260    }
261    app.state.symbol_search.close();
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn help_overlay_consumes_chat_arrow_navigation() {
270        let mut app = App::default();
271        app.state.show_help = true;
272        app.state.set_view_mode(ViewMode::Chat);
273        app.state.set_chat_max_scroll(25);
274        app.state.scroll_to_bottom();
275
276        handle_down(&mut app, KeyModifiers::NONE);
277
278        assert_eq!(app.state.help_scroll.offset, 1);
279        assert_eq!(app.state.chat_scroll, 1_000_000);
280    }
281}