1use 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#[derive(Debug, Clone, PartialEq)]
15pub enum InputResult {
16 Submitted(String),
18 Cancelled,
20 None,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum SelectResult {
27 Selected(usize),
29 Cancelled,
31 None,
33}
34
35#[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 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 pub fn handle_key(&mut self, key: KeyEvent) -> InputResult {
59 match key.code {
60 KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE => {
62 self.insert_char(c);
63 InputResult::None
64 }
65
66 KeyCode::Backspace => {
68 self.delete_char_before_cursor();
69 InputResult::None
70 }
71
72 KeyCode::Delete => {
74 self.delete_char_at_cursor();
75 InputResult::None
76 }
77
78 KeyCode::Left => {
80 self.move_cursor_left();
81 InputResult::None
82 }
83
84 KeyCode::Right => {
86 self.move_cursor_right();
87 InputResult::None
88 }
89
90 KeyCode::Home => {
92 self.cursor_pos = 0;
93 InputResult::None
94 }
95
96 KeyCode::End => {
98 self.cursor_pos = self.text.len();
99 InputResult::None
100 }
101
102 KeyCode::Enter => {
104 if self.validate() {
105 InputResult::Submitted(self.text.clone())
106 } else {
107 InputResult::None
108 }
109 }
110
111 KeyCode::Esc => InputResult::Cancelled,
113
114 _ => InputResult::None,
115 }
116 }
117
118 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 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 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 fn move_cursor_left(&mut self) {
146 if self.cursor_pos > 0 {
147 self.cursor_pos -= 1;
148 }
149 }
150
151 fn move_cursor_right(&mut self) {
153 if self.cursor_pos < self.text.len() {
154 self.cursor_pos += 1;
155 }
156 }
157
158 fn validate(&self) -> bool {
160 self.error.is_none()
161 }
162
163 pub fn set_error(&mut self, error: String) {
165 self.error = Some(error);
166 }
167
168 fn clear_error(&mut self) {
170 self.error = None;
171 }
172
173 pub fn text(&self) -> &str {
175 &self.text
176 }
177
178 pub fn cursor_pos(&self) -> usize {
180 self.cursor_pos
181 }
182
183 pub fn render(&self, area: Rect, buf: &mut Buffer) {
185 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 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 let paragraph = Paragraph::new(display_text).wrap(Wrap { trim: false });
212
213 paragraph.render(area, buf);
215
216 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#[derive(Debug, Clone)]
243pub struct SelectPrompt {
244 options: Vec<String>,
245 selected: usize,
246 title: String,
247}
248
249impl SelectPrompt {
250 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 pub fn handle_key(&mut self, key: KeyEvent) -> SelectResult {
263 match key.code {
264 KeyCode::Up | KeyCode::Char('k') => {
266 if self.selected > 0 {
267 self.selected -= 1;
268 }
269 SelectResult::None
270 }
271
272 KeyCode::Down | KeyCode::Char('j') => {
274 if self.selected + 1 < self.options.len() {
275 self.selected += 1;
276 }
277 SelectResult::None
278 }
279
280 KeyCode::PageUp => {
282 self.selected = self.selected.saturating_sub(5);
283 SelectResult::None
284 }
285
286 KeyCode::PageDown => {
288 self.selected = (self.selected + 5).min(self.options.len() - 1);
289 SelectResult::None
290 }
291
292 KeyCode::Home => {
294 self.selected = 0;
295 SelectResult::None
296 }
297
298 KeyCode::End => {
300 self.selected = self.options.len().saturating_sub(1);
301 SelectResult::None
302 }
303
304 KeyCode::Enter => SelectResult::Selected(self.selected),
306
307 KeyCode::Esc => SelectResult::Cancelled,
309
310 _ => SelectResult::None,
311 }
312 }
313
314 pub fn selected(&self) -> usize {
316 self.selected
317 }
318
319 pub fn selected_text(&self) -> Option<&str> {
321 self.options.get(self.selected).map(|s| s.as_str())
322 }
323
324 pub fn options(&self) -> &[String] {
326 &self.options
327 }
328
329 pub fn title(&self) -> &str {
331 &self.title
332 }
333
334 pub fn render(&self, area: Rect, buf: &mut Buffer) {
336 let block = Block::bordered().title(self.title.as_str());
338
339 let content_area = block.inner(area);
341
342 block.render(area, buf);
344
345 for (i, option) in self.options.iter().enumerate() {
347 if i >= content_area.height as usize {
348 break;
349 }
350
351 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 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 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 #[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 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 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 prompt.handle_key(create_key_event(KeyCode::Backspace));
449 assert_eq!(prompt.text(), "H");
450 assert_eq!(prompt.cursor_pos(), 1);
451
452 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 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 prompt.text = String::from("Hello");
489 prompt.cursor_pos = 5;
490
491 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 prompt.handle_key(create_key_event(KeyCode::Right));
500 assert_eq!(prompt.cursor_pos(), 4);
501
502 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 prompt.handle_key(create_key_event(KeyCode::Home));
517 assert_eq!(prompt.cursor_pos(), 0);
518
519 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 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 let result = prompt.handle_key(create_key_event(KeyCode::Esc));
543 assert_eq!(result, InputResult::Cancelled);
544 assert_eq!(prompt.text(), "Test"); }
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 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 prompt.handle_key(create_key_event(KeyCode::Char('H')));
566 assert!(prompt.error.is_none());
567 }
568
569 #[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 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 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 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 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 prompt.handle_key(create_key_event(KeyCode::Char('j')));
645 assert_eq!(prompt.selected(), 1);
646
647 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 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 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 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 prompt.handle_key(create_key_event(KeyCode::PageUp));
699 assert_eq!(prompt.selected(), 5);
700
701 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 prompt.handle_key(create_key_event(KeyCode::Home));
722 assert_eq!(prompt.selected(), 0);
723
724 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 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 let result = prompt.handle_key(create_key_event(KeyCode::F(1)));
755 assert_eq!(result, SelectResult::None);
756 }
757}