Skip to main content

limit_tui/components/
prompt.rs

1// Interactive prompt components for terminal UI
2
3use tracing::debug;
4
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use ratatui::{
7    buffer::Buffer,
8    layout::Rect,
9    style::{Color, Modifier, Style},
10    text::{Line, Span, Text},
11    widgets::{Block, Paragraph, Widget, Wrap},
12};
13/// Result from InputPrompt interaction
14#[derive(Debug, Clone, PartialEq)]
15pub enum InputResult {
16    /// User submitted the input
17    Submitted(String),
18    /// User cancelled the input
19    Cancelled,
20    /// No action taken
21    None,
22}
23
24/// Result from SelectPrompt interaction
25#[derive(Debug, Clone, PartialEq)]
26pub enum SelectResult {
27    /// User selected an option (index)
28    Selected(usize),
29    /// User cancelled the selection
30    Cancelled,
31    /// No action taken
32    None,
33}
34
35/// Text input prompt with cursor, placeholder, and validation
36#[derive(Debug, Clone)]
37pub struct InputPrompt {
38    text: String,
39    cursor_pos: usize,
40    placeholder: String,
41    error: Option<String>,
42}
43
44impl InputPrompt {
45    /// Create a new input prompt with placeholder text
46    pub fn new(placeholder: &str) -> Self {
47        debug!(component = %"InputPrompt", "Component created");
48        Self {
49            text: String::new(),
50            cursor_pos: 0,
51            placeholder: placeholder.to_string(),
52            error: None,
53        }
54    }
55
56    /// Handle keyboard input
57    /// Handle keyboard input
58    pub fn handle_key(&mut self, key: KeyEvent) -> InputResult {
59        match key.code {
60            // Character input
61            KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE => {
62                self.insert_char(c);
63                InputResult::None
64            }
65
66            // Backspace - delete character before cursor
67            KeyCode::Backspace => {
68                self.delete_char_before_cursor();
69                InputResult::None
70            }
71
72            // Delete - delete character at cursor
73            KeyCode::Delete => {
74                self.delete_char_at_cursor();
75                InputResult::None
76            }
77
78            // Left arrow - move cursor left
79            KeyCode::Left => {
80                self.move_cursor_left();
81                InputResult::None
82            }
83
84            // Right arrow - move cursor right
85            KeyCode::Right => {
86                self.move_cursor_right();
87                InputResult::None
88            }
89
90            // Home - move cursor to start
91            KeyCode::Home => {
92                self.cursor_pos = 0;
93                InputResult::None
94            }
95
96            // End - move cursor to end
97            KeyCode::End => {
98                self.cursor_pos = self.text.len();
99                InputResult::None
100            }
101
102            // Enter - submit input
103            KeyCode::Enter => {
104                if self.validate() {
105                    InputResult::Submitted(self.text.clone())
106                } else {
107                    InputResult::None
108                }
109            }
110
111            // Escape - cancel
112            KeyCode::Esc => InputResult::Cancelled,
113
114            _ => InputResult::None,
115        }
116    }
117
118    /// Insert a character at cursor position
119    fn insert_char(&mut self, c: char) {
120        if self.cursor_pos <= self.text.len() {
121            self.text.insert(self.cursor_pos, c);
122            self.cursor_pos += 1;
123            self.clear_error();
124        }
125    }
126
127    /// Delete character before cursor
128    fn delete_char_before_cursor(&mut self) {
129        if self.cursor_pos > 0 {
130            self.cursor_pos -= 1;
131            self.text.remove(self.cursor_pos);
132            self.clear_error();
133        }
134    }
135
136    /// Delete character at cursor position
137    fn delete_char_at_cursor(&mut self) {
138        if self.cursor_pos < self.text.len() {
139            self.text.remove(self.cursor_pos);
140            self.clear_error();
141        }
142    }
143
144    /// Move cursor left
145    fn move_cursor_left(&mut self) {
146        if self.cursor_pos > 0 {
147            self.cursor_pos -= 1;
148        }
149    }
150
151    /// Move cursor right
152    fn move_cursor_right(&mut self) {
153        if self.cursor_pos < self.text.len() {
154            self.cursor_pos += 1;
155        }
156    }
157
158    /// Validate input - override for custom validation
159    fn validate(&self) -> bool {
160        self.error.is_none()
161    }
162
163    /// Set validation error message
164    pub fn set_error(&mut self, error: String) {
165        self.error = Some(error);
166    }
167
168    /// Clear error
169    fn clear_error(&mut self) {
170        self.error = None;
171    }
172
173    /// Get current text
174    pub fn text(&self) -> &str {
175        &self.text
176    }
177
178    /// Get current cursor position
179    pub fn cursor_pos(&self) -> usize {
180        self.cursor_pos
181    }
182
183    /// Render the input prompt
184    pub fn render(&self, area: Rect, buf: &mut Buffer) {
185        // Determine display text (placeholder or actual text)
186        let display_text = if self.text.is_empty() {
187            Text::from(vec![Line::from(vec![Span::styled(
188                &self.placeholder,
189                Style::default().fg(Color::DarkGray),
190            )])])
191        } else {
192            // Split text into before cursor, at cursor, after cursor
193            let before_cursor = &self.text[..self.cursor_pos];
194            let after_cursor = &self.text[self.cursor_pos..];
195
196            Text::from(vec![Line::from(vec![
197                Span::raw(before_cursor),
198                Span::styled(
199                    if after_cursor.chars().next().is_some() {
200                        after_cursor.chars().next().unwrap().to_string()
201                    } else {
202                        " ".to_string()
203                    },
204                    Style::default().add_modifier(Modifier::REVERSED),
205                ),
206                Span::raw(&after_cursor[after_cursor.chars().next().map_or(0, |c| c.len_utf8())..]),
207            ])])
208        };
209
210        // Create paragraph with text and enable wrapping
211        let paragraph = Paragraph::new(display_text).wrap(Wrap { trim: false });
212
213        // Render the paragraph
214        paragraph.render(area, buf);
215
216        // Render error message below input if present
217        if let Some(ref error_msg) = self.error {
218            let error_area = Rect {
219                x: area.x,
220                y: area.y.saturating_add(1),
221                width: area.width,
222                height: 1,
223            };
224            let error_text = Paragraph::new(Text::from(vec![Line::from(vec![Span::styled(
225                error_msg,
226                Style::default().fg(Color::Red),
227            )])]))
228            .wrap(Wrap { trim: false });
229            error_text.render(error_area, buf);
230        }
231    }
232}
233
234impl Default for InputPrompt {
235    fn default() -> Self {
236        Self::new("")
237    }
238}
239
240/// Selection prompt for choosing from a list of options
241/// Selection prompt for choosing from a list of options
242#[derive(Debug, Clone)]
243pub struct SelectPrompt {
244    options: Vec<String>,
245    selected: usize,
246    title: String,
247}
248
249impl SelectPrompt {
250    /// Create a new select prompt with title and options
251    pub fn new(title: &str, options: Vec<String>) -> Self {
252        debug!(component = %"SelectPrompt", "Component created");
253        Self {
254            options,
255            selected: 0,
256            title: title.to_string(),
257        }
258    }
259
260    /// Handle keyboard input
261    /// Handle keyboard input
262    pub fn handle_key(&mut self, key: KeyEvent) -> SelectResult {
263        match key.code {
264            // Up arrow - move selection up
265            KeyCode::Up | KeyCode::Char('k') => {
266                if self.selected > 0 {
267                    self.selected -= 1;
268                }
269                SelectResult::None
270            }
271
272            // Down arrow - move selection down
273            KeyCode::Down | KeyCode::Char('j') => {
274                if self.selected + 1 < self.options.len() {
275                    self.selected += 1;
276                }
277                SelectResult::None
278            }
279
280            // Page Up - move up 5 items
281            KeyCode::PageUp => {
282                self.selected = self.selected.saturating_sub(5);
283                SelectResult::None
284            }
285
286            // Page Down - move down 5 items
287            KeyCode::PageDown => {
288                self.selected = (self.selected + 5).min(self.options.len() - 1);
289                SelectResult::None
290            }
291
292            // Home - select first item
293            KeyCode::Home => {
294                self.selected = 0;
295                SelectResult::None
296            }
297
298            // End - select last item
299            KeyCode::End => {
300                self.selected = self.options.len().saturating_sub(1);
301                SelectResult::None
302            }
303
304            // Enter - confirm selection
305            KeyCode::Enter => SelectResult::Selected(self.selected),
306
307            // Escape - cancel
308            KeyCode::Esc => SelectResult::Cancelled,
309
310            _ => SelectResult::None,
311        }
312    }
313
314    /// Get currently selected option
315    pub fn selected(&self) -> usize {
316        self.selected
317    }
318
319    /// Get selected option text
320    pub fn selected_text(&self) -> Option<&str> {
321        self.options.get(self.selected).map(|s| s.as_str())
322    }
323
324    /// Get all options
325    pub fn options(&self) -> &[String] {
326        &self.options
327    }
328
329    /// Get title
330    pub fn title(&self) -> &str {
331        &self.title
332    }
333
334    /// Render the select prompt
335    pub fn render(&self, area: Rect, buf: &mut Buffer) {
336        // Create block with title
337        let block = Block::bordered().title(self.title.as_str());
338
339        // Calculate content area (inside borders)
340        let content_area = block.inner(area);
341
342        // Render the block border
343        block.render(area, buf);
344
345        // Render each option
346        for (i, option) in self.options.iter().enumerate() {
347            if i >= content_area.height as usize {
348                break;
349            }
350
351            // Determine style based on selection
352            let is_selected = i == self.selected;
353            let style = if is_selected {
354                Style::default()
355                    .fg(Color::Cyan)
356                    .add_modifier(Modifier::BOLD)
357            } else {
358                Style::default()
359            };
360
361            // Build line with arrow indicator for selected item
362            let line = Line::from(vec![
363                Span::styled(
364                    if is_selected { ">" } else { " " },
365                    Style::default().fg(Color::Cyan),
366                ),
367                Span::raw(" "),
368                Span::styled(option, style),
369            ]);
370
371            // Render line
372            Paragraph::new(Text::from(line)).style(style).render(
373                Rect {
374                    x: content_area.x,
375                    y: content_area.y.saturating_add(i as u16),
376                    width: content_area.width,
377                    height: 1,
378                },
379                buf,
380            );
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
389
390    fn create_key_event(code: KeyCode) -> KeyEvent {
391        KeyEvent {
392            code,
393            modifiers: KeyModifiers::NONE,
394            kind: KeyEventKind::Press,
395            state: KeyEventState::NONE,
396        }
397    }
398
399    // InputPrompt tests
400
401    #[test]
402    fn test_input_prompt_new() {
403        let prompt = InputPrompt::new("Enter text:");
404        assert_eq!(prompt.text(), "");
405        assert_eq!(prompt.cursor_pos(), 0);
406        assert_eq!(prompt.placeholder, "Enter text:");
407        assert!(prompt.error.is_none());
408    }
409
410    #[test]
411    fn test_input_prompt_default() {
412        let prompt = InputPrompt::default();
413        assert_eq!(prompt.text(), "");
414        assert_eq!(prompt.cursor_pos(), 0);
415        assert_eq!(prompt.placeholder, "");
416    }
417
418    #[test]
419    fn test_input_prompt_type() {
420        let mut prompt = InputPrompt::new("Enter text:");
421
422        // Type characters
423        prompt.handle_key(create_key_event(KeyCode::Char('H')));
424        assert_eq!(prompt.text(), "H");
425        assert_eq!(prompt.cursor_pos(), 1);
426
427        prompt.handle_key(create_key_event(KeyCode::Char('i')));
428        assert_eq!(prompt.text(), "Hi");
429        assert_eq!(prompt.cursor_pos(), 2);
430
431        prompt.handle_key(create_key_event(KeyCode::Char('!')));
432        assert_eq!(prompt.text(), "Hi!");
433        assert_eq!(prompt.cursor_pos(), 3);
434    }
435
436    #[test]
437    fn test_input_prompt_backspace() {
438        let mut prompt = InputPrompt::new("Enter text:");
439
440        // Type some text
441        prompt.handle_key(create_key_event(KeyCode::Char('H')));
442        prompt.handle_key(create_key_event(KeyCode::Char('i')));
443
444        assert_eq!(prompt.text(), "Hi");
445        assert_eq!(prompt.cursor_pos(), 2);
446
447        // Backspace
448        prompt.handle_key(create_key_event(KeyCode::Backspace));
449        assert_eq!(prompt.text(), "H");
450        assert_eq!(prompt.cursor_pos(), 1);
451
452        // Another backspace
453        prompt.handle_key(create_key_event(KeyCode::Backspace));
454        assert_eq!(prompt.text(), "");
455        assert_eq!(prompt.cursor_pos(), 0);
456    }
457
458    #[test]
459    fn test_input_prompt_backspace_in_middle() {
460        let mut prompt = InputPrompt::new("Enter text:");
461
462        prompt.text = String::from("Hello");
463        prompt.cursor_pos = 3;
464
465        prompt.handle_key(create_key_event(KeyCode::Backspace));
466        assert_eq!(prompt.text(), "Helo");
467        assert_eq!(prompt.cursor_pos(), 2);
468    }
469
470    #[test]
471    fn test_input_prompt_delete() {
472        let mut prompt = InputPrompt::new("Enter text:");
473
474        prompt.text = String::from("Hello");
475        prompt.cursor_pos = 2;
476
477        // Delete character at cursor
478        prompt.handle_key(create_key_event(KeyCode::Delete));
479        assert_eq!(prompt.text(), "Helo");
480        assert_eq!(prompt.cursor_pos(), 2);
481    }
482
483    #[test]
484    fn test_input_prompt_cursor_move() {
485        let mut prompt = InputPrompt::new("Enter text:");
486
487        // Type text
488        prompt.text = String::from("Hello");
489        prompt.cursor_pos = 5;
490
491        // Move left
492        prompt.handle_key(create_key_event(KeyCode::Left));
493        assert_eq!(prompt.cursor_pos(), 4);
494
495        prompt.handle_key(create_key_event(KeyCode::Left));
496        assert_eq!(prompt.cursor_pos(), 3);
497
498        // Move right
499        prompt.handle_key(create_key_event(KeyCode::Right));
500        assert_eq!(prompt.cursor_pos(), 4);
501
502        // Can't move past end
503        prompt.handle_key(create_key_event(KeyCode::Right));
504        prompt.handle_key(create_key_event(KeyCode::Right));
505        assert_eq!(prompt.cursor_pos(), 5);
506    }
507
508    #[test]
509    fn test_input_prompt_home_end() {
510        let mut prompt = InputPrompt::new("Enter text:");
511
512        prompt.text = String::from("Hello");
513        prompt.cursor_pos = 2;
514
515        // Home
516        prompt.handle_key(create_key_event(KeyCode::Home));
517        assert_eq!(prompt.cursor_pos(), 0);
518
519        // End
520        prompt.handle_key(create_key_event(KeyCode::End));
521        assert_eq!(prompt.cursor_pos(), 5);
522    }
523
524    #[test]
525    fn test_input_prompt_submit() {
526        let mut prompt = InputPrompt::new("Enter text:");
527
528        prompt.text = String::from("Test");
529
530        // Submit with Enter
531        let result = prompt.handle_key(create_key_event(KeyCode::Enter));
532        assert_eq!(result, InputResult::Submitted("Test".to_string()));
533    }
534
535    #[test]
536    fn test_input_prompt_cancel() {
537        let mut prompt = InputPrompt::new("Enter text:");
538
539        prompt.text = String::from("Test");
540
541        // Cancel with Escape
542        let result = prompt.handle_key(create_key_event(KeyCode::Esc));
543        assert_eq!(result, InputResult::Cancelled);
544        assert_eq!(prompt.text(), "Test"); // Text should remain
545    }
546
547    #[test]
548    fn test_input_prompt_set_error() {
549        let mut prompt = InputPrompt::new("Enter text:");
550
551        prompt.set_error("Invalid input".to_string());
552        assert_eq!(prompt.error, Some("Invalid input".to_string()));
553
554        // Validation should fail with error
555        assert!(!prompt.validate());
556    }
557
558    #[test]
559    fn test_input_prompt_clear_error() {
560        let mut prompt = InputPrompt::new("Enter text:");
561
562        prompt.set_error("Invalid input".to_string());
563
564        // Typing should clear error
565        prompt.handle_key(create_key_event(KeyCode::Char('H')));
566        assert!(prompt.error.is_none());
567    }
568
569    // SelectPrompt tests
570
571    #[test]
572    fn test_select_prompt_new() {
573        let options = vec![
574            "Option 1".to_string(),
575            "Option 2".to_string(),
576            "Option 3".to_string(),
577        ];
578
579        let prompt = SelectPrompt::new("Choose:", options);
580        assert_eq!(prompt.title(), "Choose:");
581        assert_eq!(prompt.selected(), 0);
582        assert_eq!(prompt.options().len(), 3);
583        assert_eq!(prompt.selected_text(), Some("Option 1"));
584    }
585
586    #[test]
587    fn test_select_prompt_navigate_down() {
588        let options = vec![
589            "Option 1".to_string(),
590            "Option 2".to_string(),
591            "Option 3".to_string(),
592        ];
593
594        let mut prompt = SelectPrompt::new("Choose:", options);
595
596        // Navigate down
597        prompt.handle_key(create_key_event(KeyCode::Down));
598        assert_eq!(prompt.selected(), 1);
599        assert_eq!(prompt.selected_text(), Some("Option 2"));
600
601        prompt.handle_key(create_key_event(KeyCode::Down));
602        assert_eq!(prompt.selected(), 2);
603        assert_eq!(prompt.selected_text(), Some("Option 3"));
604
605        // Can't go past last option
606        prompt.handle_key(create_key_event(KeyCode::Down));
607        assert_eq!(prompt.selected(), 2);
608    }
609
610    #[test]
611    fn test_select_prompt_navigate_up() {
612        let options = vec![
613            "Option 1".to_string(),
614            "Option 2".to_string(),
615            "Option 3".to_string(),
616        ];
617
618        let mut prompt = SelectPrompt::new("Choose:", options);
619        prompt.selected = 2;
620
621        // Navigate up
622        prompt.handle_key(create_key_event(KeyCode::Up));
623        assert_eq!(prompt.selected(), 1);
624
625        prompt.handle_key(create_key_event(KeyCode::Up));
626        assert_eq!(prompt.selected(), 0);
627
628        // Can't go before first option
629        prompt.handle_key(create_key_event(KeyCode::Up));
630        assert_eq!(prompt.selected(), 0);
631    }
632
633    #[test]
634    fn test_select_prompt_vim_keys() {
635        let options = vec![
636            "Option 1".to_string(),
637            "Option 2".to_string(),
638            "Option 3".to_string(),
639        ];
640
641        let mut prompt = SelectPrompt::new("Choose:", options);
642
643        // 'j' for down
644        prompt.handle_key(create_key_event(KeyCode::Char('j')));
645        assert_eq!(prompt.selected(), 1);
646
647        // 'k' for up
648        prompt.handle_key(create_key_event(KeyCode::Char('k')));
649        assert_eq!(prompt.selected(), 0);
650    }
651
652    #[test]
653    fn test_select_prompt_select() {
654        let options = vec![
655            "Option 1".to_string(),
656            "Option 2".to_string(),
657            "Option 3".to_string(),
658        ];
659
660        let mut prompt = SelectPrompt::new("Choose:", options);
661        prompt.selected = 1;
662
663        // Select with Enter
664        let result = prompt.handle_key(create_key_event(KeyCode::Enter));
665        assert_eq!(result, SelectResult::Selected(1));
666    }
667
668    #[test]
669    fn test_select_prompt_cancel() {
670        let options = vec![
671            "Option 1".to_string(),
672            "Option 2".to_string(),
673            "Option 3".to_string(),
674        ];
675
676        let mut prompt = SelectPrompt::new("Choose:", options);
677
678        // Cancel with Escape
679        let result = prompt.handle_key(create_key_event(KeyCode::Esc));
680        assert_eq!(result, SelectResult::Cancelled);
681    }
682
683    #[test]
684    fn test_select_prompt_page_navigation() {
685        let options = (0..20).map(|i| format!("Option {}", i)).collect();
686
687        let mut prompt = SelectPrompt::new("Choose:", options);
688        assert_eq!(prompt.selected(), 0);
689
690        // Page Down
691        prompt.handle_key(create_key_event(KeyCode::PageDown));
692        assert_eq!(prompt.selected(), 5);
693
694        prompt.handle_key(create_key_event(KeyCode::PageDown));
695        assert_eq!(prompt.selected(), 10);
696
697        // Page Up
698        prompt.handle_key(create_key_event(KeyCode::PageUp));
699        assert_eq!(prompt.selected(), 5);
700
701        // Page Up at start
702        prompt.selected = 2;
703        prompt.handle_key(create_key_event(KeyCode::PageUp));
704        assert_eq!(prompt.selected(), 0);
705    }
706
707    #[test]
708    fn test_select_prompt_home_end() {
709        let options = vec![
710            "Option 1".to_string(),
711            "Option 2".to_string(),
712            "Option 3".to_string(),
713            "Option 4".to_string(),
714            "Option 5".to_string(),
715        ];
716
717        let mut prompt = SelectPrompt::new("Choose:", options);
718        prompt.selected = 3;
719
720        // Home
721        prompt.handle_key(create_key_event(KeyCode::Home));
722        assert_eq!(prompt.selected(), 0);
723
724        // End
725        prompt.handle_key(create_key_event(KeyCode::End));
726        assert_eq!(prompt.selected(), 4);
727    }
728
729    #[test]
730    fn test_select_prompt_empty_options() {
731        let options: Vec<String> = vec![];
732        let prompt = SelectPrompt::new("Choose:", options);
733
734        assert_eq!(prompt.selected(), 0);
735        assert!(prompt.selected_text().is_none());
736        assert_eq!(prompt.options().len(), 0);
737    }
738
739    #[test]
740    fn test_input_prompt_no_result() {
741        let mut prompt = InputPrompt::new("Enter text:");
742
743        // Unknown key should return None
744        let result = prompt.handle_key(create_key_event(KeyCode::F(1)));
745        assert_eq!(result, InputResult::None);
746    }
747
748    #[test]
749    fn test_select_prompt_no_result() {
750        let options = vec!["Option 1".to_string()];
751        let mut prompt = SelectPrompt::new("Choose:", options);
752
753        // Unknown key should return None
754        let result = prompt.handle_key(create_key_event(KeyCode::F(1)));
755        assert_eq!(result, SelectResult::None);
756    }
757}