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 if !matches!(
143 self.prompt_type,
144 crate::view::prompt::PromptType::Plugin { .. }
145 | crate::view::prompt::PromptType::QuickOpen
146 ) {
147 if let Some(suggestion) = self.suggestions.get(new_selected) {
148 self.input = suggestion.get_value().to_string();
149 self.cursor_pos = self.input.len();
150 }
151 }
152 if matches!(
154 self.prompt_type,
155 crate::view::prompt::PromptType::SelectTheme { .. }
156 ) {
157 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
158 }
159 if matches!(
161 self.prompt_type,
162 crate::view::prompt::PromptType::Plugin { .. }
163 ) {
164 ctx.defer(DeferredAction::PromptSelectionChanged {
165 selected_index: new_selected,
166 });
167 }
168 }
169 } else {
170 ctx.defer(DeferredAction::PromptHistoryPrev);
172 }
173 InputResult::Consumed
174 }
175 KeyCode::Down => {
176 if !self.suggestions.is_empty() {
177 if let Some(selected) = self.selected_suggestion {
179 let new_selected = (selected + 1).min(self.suggestions.len() - 1);
180 self.selected_suggestion = Some(new_selected);
181 if !matches!(
183 self.prompt_type,
184 crate::view::prompt::PromptType::Plugin { .. }
185 | crate::view::prompt::PromptType::QuickOpen
186 ) {
187 if let Some(suggestion) = self.suggestions.get(new_selected) {
188 self.input = suggestion.get_value().to_string();
189 self.cursor_pos = self.input.len();
190 }
191 }
192 if matches!(
194 self.prompt_type,
195 crate::view::prompt::PromptType::SelectTheme { .. }
196 ) {
197 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
198 }
199 if matches!(
201 self.prompt_type,
202 crate::view::prompt::PromptType::Plugin { .. }
203 ) {
204 ctx.defer(DeferredAction::PromptSelectionChanged {
205 selected_index: new_selected,
206 });
207 }
208 }
209 } else {
210 ctx.defer(DeferredAction::PromptHistoryNext);
212 }
213 InputResult::Consumed
214 }
215 KeyCode::PageUp => {
216 if let Some(selected) = self.selected_suggestion {
217 self.selected_suggestion = Some(selected.saturating_sub(10));
218 }
219 InputResult::Consumed
220 }
221 KeyCode::PageDown => {
222 if let Some(selected) = self.selected_suggestion {
223 let len = self.suggestions.len();
224 let new_pos = selected + 10;
225 self.selected_suggestion = Some(new_pos.min(len.saturating_sub(1)));
226 }
227 InputResult::Consumed
228 }
229
230 KeyCode::Tab => {
232 if let Some(selected) = self.selected_suggestion {
233 if let Some(suggestion) = self.suggestions.get(selected) {
234 if !suggestion.disabled {
235 let value = suggestion.get_value().to_string();
236 if matches!(
238 self.prompt_type,
239 crate::view::prompt::PromptType::QuickOpen
240 ) {
241 let prefix = self
242 .input
243 .chars()
244 .next()
245 .filter(|c| *c == '>' || *c == '#' || *c == ':');
246 if let Some(p) = prefix {
247 self.input = format!("{}{}", p, value);
248 } else {
249 self.input = value;
250 }
251 } else {
252 self.input = value;
253 }
254 self.cursor_pos = self.input.len();
255 self.clear_selection();
256 }
257 }
258 }
259 ctx.defer(DeferredAction::UpdatePromptSuggestions);
260 InputResult::Consumed
261 }
262
263 _ => InputResult::Consumed, }
265 }
266
267 fn is_modal(&self) -> bool {
268 true
269 }
270}
271
272impl Prompt {
273 fn handle_ctrl_key(&mut self, c: char, ctx: &mut InputContext) -> InputResult {
274 match c {
275 'a' => {
276 self.selection_anchor = Some(0);
278 self.cursor_pos = self.input.len();
279 InputResult::Consumed
280 }
281 'c' => {
282 ctx.defer(DeferredAction::ExecuteAction(
284 crate::input::keybindings::Action::PromptCopy,
285 ));
286 InputResult::Consumed
287 }
288 'x' => {
289 ctx.defer(DeferredAction::ExecuteAction(
291 crate::input::keybindings::Action::PromptCut,
292 ));
293 InputResult::Consumed
294 }
295 'v' => {
296 ctx.defer(DeferredAction::ExecuteAction(
298 crate::input::keybindings::Action::PromptPaste,
299 ));
300 InputResult::Consumed
301 }
302 'k' => {
303 self.delete_to_end();
305 ctx.defer(DeferredAction::UpdatePromptSuggestions);
306 InputResult::Consumed
307 }
308 _ => InputResult::Ignored,
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::view::prompt::PromptType;
318
319 fn key(code: KeyCode) -> KeyEvent {
320 KeyEvent::new(code, KeyModifiers::NONE)
321 }
322
323 fn key_with_ctrl(c: char) -> KeyEvent {
324 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
325 }
326
327 fn key_with_shift(code: KeyCode) -> KeyEvent {
328 KeyEvent::new(code, KeyModifiers::SHIFT)
329 }
330
331 #[test]
332 fn test_prompt_character_input() {
333 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
334 let mut ctx = InputContext::new();
335
336 prompt.handle_key_event(
337 &KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
338 &mut ctx,
339 );
340 prompt.handle_key_event(
341 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
342 &mut ctx,
343 );
344
345 assert_eq!(prompt.input, "hi");
346 assert_eq!(prompt.cursor_pos, 2);
347 }
348
349 #[test]
350 fn test_prompt_backspace() {
351 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
352 prompt.input = "hello".to_string();
353 prompt.cursor_pos = 5;
354 let mut ctx = InputContext::new();
355
356 prompt.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
357 assert_eq!(prompt.input, "hell");
358 assert_eq!(prompt.cursor_pos, 4);
359 }
360
361 #[test]
362 fn test_prompt_cursor_movement() {
363 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
364 prompt.input = "hello".to_string();
365 prompt.cursor_pos = 5;
366 let mut ctx = InputContext::new();
367
368 prompt.handle_key_event(&key(KeyCode::Home), &mut ctx);
370 assert_eq!(prompt.cursor_pos, 0);
371
372 prompt.handle_key_event(&key(KeyCode::End), &mut ctx);
374 assert_eq!(prompt.cursor_pos, 5);
375
376 prompt.handle_key_event(&key(KeyCode::Left), &mut ctx);
378 assert_eq!(prompt.cursor_pos, 4);
379
380 prompt.handle_key_event(&key(KeyCode::Right), &mut ctx);
382 assert_eq!(prompt.cursor_pos, 5);
383 }
384
385 #[test]
386 fn test_prompt_selection() {
387 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
388 prompt.input = "hello world".to_string();
389 prompt.cursor_pos = 0;
390 let mut ctx = InputContext::new();
391
392 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
394 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
395 assert!(prompt.has_selection());
396 assert_eq!(prompt.selected_text(), Some("he".to_string()));
397
398 prompt.handle_key_event(&key_with_ctrl('a'), &mut ctx);
400 assert_eq!(prompt.selected_text(), Some("hello world".to_string()));
401 }
402
403 #[test]
404 fn test_prompt_enter_confirms() {
405 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
406 let mut ctx = InputContext::new();
407
408 prompt.handle_key_event(&key(KeyCode::Enter), &mut ctx);
409 assert!(ctx
410 .deferred_actions
411 .iter()
412 .any(|a| matches!(a, DeferredAction::ConfirmPrompt)));
413 }
414
415 #[test]
416 fn test_prompt_escape_cancels() {
417 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
418 let mut ctx = InputContext::new();
419
420 prompt.handle_key_event(&key(KeyCode::Esc), &mut ctx);
421 assert!(ctx
422 .deferred_actions
423 .iter()
424 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
425 }
426
427 #[test]
428 fn test_prompt_is_modal() {
429 let prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
430 assert!(prompt.is_modal());
431 }
432
433 #[test]
434 fn test_prompt_ctrl_p_returns_ignored() {
435 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
436 let mut ctx = InputContext::new();
437
438 let result = prompt.handle_key_event(&key_with_ctrl('p'), &mut ctx);
440 assert_eq!(result, InputResult::Ignored, "Ctrl+P should return Ignored");
441 }
442
443 #[test]
444 fn test_prompt_ctrl_p_dispatch_returns_ignored() {
445 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
446 let mut ctx = InputContext::new();
447
448 let result = prompt.dispatch_input(&key_with_ctrl('p'), &mut ctx);
450 assert_eq!(
451 result,
452 InputResult::Ignored,
453 "dispatch_input should return Ignored for Ctrl+P"
454 );
455 }
456}