Skip to main content

fresh/view/
file_browser_input.rs

1//! Input handling for the File Browser (Open File / Switch Project).
2//!
3//! This handler wraps both the file browser state and the prompt,
4//! handling navigation while delegating text input to the prompt.
5
6use crate::app::file_open::FileOpenState;
7use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
8use crate::view::prompt::Prompt;
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11/// Input handler for file browser prompts (OpenFile, SwitchProject).
12///
13/// Handles navigation keys (Up, Down, Enter, Tab, etc.) directly,
14/// delegates text editing to the wrapped Prompt.
15pub struct FileBrowserInputHandler<'a> {
16    pub file_state: &'a mut FileOpenState,
17    pub prompt: &'a mut Prompt,
18}
19
20impl<'a> FileBrowserInputHandler<'a> {
21    pub fn new(file_state: &'a mut FileOpenState, prompt: &'a mut Prompt) -> Self {
22        Self { file_state, prompt }
23    }
24}
25
26impl<'a> InputHandler for FileBrowserInputHandler<'a> {
27    fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
28        let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
29        let alt = event.modifiers.contains(KeyModifiers::ALT);
30
31        // Alt+key combinations pass through to keybindings (including Alt+. for toggle hidden)
32        if alt {
33            if let KeyCode::Char(_) = event.code {
34                return InputResult::Ignored;
35            }
36        }
37
38        match event.code {
39            // Navigation in file list
40            KeyCode::Up => {
41                ctx.defer(DeferredAction::FileBrowserSelectPrev);
42                InputResult::Consumed
43            }
44            KeyCode::Down => {
45                ctx.defer(DeferredAction::FileBrowserSelectNext);
46                InputResult::Consumed
47            }
48            KeyCode::PageUp => {
49                ctx.defer(DeferredAction::FileBrowserPageUp);
50                InputResult::Consumed
51            }
52            KeyCode::PageDown => {
53                ctx.defer(DeferredAction::FileBrowserPageDown);
54                InputResult::Consumed
55            }
56
57            // Confirmation
58            KeyCode::Enter => {
59                ctx.defer(DeferredAction::FileBrowserConfirm);
60                InputResult::Consumed
61            }
62
63            // Tab accepts suggestion / navigates into directory
64            KeyCode::Tab => {
65                ctx.defer(DeferredAction::FileBrowserAcceptSuggestion);
66                InputResult::Consumed
67            }
68
69            // Escape cancels
70            KeyCode::Esc => {
71                ctx.defer(DeferredAction::ClosePrompt);
72                InputResult::Consumed
73            }
74
75            // Backspace: if input is empty, go to parent directory
76            // Otherwise, delegate to prompt for character deletion
77            KeyCode::Backspace if !ctrl => {
78                if self.prompt.input.is_empty() {
79                    ctx.defer(DeferredAction::FileBrowserGoParent);
80                    InputResult::Consumed
81                } else {
82                    // Delegate to prompt for backspace
83                    if self.prompt.has_selection() {
84                        self.prompt.delete_selection();
85                    } else {
86                        self.prompt.backspace();
87                    }
88                    ctx.defer(DeferredAction::FileBrowserUpdateFilter);
89                    InputResult::Consumed
90                }
91            }
92
93            // Ctrl+Backspace: delete word backward
94            KeyCode::Backspace if ctrl => {
95                self.prompt.delete_word_backward();
96                ctx.defer(DeferredAction::FileBrowserUpdateFilter);
97                InputResult::Consumed
98            }
99
100            // Delete key
101            KeyCode::Delete if ctrl => {
102                self.prompt.delete_word_forward();
103                ctx.defer(DeferredAction::FileBrowserUpdateFilter);
104                InputResult::Consumed
105            }
106            KeyCode::Delete => {
107                if self.prompt.has_selection() {
108                    self.prompt.delete_selection();
109                } else {
110                    self.prompt.delete();
111                }
112                ctx.defer(DeferredAction::FileBrowserUpdateFilter);
113                InputResult::Consumed
114            }
115
116            // Character input - insert into prompt and update filter
117            KeyCode::Char(c) if !ctrl && !alt => {
118                if self.prompt.has_selection() {
119                    self.prompt.delete_selection();
120                }
121                self.prompt.insert_char(c);
122                ctx.defer(DeferredAction::FileBrowserUpdateFilter);
123                InputResult::Consumed
124            }
125
126            // Ctrl+key combinations
127            KeyCode::Char(c) if ctrl => {
128                match c {
129                    'a' => {
130                        // Select all
131                        self.prompt.selection_anchor = Some(0);
132                        self.prompt.cursor_pos = self.prompt.input.len();
133                        InputResult::Consumed
134                    }
135                    'c' => {
136                        // Copy - defer to Editor for clipboard
137                        ctx.defer(DeferredAction::ExecuteAction(
138                            crate::input::keybindings::Action::PromptCopy,
139                        ));
140                        InputResult::Consumed
141                    }
142                    'x' => {
143                        // Cut
144                        ctx.defer(DeferredAction::ExecuteAction(
145                            crate::input::keybindings::Action::PromptCut,
146                        ));
147                        InputResult::Consumed
148                    }
149                    'v' => {
150                        // Paste
151                        ctx.defer(DeferredAction::ExecuteAction(
152                            crate::input::keybindings::Action::PromptPaste,
153                        ));
154                        InputResult::Consumed
155                    }
156                    'k' => {
157                        // Delete to end of line
158                        self.prompt.delete_to_end();
159                        ctx.defer(DeferredAction::FileBrowserUpdateFilter);
160                        InputResult::Consumed
161                    }
162                    _ => InputResult::Consumed,
163                }
164            }
165
166            // Cursor movement within prompt
167            KeyCode::Left if ctrl => {
168                self.prompt.move_word_left();
169                InputResult::Consumed
170            }
171            KeyCode::Left => {
172                self.prompt.clear_selection();
173                self.prompt.cursor_left();
174                InputResult::Consumed
175            }
176            KeyCode::Right if ctrl => {
177                self.prompt.move_word_right();
178                InputResult::Consumed
179            }
180            KeyCode::Right => {
181                self.prompt.clear_selection();
182                self.prompt.cursor_right();
183                InputResult::Consumed
184            }
185            KeyCode::Home => {
186                self.prompt.clear_selection();
187                self.prompt.move_to_start();
188                InputResult::Consumed
189            }
190            KeyCode::End => {
191                self.prompt.clear_selection();
192                self.prompt.move_to_end();
193                InputResult::Consumed
194            }
195
196            // Consume all other keys (modal behavior)
197            _ => InputResult::Consumed,
198        }
199    }
200
201    fn is_modal(&self) -> bool {
202        true
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::model::filesystem::StdFileSystem;
210    use crate::view::prompt::PromptType;
211    use std::path::PathBuf;
212    use std::sync::Arc;
213
214    fn create_test_file_state() -> FileOpenState {
215        FileOpenState::new(PathBuf::from("/tmp"), false, Arc::new(StdFileSystem))
216    }
217
218    fn create_test_prompt() -> Prompt {
219        Prompt::new("Open: ".to_string(), PromptType::OpenFile)
220    }
221
222    fn key(code: KeyCode) -> KeyEvent {
223        KeyEvent::new(code, KeyModifiers::NONE)
224    }
225
226    #[test]
227    fn test_navigation_keys() {
228        let mut file_state = create_test_file_state();
229        let mut prompt = create_test_prompt();
230        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
231        let mut ctx = InputContext::new();
232
233        // Up should defer FileBrowserSelectPrev
234        let result = handler.handle_key_event(&key(KeyCode::Up), &mut ctx);
235        assert_eq!(result, InputResult::Consumed);
236        assert!(ctx
237            .deferred_actions
238            .iter()
239            .any(|a| matches!(a, DeferredAction::FileBrowserSelectPrev)));
240    }
241
242    #[test]
243    fn test_character_input_updates_filter() {
244        let mut file_state = create_test_file_state();
245        let mut prompt = create_test_prompt();
246        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
247        let mut ctx = InputContext::new();
248
249        handler.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
250        handler.handle_key_event(&key(KeyCode::Char('e')), &mut ctx);
251        handler.handle_key_event(&key(KeyCode::Char('s')), &mut ctx);
252        handler.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
253
254        assert_eq!(prompt.input, "test");
255        // Should have deferred filter updates
256        assert!(ctx
257            .deferred_actions
258            .iter()
259            .any(|a| matches!(a, DeferredAction::FileBrowserUpdateFilter)));
260    }
261
262    #[test]
263    fn test_backspace_empty_goes_parent() {
264        let mut file_state = create_test_file_state();
265        let mut prompt = create_test_prompt();
266        prompt.input = String::new();
267        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
268        let mut ctx = InputContext::new();
269
270        handler.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
271
272        assert!(ctx
273            .deferred_actions
274            .iter()
275            .any(|a| matches!(a, DeferredAction::FileBrowserGoParent)));
276    }
277
278    #[test]
279    fn test_backspace_with_text_deletes() {
280        let mut file_state = create_test_file_state();
281        let mut prompt = create_test_prompt();
282        prompt.input = "test".to_string();
283        prompt.cursor_pos = 4;
284        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
285        let mut ctx = InputContext::new();
286
287        handler.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
288
289        assert_eq!(prompt.input, "tes");
290        assert!(ctx
291            .deferred_actions
292            .iter()
293            .any(|a| matches!(a, DeferredAction::FileBrowserUpdateFilter)));
294    }
295
296    #[test]
297    fn test_is_modal() {
298        let mut file_state = create_test_file_state();
299        let mut prompt = create_test_prompt();
300        let handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
301        assert!(handler.is_modal());
302    }
303
304    #[test]
305    fn test_enter_confirms() {
306        let mut file_state = create_test_file_state();
307        let mut prompt = create_test_prompt();
308        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
309        let mut ctx = InputContext::new();
310
311        handler.handle_key_event(&key(KeyCode::Enter), &mut ctx);
312
313        assert!(ctx
314            .deferred_actions
315            .iter()
316            .any(|a| matches!(a, DeferredAction::FileBrowserConfirm)));
317    }
318
319    #[test]
320    fn test_escape_closes() {
321        let mut file_state = create_test_file_state();
322        let mut prompt = create_test_prompt();
323        let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
324        let mut ctx = InputContext::new();
325
326        handler.handle_key_event(&key(KeyCode::Esc), &mut ctx);
327
328        assert!(ctx
329            .deferred_actions
330            .iter()
331            .any(|a| matches!(a, DeferredAction::ClosePrompt)));
332    }
333}