fresh/view/
file_browser_input.rs1use 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
11pub 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 if alt {
33 if let KeyCode::Char(_) = event.code {
34 return InputResult::Ignored;
35 }
36 }
37
38 match event.code {
39 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 KeyCode::Enter => {
59 ctx.defer(DeferredAction::FileBrowserConfirm);
60 InputResult::Consumed
61 }
62
63 KeyCode::Tab => {
65 ctx.defer(DeferredAction::FileBrowserAcceptSuggestion);
66 InputResult::Consumed
67 }
68
69 KeyCode::Esc => {
71 ctx.defer(DeferredAction::ClosePrompt);
72 InputResult::Consumed
73 }
74
75 KeyCode::Backspace if !ctrl => {
78 if self.prompt.input.is_empty() {
79 ctx.defer(DeferredAction::FileBrowserGoParent);
80 InputResult::Consumed
81 } else {
82 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 KeyCode::Backspace if ctrl => {
95 self.prompt.delete_word_backward();
96 ctx.defer(DeferredAction::FileBrowserUpdateFilter);
97 InputResult::Consumed
98 }
99
100 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 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 KeyCode::Char(c) if ctrl => {
128 match c {
129 'a' => {
130 self.prompt.selection_anchor = Some(0);
132 self.prompt.cursor_pos = self.prompt.input.len();
133 InputResult::Consumed
134 }
135 'c' => {
136 ctx.defer(DeferredAction::ExecuteAction(
138 crate::input::keybindings::Action::PromptCopy,
139 ));
140 InputResult::Consumed
141 }
142 'x' => {
143 ctx.defer(DeferredAction::ExecuteAction(
145 crate::input::keybindings::Action::PromptCut,
146 ));
147 InputResult::Consumed
148 }
149 'v' => {
150 ctx.defer(DeferredAction::ExecuteAction(
152 crate::input::keybindings::Action::PromptPaste,
153 ));
154 InputResult::Consumed
155 }
156 'k' => {
157 self.prompt.delete_to_end();
159 ctx.defer(DeferredAction::FileBrowserUpdateFilter);
160 InputResult::Consumed
161 }
162 _ => InputResult::Consumed,
163 }
164 }
165
166 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 _ => 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 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 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}