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::Protocol => {
208            app.state.protocol_scroll = app.state.protocol_scroll.saturating_add(10);
209        }
210        ViewMode::Chat => app.state.scroll_down(10),
211        _ => {}
212    }
213}
214
215pub fn handle_home(app: &mut App) {
216    if app.state.view_mode == ViewMode::Chat {
217        app.state.move_cursor_home();
218    }
219}
220
221pub fn handle_end(app: &mut App) {
222    if app.state.view_mode == ViewMode::Chat {
223        app.state.move_cursor_end();
224    }
225}
226
227pub fn handle_left(app: &mut App, modifiers: KeyModifiers) {
228    if app.state.view_mode == ViewMode::Chat {
229        if modifiers.contains(KeyModifiers::CONTROL) {
230            app.state.move_cursor_word_left();
231        } else {
232            app.state.move_cursor_left();
233        }
234    }
235}
236
237pub fn handle_right(app: &mut App, modifiers: KeyModifiers) {
238    if app.state.view_mode == ViewMode::Chat {
239        if modifiers.contains(KeyModifiers::CONTROL) {
240            app.state.move_cursor_word_right();
241        } else {
242            app.state.move_cursor_right();
243        }
244    }
245}
246
247pub fn handle_delete(app: &mut App) {
248    if app.state.view_mode == ViewMode::Chat {
249        app.state.delete_forward();
250    }
251}
252
253pub fn handle_symbol_enter(app: &mut App) {
254    if let Some(symbol) = app.state.symbol_search.selected_symbol() {
255        app.state.status = format!(
256            "Selected symbol {} {}",
257            symbol.name,
258            symbol
259                .line
260                .map(|line| format!("at line {line}"))
261                .unwrap_or_default()
262        );
263    }
264    app.state.symbol_search.close();
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn help_overlay_consumes_chat_arrow_navigation() {
273        let mut app = App::default();
274        app.state.show_help = true;
275        app.state.set_view_mode(ViewMode::Chat);
276        app.state.set_chat_max_scroll(25);
277        app.state.scroll_to_bottom();
278
279        handle_down(&mut app, KeyModifiers::NONE);
280
281        assert_eq!(app.state.help_scroll.offset, 1);
282        assert_eq!(app.state.chat_scroll, 1_000_000);
283    }
284}