1use std::fmt;
18use std::io::{self, BufRead, Write};
19
20use crate::console::Console;
21use crate::style::Style;
22
23#[derive(Debug)]
29pub enum PromptError {
30 InvalidResponse(String),
32 IOError(io::Error),
34 Cancelled,
36}
37
38impl fmt::Display for PromptError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::InvalidResponse(msg) => write!(f, "{}", msg),
42 Self::IOError(e) => write!(f, "I/O error: {}", e),
43 Self::Cancelled => write!(f, "cancelled"),
44 }
45 }
46}
47
48impl std::error::Error for PromptError {
49 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50 match self {
51 Self::IOError(e) => Some(e),
52 _ => None,
53 }
54 }
55}
56
57impl From<io::Error> for PromptError {
58 fn from(e: io::Error) -> Self {
59 PromptError::IOError(e)
60 }
61}
62
63fn read_password() -> Result<String, PromptError> {
70 use crossterm::event::{self, Event, KeyCode, KeyEventKind};
71 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
72
73 enable_raw_mode().map_err(PromptError::IOError)?;
74
75 let mut result = String::new();
76
77 let cleanup = || {
78 let _ = disable_raw_mode();
79 };
80
81 loop {
82 match event::read() {
83 Ok(Event::Key(key))
84 if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
85 {
86 match key.code {
87 KeyCode::Enter => {
88 let _ = io::stdout().write(b"\n");
89 let _ = io::stdout().flush();
90 break;
91 }
92 KeyCode::Char(c) => {
93 result.push(c);
94 let _ = io::stdout().write(b"*");
95 let _ = io::stdout().flush();
96 }
97 KeyCode::Backspace => {
98 if result.pop().is_some() {
99 let _ = io::stdout().write(b"\x08 \x08");
100 let _ = io::stdout().flush();
101 }
102 }
103 KeyCode::Esc | KeyCode::Delete => {
104 cleanup();
105 return Err(PromptError::Cancelled);
106 }
107 _ => {}
108 }
109 }
110 Ok(Event::Key(key)) if key.code == KeyCode::Enter => {
111 let _ = io::stdout().write(b"\n");
112 let _ = io::stdout().flush();
113 break;
114 }
115 Ok(Event::Key(key)) if key.code == KeyCode::Esc => {
116 cleanup();
117 return Err(PromptError::Cancelled);
118 }
119 Ok(_) => {}
120 Err(e) => {
121 cleanup();
122 return Err(PromptError::IOError(e));
123 }
124 }
125 }
126
127 cleanup();
128 Ok(result)
129}
130
131pub struct PromptBase {
140 pub prompt: String,
142 pub console: Option<Console>,
145 pub password: bool,
147 pub choices: Option<Vec<String>>,
150 pub case_sensitive: bool,
152 pub show_default: bool,
154 pub show_choices: bool,
156}
157
158impl PromptBase {
159 pub fn new(prompt: impl Into<String>) -> Self {
161 Self {
162 prompt: prompt.into(),
163 console: None,
164 password: false,
165 choices: None,
166 case_sensitive: false,
167 show_default: true,
168 show_choices: true,
169 }
170 }
171
172 pub fn console(mut self, console: Console) -> Self {
174 self.console = Some(console);
175 self
176 }
177
178 pub fn password(mut self, yes: bool) -> Self {
180 self.password = yes;
181 self
182 }
183
184 pub fn choices(mut self, choices: Vec<String>) -> Self {
186 self.choices = Some(choices);
187 self
188 }
189
190 pub fn case_sensitive(mut self, yes: bool) -> Self {
192 self.case_sensitive = yes;
193 self
194 }
195
196 pub fn show_default(mut self, yes: bool) -> Self {
198 self.show_default = yes;
199 self
200 }
201
202 pub fn show_choices(mut self, yes: bool) -> Self {
204 self.show_choices = yes;
205 self
206 }
207
208 pub fn render_default(&self, default: &str) -> String {
217 if !self.show_default || default.is_empty() {
218 return String::new();
219 }
220 let styled = apply_style(default, "prompt.default");
221 format!(" ({})", styled)
222 }
223
224 pub fn make_prompt(&self) -> String {
229 let mut parts = Vec::new();
230
231 if self.show_choices {
233 if let Some(choices) = &self.choices {
234 let display_choices: Vec<&str> = choices.iter().map(|s| s.as_str()).collect();
235 let styled = apply_style(&display_choices.join("/"), "prompt.choices");
236 parts.push(format!("[{}]", styled));
237 }
238 }
239
240 let suffix = if parts.is_empty() {
241 String::new()
242 } else {
243 format!(" {} ", parts.join(" "))
244 };
245
246 let styled_prompt = apply_style(&self.prompt, "prompt");
247 format!("{}{}: ", styled_prompt, suffix)
248 }
249
250 pub fn check_choice(&self, value: &str) -> bool {
256 match &self.choices {
257 None => true,
258 Some(choices) => {
259 if self.case_sensitive {
260 choices.iter().any(|c| c == value)
261 } else {
262 let lower = value.to_lowercase();
263 choices.iter().any(|c| c.to_lowercase() == lower)
264 }
265 }
266 }
267 }
268
269 fn read_line(&self) -> Result<String, PromptError> {
271 if self.password {
272 read_password()
273 } else {
274 let mut input = String::new();
275 io::stdin()
276 .lock()
277 .read_line(&mut input)
278 .map_err(PromptError::IOError)?;
279 if input.is_empty() {
280 return Err(PromptError::Cancelled);
281 }
282 Ok(input
283 .trim_end_matches('\n')
284 .trim_end_matches('\r')
285 .to_string())
286 }
287 }
288
289 fn write_output(&self, text: &str) -> Result<(), PromptError> {
291 let mut out = io::stdout();
292 out.write_all(text.as_bytes())?;
293 out.flush()?;
294 Ok(())
295 }
296}
297
298pub struct Prompt {
312 base: PromptBase,
313}
314
315impl Prompt {
316 pub fn new(prompt: impl Into<String>) -> Self {
318 Self {
319 base: PromptBase::new(prompt),
320 }
321 }
322
323 pub fn console(mut self, console: Console) -> Self {
325 self.base.console = Some(console);
326 self
327 }
328
329 pub fn password(mut self, yes: bool) -> Self {
331 self.base.password = yes;
332 self
333 }
334
335 pub fn choices(mut self, choices: Vec<String>) -> Self {
337 self.base.choices = Some(choices);
338 self
339 }
340
341 pub fn case_sensitive(mut self, yes: bool) -> Self {
343 self.base.case_sensitive = yes;
344 self
345 }
346
347 pub fn show_choices(mut self, yes: bool) -> Self {
349 self.base.show_choices = yes;
350 self
351 }
352
353 pub fn show_default(mut self, yes: bool) -> Self {
355 self.base.show_default = yes;
356 self
357 }
358
359 pub fn render(&self) -> String {
365 self.base.make_prompt()
366 }
367
368 pub fn ask(&self) -> Result<String, PromptError> {
379 let prompt_str = self.base.make_prompt();
380 self.base.write_output(&prompt_str)?;
381 let value = self.base.read_line()?;
382 if !self.base.check_choice(&value) {
383 return Err(PromptError::InvalidResponse(format!(
384 "invalid choice: '{}'",
385 value
386 )));
387 }
388 Ok(value)
389 }
390
391 pub fn ask_with(prompt: impl Into<String>) -> Result<String, PromptError> {
395 Prompt::new(prompt).ask()
396 }
397}
398
399pub struct IntPrompt {
413 base: PromptBase,
414}
415
416impl IntPrompt {
417 pub fn new(prompt: impl Into<String>) -> Self {
419 Self {
420 base: PromptBase::new(prompt),
421 }
422 }
423
424 pub fn console(mut self, console: Console) -> Self {
426 self.base.console = Some(console);
427 self
428 }
429
430 pub fn password(mut self, yes: bool) -> Self {
432 self.base.password = yes;
433 self
434 }
435
436 pub fn choices(mut self, choices: Vec<String>) -> Self {
438 self.base.choices = Some(choices);
439 self
440 }
441
442 pub fn case_sensitive(mut self, yes: bool) -> Self {
444 self.base.case_sensitive = yes;
445 self
446 }
447
448 pub fn ask(&self) -> Result<i64, PromptError> {
458 loop {
459 let prompt_str = self.base.make_prompt();
460 self.base.write_output(&prompt_str)?;
461 let value = self.base.read_line()?;
462 if value.is_empty() {
463 continue;
464 }
465 if !self.base.check_choice(&value) {
466 let _ = self.base.write_output(&format!(
467 "Invalid choice: '{}'. Please try again.\n",
468 value
469 ));
470 continue;
471 }
472 match value.parse::<i64>() {
473 Ok(n) => return Ok(n),
474 Err(_) => {
475 let _ =
476 self.base.write_output("Please enter a valid integer.\n");
477 }
478 }
479 }
480 }
481
482 pub fn ask_with(prompt: impl Into<String>) -> Result<i64, PromptError> {
484 IntPrompt::new(prompt).ask()
485 }
486}
487
488pub struct FloatPrompt {
502 base: PromptBase,
503}
504
505impl FloatPrompt {
506 pub fn new(prompt: impl Into<String>) -> Self {
508 Self {
509 base: PromptBase::new(prompt),
510 }
511 }
512
513 pub fn console(mut self, console: Console) -> Self {
515 self.base.console = Some(console);
516 self
517 }
518
519 pub fn password(mut self, yes: bool) -> Self {
521 self.base.password = yes;
522 self
523 }
524
525 pub fn choices(mut self, choices: Vec<String>) -> Self {
527 self.base.choices = Some(choices);
528 self
529 }
530
531 pub fn case_sensitive(mut self, yes: bool) -> Self {
533 self.base.case_sensitive = yes;
534 self
535 }
536
537 pub fn ask(&self) -> Result<f64, PromptError> {
547 loop {
548 let prompt_str = self.base.make_prompt();
549 self.base.write_output(&prompt_str)?;
550 let value = self.base.read_line()?;
551 if value.is_empty() {
552 continue;
553 }
554 if !self.base.check_choice(&value) {
555 let _ = self.base.write_output(&format!(
556 "Invalid choice: '{}'. Please try again.\n",
557 value
558 ));
559 continue;
560 }
561 match value.parse::<f64>() {
562 Ok(n) => return Ok(n),
563 Err(_) => {
564 let _ =
565 self.base.write_output("Please enter a valid number.\n");
566 }
567 }
568 }
569 }
570
571 pub fn ask_with(prompt: impl Into<String>) -> Result<f64, PromptError> {
573 FloatPrompt::new(prompt).ask()
574 }
575}
576
577pub struct Confirm {
593 base: PromptBase,
594 pub default: bool,
596}
597
598impl Confirm {
599 pub fn new(prompt: impl Into<String>, default: bool) -> Self {
601 Self {
602 base: PromptBase::new(prompt),
603 default,
604 }
605 }
606
607 pub fn console(mut self, console: Console) -> Self {
609 self.base.console = Some(console);
610 self
611 }
612
613 fn make_confirm_prompt(&self) -> String {
617 let (yes, no) = if self.default {
618 ("Y", "n")
619 } else {
620 ("y", "N")
621 };
622 let styled_prompt = apply_style(&self.base.prompt, "prompt");
623 let styled_choices = apply_style(&format!("[{}/{}]", yes, no), "prompt.choices");
624 format!("{} {}: ", styled_prompt, styled_choices)
625 }
626
627 pub fn ask(&self) -> Result<bool, PromptError> {
637 loop {
638 let prompt_str = self.make_confirm_prompt();
639 self.base.write_output(&prompt_str)?;
640 let value = self.base.read_line()?;
641 match value.to_lowercase().as_str() {
642 "" => return Ok(self.default),
643 "y" | "yes" | "true" | "1" => return Ok(true),
644 "n" | "no" | "false" | "0" => return Ok(false),
645 _ => {
646 let _ =
647 self.base.write_output("Please answer y or n.\n");
648 }
649 }
650 }
651 }
652
653 pub fn ask_with(prompt: impl Into<String>, default: bool) -> Result<bool, PromptError> {
656 Confirm::new(prompt, default).ask()
657 }
658}
659
660pub struct Select<T> {
681 base: PromptBase,
682 choices: Vec<(String, T)>,
683}
684
685impl<T> Select<T> {
686 pub fn new(prompt: impl Into<String>) -> Self {
688 Self {
689 base: PromptBase::new(prompt),
690 choices: Vec::new(),
691 }
692 }
693
694 pub fn console(mut self, console: Console) -> Self {
696 self.base.console = Some(console);
697 self
698 }
699
700 pub fn choice(mut self, label: impl Into<String>, value: T) -> Self {
702 self.choices.push((label.into(), value));
703 self
704 }
705}
706
707impl<T: fmt::Display> Select<T> {
708 pub fn render(&self) -> String {
719 let mut output = String::new();
720 let styled_prompt = apply_style(&self.base.prompt, "prompt");
721 output.push_str(&styled_prompt);
722 output.push('\n');
723
724 for (i, (label, _)) in self.choices.iter().enumerate() {
725 output.push_str(&format!(" {}) {}\n", i + 1, label));
726 }
727
728 let styled_choices = apply_style(
729 &format!("Enter number [1-{}]", self.choices.len()),
730 "prompt.choices",
731 );
732 output.push_str(&format!("{}: ", styled_choices));
733 output
734 }
735}
736
737impl<T: fmt::Display + Clone> Select<T> {
738 pub fn ask(&self) -> Result<T, PromptError> {
748 if self.choices.is_empty() {
749 return Err(PromptError::InvalidResponse(
750 "no choices available".into(),
751 ));
752 }
753
754 let prompt_str = self.render();
755 self.base.write_output(&prompt_str)?;
756
757 loop {
758 let value = self.base.read_line()?;
759 if value.is_empty() {
760 continue;
761 }
762 match value.trim().parse::<usize>() {
763 Ok(n) if n >= 1 && n <= self.choices.len() => {
764 return Ok(self.choices[n - 1].1.clone());
765 }
766 _ => {
767 let _ = self.base.write_output(&format!(
768 "Please enter a number between 1 and {}.\n",
769 self.choices.len()
770 ));
771 }
772 }
773 }
774 }
775}
776
777fn apply_style(text: &str, style_name: &str) -> String {
785 let theme = crate::theme::default_theme();
786 if let Some(style) = theme.get(style_name) {
787 let ansi = style.to_ansi();
788 if ansi.is_empty() {
789 text.to_string()
790 } else {
791 format!("\x1b[{}m{}\x1b[0m", ansi, text)
792 }
793 } else {
794 text.to_string()
795 }
796}
797
798#[allow(dead_code)]
800fn apply_raw_style(text: &str, style: &Style) -> String {
801 let ansi = style.to_ansi();
802 if ansi.is_empty() {
803 text.to_string()
804 } else {
805 format!("\x1b[{}m{}\x1b[0m", ansi, text)
806 }
807}
808
809#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
820 fn test_make_prompt_no_choices() {
821 let pb = PromptBase::new("Enter name");
822 let result = pb.make_prompt();
823 assert!(result.contains("Enter name"));
824 assert!(result.ends_with(": "));
825 }
826
827 #[test]
828 fn test_make_prompt_with_choices() {
829 let pb = PromptBase::new("Choose").choices(vec!["a".into(), "b".into()]);
830 let result = pb.make_prompt();
831 assert!(result.contains("Choose"));
832 assert!(result.contains("["));
833 assert!(result.contains("a/b"));
834 assert!(result.contains("]"));
835 }
836
837 #[test]
838 fn test_render_default() {
839 let pb = PromptBase::new("test");
840 let rendered = pb.render_default("hello");
841 assert!(rendered.contains("hello"));
842
843 let pb_hidden = PromptBase::new("test").show_default(false);
844 let rendered_hidden = pb_hidden.render_default("hello");
845 assert_eq!(rendered_hidden, "");
846 }
847
848 #[test]
849 fn test_check_choice_no_choices() {
850 let pb = PromptBase::new("test");
851 assert!(pb.check_choice("anything"));
852 }
853
854 #[test]
855 fn test_check_choice_case_insensitive() {
856 let pb = PromptBase::new("test")
857 .choices(vec!["yes".into(), "no".into()])
858 .case_sensitive(false);
859 assert!(pb.check_choice("YES"));
860 assert!(pb.check_choice("yes"));
861 assert!(pb.check_choice("No"));
862 assert!(!pb.check_choice("maybe"));
863 }
864
865 #[test]
866 fn test_check_choice_case_sensitive() {
867 let pb = PromptBase::new("test")
868 .choices(vec!["Yes".into(), "No".into()])
869 .case_sensitive(true);
870 assert!(pb.check_choice("Yes"));
871 assert!(!pb.check_choice("yes"));
872 }
873
874 #[test]
877 fn test_prompt_error_display() {
878 let err = PromptError::InvalidResponse("bad input".into());
879 assert_eq!(format!("{}", err), "bad input");
880
881 let err = PromptError::Cancelled;
882 assert_eq!(format!("{}", err), "cancelled");
883
884 let io_err = io::Error::new(io::ErrorKind::Other, "oh no");
885 let err = PromptError::IOError(io_err);
886 let msg = format!("{}", err);
887 assert!(msg.contains("I/O error"));
888 }
889
890 #[test]
891 fn test_prompt_error_source() {
892 use std::error::Error;
893
894 let err = PromptError::InvalidResponse("bad".into());
895 assert!(err.source().is_none());
896
897 let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
898 let err = PromptError::IOError(io_err);
899 assert!(err.source().is_some());
900 }
901
902 #[test]
903 fn test_from_io_error() {
904 let io_err = io::Error::new(io::ErrorKind::Other, "oh no");
905 let err: PromptError = io_err.into();
906 match err {
907 PromptError::IOError(_) => {}
908 _ => panic!("expected IOError"),
909 }
910 }
911
912 #[test]
915 fn test_confirm_prompt_text_default_true() {
916 let c = Confirm::new("Continue?", true);
917 let prompt = c.make_confirm_prompt();
918 assert!(prompt.contains("Continue?"));
919 assert!(prompt.contains("[Y/n]"));
920 }
921
922 #[test]
923 fn test_confirm_prompt_text_default_false() {
924 let c = Confirm::new("Continue?", false);
925 let prompt = c.make_confirm_prompt();
926 assert!(prompt.contains("Continue?"));
927 assert!(prompt.contains("[y/N]"));
928 }
929
930 #[test]
933 fn test_select_render() {
934 let s: Select<&str> = Select::new("Pick")
935 .choice("Option A", "a")
936 .choice("Option B", "b");
937 let rendered = s.render();
938 assert!(rendered.contains("Pick"));
939 assert!(rendered.contains("1) Option A"));
940 assert!(rendered.contains("2) Option B"));
941 assert!(rendered.contains("Enter number [1-2]"));
942 }
943
944 #[test]
945 fn test_select_no_choices_error() {
946 let s: Select<String> = Select::new("empty");
947 let result = s.ask();
948 match result {
949 Err(PromptError::InvalidResponse(msg)) => {
950 assert!(msg.contains("no choices"));
951 }
952 _ => panic!("expected InvalidResponse for no choices"),
953 }
954 }
955
956 #[test]
959 fn test_prompt_builder() {
960 let p = Prompt::new("Enter value").password(false).show_choices(true);
961 let rendered = p.render();
962 assert!(rendered.contains("Enter value"));
963 }
964
965 #[test]
966 fn test_prompt_render_default() {
967 let pb = PromptBase::new("Name").show_default(true);
968 assert!(pb.render_default("Alice").contains("Alice"));
969 }
970
971 #[test]
974 fn test_apply_style_plain() {
975 let result = apply_style("hello", "nonexistent.style");
976 assert_eq!(result, "hello");
977 }
978
979 #[test]
980 fn test_apply_style_with_theme() {
981 let result = apply_style("hello", "prompt");
982 assert!(result.contains("hello"));
983 }
984
985 #[test]
986 fn test_apply_raw_style_empty() {
987 let s = Style::new();
988 let result = apply_raw_style("test", &s);
989 assert_eq!(result, "test");
990 }
991}