fresh/view/
prompt_input.rs1use super::prompt::Prompt;
7use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10impl InputHandler for Prompt {
11 fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
12 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
13 let alt = event.modifiers.contains(KeyModifiers::ALT);
14 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
15
16 match event.code {
17 KeyCode::Enter => {
19 ctx.defer(DeferredAction::ConfirmPrompt);
20 InputResult::Consumed
21 }
22 KeyCode::Esc => {
23 ctx.defer(DeferredAction::ClosePrompt);
24 InputResult::Consumed
25 }
26
27 KeyCode::Char(_) if alt => InputResult::Ignored,
29
30 KeyCode::Char(c) if !ctrl => {
32 if self.has_selection() {
34 self.delete_selection();
35 }
36 if shift {
37 self.insert_char(c.to_ascii_uppercase());
38 } else {
39 self.insert_char(c);
40 }
41 ctx.defer(DeferredAction::UpdatePromptSuggestions);
42 InputResult::Consumed
43 }
44 KeyCode::Char(c) if ctrl => self.handle_ctrl_key(c, ctx),
45
46 KeyCode::Backspace if ctrl => {
48 self.delete_word_backward();
49 ctx.defer(DeferredAction::UpdatePromptSuggestions);
50 InputResult::Consumed
51 }
52 KeyCode::Backspace => {
53 if self.has_selection() {
54 self.delete_selection();
55 } else {
56 self.backspace();
57 }
58 ctx.defer(DeferredAction::UpdatePromptSuggestions);
59 InputResult::Consumed
60 }
61 KeyCode::Delete if ctrl => {
62 self.delete_word_forward();
63 ctx.defer(DeferredAction::UpdatePromptSuggestions);
64 InputResult::Consumed
65 }
66 KeyCode::Delete => {
67 if self.has_selection() {
68 self.delete_selection();
69 } else {
70 self.delete();
71 }
72 ctx.defer(DeferredAction::UpdatePromptSuggestions);
73 InputResult::Consumed
74 }
75
76 KeyCode::Left if ctrl && shift => {
78 self.move_word_left_selecting();
79 InputResult::Consumed
80 }
81 KeyCode::Left if ctrl => {
82 self.move_word_left();
83 InputResult::Consumed
84 }
85 KeyCode::Left if shift => {
86 self.move_left_selecting();
87 InputResult::Consumed
88 }
89 KeyCode::Left => {
90 self.clear_selection();
91 self.cursor_left();
92 InputResult::Consumed
93 }
94 KeyCode::Right if ctrl && shift => {
95 self.move_word_right_selecting();
96 InputResult::Consumed
97 }
98 KeyCode::Right if ctrl => {
99 self.move_word_right();
100 InputResult::Consumed
101 }
102 KeyCode::Right if shift => {
103 self.move_right_selecting();
104 InputResult::Consumed
105 }
106 KeyCode::Right => {
107 self.clear_selection();
108 self.cursor_right();
109 InputResult::Consumed
110 }
111 KeyCode::Home if shift => {
112 self.move_home_selecting();
113 InputResult::Consumed
114 }
115 KeyCode::Home => {
116 self.clear_selection();
117 self.move_to_start();
118 InputResult::Consumed
119 }
120 KeyCode::End if shift => {
121 self.move_end_selecting();
122 InputResult::Consumed
123 }
124 KeyCode::End => {
125 self.clear_selection();
126 self.move_to_end();
127 InputResult::Consumed
128 }
129
130 KeyCode::Up => {
136 if !self.suggestions.is_empty() {
137 if let Some(selected) = self.selected_suggestion {
139 let new_selected = if selected == 0 { 0 } else { selected - 1 };
140 self.selected_suggestion = Some(new_selected);
141 let should_sync = self.sync_input_on_navigate
144 || !matches!(
145 self.prompt_type,
146 crate::view::prompt::PromptType::Plugin { .. }
147 | crate::view::prompt::PromptType::QuickOpen
148 );
149 if should_sync {
150 if let Some(suggestion) = self.suggestions.get(new_selected) {
151 self.input = suggestion.get_value().to_string();
152 self.cursor_pos = self.input.len();
153 self.selection_anchor = Some(0);
154 }
155 }
156 if matches!(
158 self.prompt_type,
159 crate::view::prompt::PromptType::SelectTheme { .. }
160 ) {
161 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
162 }
163 if matches!(
165 self.prompt_type,
166 crate::view::prompt::PromptType::Plugin { .. }
167 ) {
168 ctx.defer(DeferredAction::PromptSelectionChanged {
169 selected_index: new_selected,
170 });
171 }
172 }
173 } else {
174 ctx.defer(DeferredAction::PromptHistoryPrev);
176 }
177 InputResult::Consumed
178 }
179 KeyCode::Down => {
180 if !self.suggestions.is_empty() {
181 if let Some(selected) = self.selected_suggestion {
183 let new_selected = (selected + 1).min(self.suggestions.len() - 1);
184 self.selected_suggestion = Some(new_selected);
185 let should_sync = self.sync_input_on_navigate
188 || !matches!(
189 self.prompt_type,
190 crate::view::prompt::PromptType::Plugin { .. }
191 | crate::view::prompt::PromptType::QuickOpen
192 );
193 if should_sync {
194 if let Some(suggestion) = self.suggestions.get(new_selected) {
195 self.input = suggestion.get_value().to_string();
196 self.cursor_pos = self.input.len();
197 self.selection_anchor = Some(0);
198 }
199 }
200 if matches!(
202 self.prompt_type,
203 crate::view::prompt::PromptType::SelectTheme { .. }
204 ) {
205 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
206 }
207 if matches!(
209 self.prompt_type,
210 crate::view::prompt::PromptType::Plugin { .. }
211 ) {
212 ctx.defer(DeferredAction::PromptSelectionChanged {
213 selected_index: new_selected,
214 });
215 }
216 }
217 } else {
218 ctx.defer(DeferredAction::PromptHistoryNext);
220 }
221 InputResult::Consumed
222 }
223 KeyCode::PageUp => {
224 if let Some(selected) = self.selected_suggestion {
225 self.selected_suggestion = Some(selected.saturating_sub(10));
226 }
227 InputResult::Consumed
228 }
229 KeyCode::PageDown => {
230 if let Some(selected) = self.selected_suggestion {
231 let len = self.suggestions.len();
232 let new_pos = selected + 10;
233 self.selected_suggestion = Some(new_pos.min(len.saturating_sub(1)));
234 }
235 InputResult::Consumed
236 }
237
238 KeyCode::Tab => {
240 if self.overlay {
247 return InputResult::Consumed;
248 }
249 if let Some(selected) = self.selected_suggestion {
250 if let Some(suggestion) = self.suggestions.get(selected) {
251 if !suggestion.disabled {
252 let value = suggestion.get_value().to_string();
253 if matches!(
255 self.prompt_type,
256 crate::view::prompt::PromptType::QuickOpen
257 ) {
258 let prefix = self
259 .input
260 .chars()
261 .next()
262 .filter(|c| *c == '>' || *c == '#' || *c == ':');
263 if let Some(p) = prefix {
264 self.input = format!("{}{}", p, value);
265 } else {
266 self.input = value;
267 }
268 } else {
269 self.input = value;
270 }
271 self.cursor_pos = self.input.len();
272 self.clear_selection();
273 }
274 }
275 }
276 ctx.defer(DeferredAction::UpdatePromptSuggestions);
277 InputResult::Consumed
278 }
279
280 _ => InputResult::Consumed, }
282 }
283
284 fn is_modal(&self) -> bool {
285 true
286 }
287}
288
289impl Prompt {
290 fn handle_ctrl_key(&mut self, c: char, ctx: &mut InputContext) -> InputResult {
291 match c {
292 'a' => {
293 self.selection_anchor = Some(0);
295 self.cursor_pos = self.input.len();
296 InputResult::Consumed
297 }
298 'c' => {
299 ctx.defer(DeferredAction::ExecuteAction(
301 crate::input::keybindings::Action::PromptCopy,
302 ));
303 InputResult::Consumed
304 }
305 'x' => {
306 ctx.defer(DeferredAction::ExecuteAction(
308 crate::input::keybindings::Action::PromptCut,
309 ));
310 InputResult::Consumed
311 }
312 'v' => {
313 ctx.defer(DeferredAction::ExecuteAction(
315 crate::input::keybindings::Action::PromptPaste,
316 ));
317 InputResult::Consumed
318 }
319 'k' => {
320 self.delete_to_end();
322 ctx.defer(DeferredAction::UpdatePromptSuggestions);
323 InputResult::Consumed
324 }
325 _ => InputResult::Ignored,
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::view::prompt::PromptType;
335
336 fn key(code: KeyCode) -> KeyEvent {
337 KeyEvent::new(code, KeyModifiers::NONE)
338 }
339
340 fn key_with_ctrl(c: char) -> KeyEvent {
341 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
342 }
343
344 fn key_with_shift(code: KeyCode) -> KeyEvent {
345 KeyEvent::new(code, KeyModifiers::SHIFT)
346 }
347
348 #[test]
349 fn test_prompt_character_input() {
350 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
351 let mut ctx = InputContext::new();
352
353 prompt.handle_key_event(
354 &KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
355 &mut ctx,
356 );
357 prompt.handle_key_event(
358 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
359 &mut ctx,
360 );
361
362 assert_eq!(prompt.input, "hi");
363 assert_eq!(prompt.cursor_pos, 2);
364 }
365
366 #[test]
367 fn test_prompt_backspace() {
368 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
369 prompt.input = "hello".to_string();
370 prompt.cursor_pos = 5;
371 let mut ctx = InputContext::new();
372
373 prompt.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
374 assert_eq!(prompt.input, "hell");
375 assert_eq!(prompt.cursor_pos, 4);
376 }
377
378 #[test]
379 fn test_prompt_cursor_movement() {
380 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
381 prompt.input = "hello".to_string();
382 prompt.cursor_pos = 5;
383 let mut ctx = InputContext::new();
384
385 prompt.handle_key_event(&key(KeyCode::Home), &mut ctx);
387 assert_eq!(prompt.cursor_pos, 0);
388
389 prompt.handle_key_event(&key(KeyCode::End), &mut ctx);
391 assert_eq!(prompt.cursor_pos, 5);
392
393 prompt.handle_key_event(&key(KeyCode::Left), &mut ctx);
395 assert_eq!(prompt.cursor_pos, 4);
396
397 prompt.handle_key_event(&key(KeyCode::Right), &mut ctx);
399 assert_eq!(prompt.cursor_pos, 5);
400 }
401
402 #[test]
403 fn test_prompt_selection() {
404 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
405 prompt.input = "hello world".to_string();
406 prompt.cursor_pos = 0;
407 let mut ctx = InputContext::new();
408
409 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
411 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
412 assert!(prompt.has_selection());
413 assert_eq!(prompt.selected_text(), Some("he".to_string()));
414
415 prompt.handle_key_event(&key_with_ctrl('a'), &mut ctx);
417 assert_eq!(prompt.selected_text(), Some("hello world".to_string()));
418 }
419
420 #[test]
421 fn test_prompt_enter_confirms() {
422 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
423 let mut ctx = InputContext::new();
424
425 prompt.handle_key_event(&key(KeyCode::Enter), &mut ctx);
426 assert!(ctx
427 .deferred_actions
428 .iter()
429 .any(|a| matches!(a, DeferredAction::ConfirmPrompt)));
430 }
431
432 #[test]
433 fn test_prompt_escape_cancels() {
434 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
435 let mut ctx = InputContext::new();
436
437 prompt.handle_key_event(&key(KeyCode::Esc), &mut ctx);
438 assert!(ctx
439 .deferred_actions
440 .iter()
441 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
442 }
443
444 #[test]
445 fn test_prompt_is_modal() {
446 let prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
447 assert!(prompt.is_modal());
448 }
449
450 #[test]
451 fn test_prompt_ctrl_p_returns_ignored() {
452 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
453 let mut ctx = InputContext::new();
454
455 let result = prompt.handle_key_event(&key_with_ctrl('p'), &mut ctx);
457 assert_eq!(result, InputResult::Ignored, "Ctrl+P should return Ignored");
458 }
459
460 #[test]
461 fn test_prompt_ctrl_p_dispatch_returns_ignored() {
462 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
463 let mut ctx = InputContext::new();
464
465 let result = prompt.dispatch_input(&key_with_ctrl('p'), &mut ctx);
467 assert_eq!(
468 result,
469 InputResult::Ignored,
470 "dispatch_input should return Ignored for Ctrl+P"
471 );
472 }
473}