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
467 .base
468 .write_output(&format!("Invalid choice: '{}'. Please try again.\n", value));
469 continue;
470 }
471 match value.parse::<i64>() {
472 Ok(n) => return Ok(n),
473 Err(_) => {
474 let _ = self.base.write_output("Please enter a valid integer.\n");
475 }
476 }
477 }
478 }
479
480 pub fn ask_with(prompt: impl Into<String>) -> Result<i64, PromptError> {
482 IntPrompt::new(prompt).ask()
483 }
484}
485
486pub struct FloatPrompt {
500 base: PromptBase,
501}
502
503impl FloatPrompt {
504 pub fn new(prompt: impl Into<String>) -> Self {
506 Self {
507 base: PromptBase::new(prompt),
508 }
509 }
510
511 pub fn console(mut self, console: Console) -> Self {
513 self.base.console = Some(console);
514 self
515 }
516
517 pub fn password(mut self, yes: bool) -> Self {
519 self.base.password = yes;
520 self
521 }
522
523 pub fn choices(mut self, choices: Vec<String>) -> Self {
525 self.base.choices = Some(choices);
526 self
527 }
528
529 pub fn case_sensitive(mut self, yes: bool) -> Self {
531 self.base.case_sensitive = yes;
532 self
533 }
534
535 pub fn ask(&self) -> Result<f64, PromptError> {
545 loop {
546 let prompt_str = self.base.make_prompt();
547 self.base.write_output(&prompt_str)?;
548 let value = self.base.read_line()?;
549 if value.is_empty() {
550 continue;
551 }
552 if !self.base.check_choice(&value) {
553 let _ = self
554 .base
555 .write_output(&format!("Invalid choice: '{}'. Please try again.\n", value));
556 continue;
557 }
558 match value.parse::<f64>() {
559 Ok(n) => return Ok(n),
560 Err(_) => {
561 let _ = self.base.write_output("Please enter a valid number.\n");
562 }
563 }
564 }
565 }
566
567 pub fn ask_with(prompt: impl Into<String>) -> Result<f64, PromptError> {
569 FloatPrompt::new(prompt).ask()
570 }
571}
572
573pub struct Confirm {
589 base: PromptBase,
590 pub default: bool,
592}
593
594impl Confirm {
595 pub fn new(prompt: impl Into<String>, default: bool) -> Self {
597 Self {
598 base: PromptBase::new(prompt),
599 default,
600 }
601 }
602
603 pub fn console(mut self, console: Console) -> Self {
605 self.base.console = Some(console);
606 self
607 }
608
609 fn make_confirm_prompt(&self) -> String {
613 let (yes, no) = if self.default { ("Y", "n") } else { ("y", "N") };
614 let styled_prompt = apply_style(&self.base.prompt, "prompt");
615 let styled_choices = apply_style(&format!("[{}/{}]", yes, no), "prompt.choices");
616 format!("{} {}: ", styled_prompt, styled_choices)
617 }
618
619 pub fn ask(&self) -> Result<bool, PromptError> {
629 loop {
630 let prompt_str = self.make_confirm_prompt();
631 self.base.write_output(&prompt_str)?;
632 let value = self.base.read_line()?;
633 match value.to_lowercase().as_str() {
634 "" => return Ok(self.default),
635 "y" | "yes" | "true" | "1" => return Ok(true),
636 "n" | "no" | "false" | "0" => return Ok(false),
637 _ => {
638 let _ = self.base.write_output("Please answer y or n.\n");
639 }
640 }
641 }
642 }
643
644 pub fn ask_with(prompt: impl Into<String>, default: bool) -> Result<bool, PromptError> {
647 Confirm::new(prompt, default).ask()
648 }
649}
650
651pub struct Select<T> {
672 base: PromptBase,
673 choices: Vec<(String, T)>,
674}
675
676impl<T> Select<T> {
677 pub fn new(prompt: impl Into<String>) -> Self {
679 Self {
680 base: PromptBase::new(prompt),
681 choices: Vec::new(),
682 }
683 }
684
685 pub fn console(mut self, console: Console) -> Self {
687 self.base.console = Some(console);
688 self
689 }
690
691 pub fn choice(mut self, label: impl Into<String>, value: T) -> Self {
693 self.choices.push((label.into(), value));
694 self
695 }
696}
697
698impl<T: fmt::Display> Select<T> {
699 pub fn render(&self) -> String {
710 let mut output = String::new();
711 let styled_prompt = apply_style(&self.base.prompt, "prompt");
712 output.push_str(&styled_prompt);
713 output.push('\n');
714
715 for (i, (label, _)) in self.choices.iter().enumerate() {
716 output.push_str(&format!(" {}) {}\n", i + 1, label));
717 }
718
719 let styled_choices = apply_style(
720 &format!("Enter number [1-{}]", self.choices.len()),
721 "prompt.choices",
722 );
723 output.push_str(&format!("{}: ", styled_choices));
724 output
725 }
726}
727
728impl<T: fmt::Display + Clone> Select<T> {
729 pub fn ask(&self) -> Result<T, PromptError> {
739 if self.choices.is_empty() {
740 return Err(PromptError::InvalidResponse("no choices available".into()));
741 }
742
743 let prompt_str = self.render();
744 self.base.write_output(&prompt_str)?;
745
746 loop {
747 let value = self.base.read_line()?;
748 if value.is_empty() {
749 continue;
750 }
751 match value.trim().parse::<usize>() {
752 Ok(n) if n >= 1 && n <= self.choices.len() => {
753 return Ok(self.choices[n - 1].1.clone());
754 }
755 _ => {
756 let _ = self.base.write_output(&format!(
757 "Please enter a number between 1 and {}.\n",
758 self.choices.len()
759 ));
760 }
761 }
762 }
763 }
764}
765
766fn apply_style(text: &str, style_name: &str) -> String {
774 let theme = crate::theme::default_theme();
775 if let Some(style) = theme.get(style_name) {
776 let ansi = style.to_ansi();
777 if ansi.is_empty() {
778 text.to_string()
779 } else {
780 format!("\x1b[{}m{}\x1b[0m", ansi, text)
781 }
782 } else {
783 text.to_string()
784 }
785}
786
787#[allow(dead_code)]
789fn apply_raw_style(text: &str, style: &Style) -> String {
790 let ansi = style.to_ansi();
791 if ansi.is_empty() {
792 text.to_string()
793 } else {
794 format!("\x1b[{}m{}\x1b[0m", ansi, text)
795 }
796}
797
798#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
809 fn test_make_prompt_no_choices() {
810 let pb = PromptBase::new("Enter name");
811 let result = pb.make_prompt();
812 assert!(result.contains("Enter name"));
813 assert!(result.ends_with(": "));
814 }
815
816 #[test]
817 fn test_make_prompt_with_choices() {
818 let pb = PromptBase::new("Choose").choices(vec!["a".into(), "b".into()]);
819 let result = pb.make_prompt();
820 assert!(result.contains("Choose"));
821 assert!(result.contains("["));
822 assert!(result.contains("a/b"));
823 assert!(result.contains("]"));
824 }
825
826 #[test]
827 fn test_render_default() {
828 let pb = PromptBase::new("test");
829 let rendered = pb.render_default("hello");
830 assert!(rendered.contains("hello"));
831
832 let pb_hidden = PromptBase::new("test").show_default(false);
833 let rendered_hidden = pb_hidden.render_default("hello");
834 assert_eq!(rendered_hidden, "");
835 }
836
837 #[test]
838 fn test_check_choice_no_choices() {
839 let pb = PromptBase::new("test");
840 assert!(pb.check_choice("anything"));
841 }
842
843 #[test]
844 fn test_check_choice_case_insensitive() {
845 let pb = PromptBase::new("test")
846 .choices(vec!["yes".into(), "no".into()])
847 .case_sensitive(false);
848 assert!(pb.check_choice("YES"));
849 assert!(pb.check_choice("yes"));
850 assert!(pb.check_choice("No"));
851 assert!(!pb.check_choice("maybe"));
852 }
853
854 #[test]
855 fn test_check_choice_case_sensitive() {
856 let pb = PromptBase::new("test")
857 .choices(vec!["Yes".into(), "No".into()])
858 .case_sensitive(true);
859 assert!(pb.check_choice("Yes"));
860 assert!(!pb.check_choice("yes"));
861 }
862
863 #[test]
866 fn test_prompt_error_display() {
867 let err = PromptError::InvalidResponse("bad input".into());
868 assert_eq!(format!("{}", err), "bad input");
869
870 let err = PromptError::Cancelled;
871 assert_eq!(format!("{}", err), "cancelled");
872
873 let io_err = io::Error::new(io::ErrorKind::Other, "oh no");
874 let err = PromptError::IOError(io_err);
875 let msg = format!("{}", err);
876 assert!(msg.contains("I/O error"));
877 }
878
879 #[test]
880 fn test_prompt_error_source() {
881 use std::error::Error;
882
883 let err = PromptError::InvalidResponse("bad".into());
884 assert!(err.source().is_none());
885
886 let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
887 let err = PromptError::IOError(io_err);
888 assert!(err.source().is_some());
889 }
890
891 #[test]
892 fn test_from_io_error() {
893 let io_err = io::Error::new(io::ErrorKind::Other, "oh no");
894 let err: PromptError = io_err.into();
895 match err {
896 PromptError::IOError(_) => {}
897 _ => panic!("expected IOError"),
898 }
899 }
900
901 #[test]
904 fn test_confirm_prompt_text_default_true() {
905 let c = Confirm::new("Continue?", true);
906 let prompt = c.make_confirm_prompt();
907 assert!(prompt.contains("Continue?"));
908 assert!(prompt.contains("[Y/n]"));
909 }
910
911 #[test]
912 fn test_confirm_prompt_text_default_false() {
913 let c = Confirm::new("Continue?", false);
914 let prompt = c.make_confirm_prompt();
915 assert!(prompt.contains("Continue?"));
916 assert!(prompt.contains("[y/N]"));
917 }
918
919 #[test]
922 fn test_select_render() {
923 let s: Select<&str> = Select::new("Pick")
924 .choice("Option A", "a")
925 .choice("Option B", "b");
926 let rendered = s.render();
927 assert!(rendered.contains("Pick"));
928 assert!(rendered.contains("1) Option A"));
929 assert!(rendered.contains("2) Option B"));
930 assert!(rendered.contains("Enter number [1-2]"));
931 }
932
933 #[test]
934 fn test_select_no_choices_error() {
935 let s: Select<String> = Select::new("empty");
936 let result = s.ask();
937 match result {
938 Err(PromptError::InvalidResponse(msg)) => {
939 assert!(msg.contains("no choices"));
940 }
941 _ => panic!("expected InvalidResponse for no choices"),
942 }
943 }
944
945 #[test]
948 fn test_prompt_builder() {
949 let p = Prompt::new("Enter value")
950 .password(false)
951 .show_choices(true);
952 let rendered = p.render();
953 assert!(rendered.contains("Enter value"));
954 }
955
956 #[test]
957 fn test_prompt_render_default() {
958 let pb = PromptBase::new("Name").show_default(true);
959 assert!(pb.render_default("Alice").contains("Alice"));
960 }
961
962 #[test]
965 fn test_apply_style_plain() {
966 let result = apply_style("hello", "nonexistent.style");
967 assert_eq!(result, "hello");
968 }
969
970 #[test]
971 fn test_apply_style_with_theme() {
972 let result = apply_style("hello", "prompt");
973 assert!(result.contains("hello"));
974 }
975
976 #[test]
977 fn test_apply_raw_style_empty() {
978 let s = Style::new();
979 let result = apply_raw_style("test", &s);
980 assert_eq!(result, "test");
981 }
982}