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::view::prompt::PromptType;
210 use std::path::PathBuf;
211
212 fn create_test_file_state() -> FileOpenState {
213 FileOpenState::new(PathBuf::from("/tmp"), false)
214 }
215
216 fn create_test_prompt() -> Prompt {
217 Prompt::new("Open: ".to_string(), PromptType::OpenFile)
218 }
219
220 fn key(code: KeyCode) -> KeyEvent {
221 KeyEvent::new(code, KeyModifiers::NONE)
222 }
223
224 #[test]
225 fn test_navigation_keys() {
226 let mut file_state = create_test_file_state();
227 let mut prompt = create_test_prompt();
228 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
229 let mut ctx = InputContext::new();
230
231 let result = handler.handle_key_event(&key(KeyCode::Up), &mut ctx);
233 assert_eq!(result, InputResult::Consumed);
234 assert!(ctx
235 .deferred_actions
236 .iter()
237 .any(|a| matches!(a, DeferredAction::FileBrowserSelectPrev)));
238 }
239
240 #[test]
241 fn test_character_input_updates_filter() {
242 let mut file_state = create_test_file_state();
243 let mut prompt = create_test_prompt();
244 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
245 let mut ctx = InputContext::new();
246
247 handler.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
248 handler.handle_key_event(&key(KeyCode::Char('e')), &mut ctx);
249 handler.handle_key_event(&key(KeyCode::Char('s')), &mut ctx);
250 handler.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
251
252 assert_eq!(prompt.input, "test");
253 assert!(ctx
255 .deferred_actions
256 .iter()
257 .any(|a| matches!(a, DeferredAction::FileBrowserUpdateFilter)));
258 }
259
260 #[test]
261 fn test_backspace_empty_goes_parent() {
262 let mut file_state = create_test_file_state();
263 let mut prompt = create_test_prompt();
264 prompt.input = String::new();
265 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
266 let mut ctx = InputContext::new();
267
268 handler.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
269
270 assert!(ctx
271 .deferred_actions
272 .iter()
273 .any(|a| matches!(a, DeferredAction::FileBrowserGoParent)));
274 }
275
276 #[test]
277 fn test_backspace_with_text_deletes() {
278 let mut file_state = create_test_file_state();
279 let mut prompt = create_test_prompt();
280 prompt.input = "test".to_string();
281 prompt.cursor_pos = 4;
282 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
283 let mut ctx = InputContext::new();
284
285 handler.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
286
287 assert_eq!(prompt.input, "tes");
288 assert!(ctx
289 .deferred_actions
290 .iter()
291 .any(|a| matches!(a, DeferredAction::FileBrowserUpdateFilter)));
292 }
293
294 #[test]
295 fn test_is_modal() {
296 let mut file_state = create_test_file_state();
297 let mut prompt = create_test_prompt();
298 let handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
299 assert!(handler.is_modal());
300 }
301
302 #[test]
303 fn test_enter_confirms() {
304 let mut file_state = create_test_file_state();
305 let mut prompt = create_test_prompt();
306 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
307 let mut ctx = InputContext::new();
308
309 handler.handle_key_event(&key(KeyCode::Enter), &mut ctx);
310
311 assert!(ctx
312 .deferred_actions
313 .iter()
314 .any(|a| matches!(a, DeferredAction::FileBrowserConfirm)));
315 }
316
317 #[test]
318 fn test_escape_closes() {
319 let mut file_state = create_test_file_state();
320 let mut prompt = create_test_prompt();
321 let mut handler = FileBrowserInputHandler::new(&mut file_state, &mut prompt);
322 let mut ctx = InputContext::new();
323
324 handler.handle_key_event(&key(KeyCode::Esc), &mut ctx);
325
326 assert!(ctx
327 .deferred_actions
328 .iter()
329 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
330 }
331}