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 let Some(selected) = self.selected_suggestion {
241 if let Some(suggestion) = self.suggestions.get(selected) {
242 if !suggestion.disabled {
243 let value = suggestion.get_value().to_string();
244 if matches!(
246 self.prompt_type,
247 crate::view::prompt::PromptType::QuickOpen
248 ) {
249 let prefix = self
250 .input
251 .chars()
252 .next()
253 .filter(|c| *c == '>' || *c == '#' || *c == ':');
254 if let Some(p) = prefix {
255 self.input = format!("{}{}", p, value);
256 } else {
257 self.input = value;
258 }
259 } else {
260 self.input = value;
261 }
262 self.cursor_pos = self.input.len();
263 self.clear_selection();
264 }
265 }
266 }
267 ctx.defer(DeferredAction::UpdatePromptSuggestions);
268 InputResult::Consumed
269 }
270
271 _ => InputResult::Consumed, }
273 }
274
275 fn is_modal(&self) -> bool {
276 true
277 }
278}
279
280impl Prompt {
281 fn handle_ctrl_key(&mut self, c: char, ctx: &mut InputContext) -> InputResult {
282 match c {
283 'a' => {
284 self.selection_anchor = Some(0);
286 self.cursor_pos = self.input.len();
287 InputResult::Consumed
288 }
289 'c' => {
290 ctx.defer(DeferredAction::ExecuteAction(
292 crate::input::keybindings::Action::PromptCopy,
293 ));
294 InputResult::Consumed
295 }
296 'x' => {
297 ctx.defer(DeferredAction::ExecuteAction(
299 crate::input::keybindings::Action::PromptCut,
300 ));
301 InputResult::Consumed
302 }
303 'v' => {
304 ctx.defer(DeferredAction::ExecuteAction(
306 crate::input::keybindings::Action::PromptPaste,
307 ));
308 InputResult::Consumed
309 }
310 'k' => {
311 self.delete_to_end();
313 ctx.defer(DeferredAction::UpdatePromptSuggestions);
314 InputResult::Consumed
315 }
316 _ => InputResult::Ignored,
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::view::prompt::PromptType;
326
327 fn key(code: KeyCode) -> KeyEvent {
328 KeyEvent::new(code, KeyModifiers::NONE)
329 }
330
331 fn key_with_ctrl(c: char) -> KeyEvent {
332 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
333 }
334
335 fn key_with_shift(code: KeyCode) -> KeyEvent {
336 KeyEvent::new(code, KeyModifiers::SHIFT)
337 }
338
339 #[test]
340 fn test_prompt_character_input() {
341 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
342 let mut ctx = InputContext::new();
343
344 prompt.handle_key_event(
345 &KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
346 &mut ctx,
347 );
348 prompt.handle_key_event(
349 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
350 &mut ctx,
351 );
352
353 assert_eq!(prompt.input, "hi");
354 assert_eq!(prompt.cursor_pos, 2);
355 }
356
357 #[test]
358 fn test_prompt_backspace() {
359 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
360 prompt.input = "hello".to_string();
361 prompt.cursor_pos = 5;
362 let mut ctx = InputContext::new();
363
364 prompt.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
365 assert_eq!(prompt.input, "hell");
366 assert_eq!(prompt.cursor_pos, 4);
367 }
368
369 #[test]
370 fn test_prompt_cursor_movement() {
371 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
372 prompt.input = "hello".to_string();
373 prompt.cursor_pos = 5;
374 let mut ctx = InputContext::new();
375
376 prompt.handle_key_event(&key(KeyCode::Home), &mut ctx);
378 assert_eq!(prompt.cursor_pos, 0);
379
380 prompt.handle_key_event(&key(KeyCode::End), &mut ctx);
382 assert_eq!(prompt.cursor_pos, 5);
383
384 prompt.handle_key_event(&key(KeyCode::Left), &mut ctx);
386 assert_eq!(prompt.cursor_pos, 4);
387
388 prompt.handle_key_event(&key(KeyCode::Right), &mut ctx);
390 assert_eq!(prompt.cursor_pos, 5);
391 }
392
393 #[test]
394 fn test_prompt_selection() {
395 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
396 prompt.input = "hello world".to_string();
397 prompt.cursor_pos = 0;
398 let mut ctx = InputContext::new();
399
400 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
402 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
403 assert!(prompt.has_selection());
404 assert_eq!(prompt.selected_text(), Some("he".to_string()));
405
406 prompt.handle_key_event(&key_with_ctrl('a'), &mut ctx);
408 assert_eq!(prompt.selected_text(), Some("hello world".to_string()));
409 }
410
411 #[test]
412 fn test_prompt_enter_confirms() {
413 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
414 let mut ctx = InputContext::new();
415
416 prompt.handle_key_event(&key(KeyCode::Enter), &mut ctx);
417 assert!(ctx
418 .deferred_actions
419 .iter()
420 .any(|a| matches!(a, DeferredAction::ConfirmPrompt)));
421 }
422
423 #[test]
424 fn test_prompt_escape_cancels() {
425 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
426 let mut ctx = InputContext::new();
427
428 prompt.handle_key_event(&key(KeyCode::Esc), &mut ctx);
429 assert!(ctx
430 .deferred_actions
431 .iter()
432 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
433 }
434
435 #[test]
436 fn test_prompt_is_modal() {
437 let prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
438 assert!(prompt.is_modal());
439 }
440
441 #[test]
442 fn test_prompt_ctrl_p_returns_ignored() {
443 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
444 let mut ctx = InputContext::new();
445
446 let result = prompt.handle_key_event(&key_with_ctrl('p'), &mut ctx);
448 assert_eq!(result, InputResult::Ignored, "Ctrl+P should return Ignored");
449 }
450
451 #[test]
452 fn test_prompt_ctrl_p_dispatch_returns_ignored() {
453 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
454 let mut ctx = InputContext::new();
455
456 let result = prompt.dispatch_input(&key_with_ctrl('p'), &mut ctx);
458 assert_eq!(
459 result,
460 InputResult::Ignored,
461 "dispatch_input should return Ignored for Ctrl+P"
462 );
463 }
464}