1use std::io::IsTerminal;
15
16use rskit_errors::AppResult;
17
18use super::choice::{Choice, ChoiceId};
19use super::kinds;
20use super::mode::PromptMode;
21use super::render::Style;
22use super::terminal::{LineTerminal, Terminal};
23use super::validate::Validator;
24use crate::theme::{ColorChoice, Glyphs, Palette};
25
26pub struct Prompter<T> {
31 terminal: T,
32 mode: PromptMode,
33 style: Style,
34}
35
36impl Prompter<Box<dyn Terminal>> {
37 #[must_use]
46 pub fn from_env(color: ColorChoice) -> Self {
47 let stderr = std::io::stderr();
48 let mode = PromptMode::from_stdio(std::io::stdin().is_terminal(), stderr.is_terminal());
49 let palette = Palette::for_stream(color, &stderr);
50 let glyphs = Glyphs::from_env();
51 let terminal = resolve_terminal(mode.is_interactive());
52 Self {
53 terminal,
54 mode,
55 style: Style::new(palette, glyphs),
56 }
57 }
58}
59
60impl<T: Terminal> Prompter<T> {
61 #[must_use]
66 pub const fn new(terminal: T, mode: PromptMode, palette: Palette) -> Self {
67 Self {
68 terminal,
69 mode,
70 style: Style::new(palette, Glyphs::new(false)),
71 }
72 }
73
74 #[must_use]
76 pub const fn with_glyphs(mut self, glyphs: Glyphs) -> Self {
77 self.style = Style::new(self.style.palette(), glyphs);
78 self
79 }
80
81 #[must_use]
83 pub const fn mode(&self) -> PromptMode {
84 self.mode
85 }
86
87 #[must_use]
89 pub const fn terminal(&self) -> &T {
90 &self.terminal
91 }
92
93 const fn ask<'a>(&self, prompt: &'a str) -> kinds::Ask<'a> {
96 kinds::Ask {
97 style: self.style,
98 mode: self.mode,
99 prompt,
100 }
101 }
102
103 pub fn select(&mut self, prompt: &str, choices: &[Choice]) -> AppResult<ChoiceId> {
114 let ask = self.ask(prompt);
115 kinds::select::run(&mut self.terminal, ask, choices)
116 }
117
118 pub fn multi_select(&mut self, prompt: &str, choices: &[Choice]) -> AppResult<Vec<ChoiceId>> {
128 let ask = self.ask(prompt);
129 kinds::multi_select::run(&mut self.terminal, ask, choices)
130 }
131
132 pub fn confirm(&mut self, prompt: &str, default: bool) -> AppResult<bool> {
138 let ask = self.ask(prompt);
139 kinds::confirm::run(&mut self.terminal, ask, default)
140 }
141
142 pub fn text(&mut self, prompt: &str, default: Option<&str>) -> AppResult<String> {
151 let ask = self.ask(prompt);
152 kinds::text::run(&mut self.terminal, ask, default, None)
153 }
154
155 pub fn text_with(
164 &mut self,
165 prompt: &str,
166 default: Option<&str>,
167 validator: &dyn Validator,
168 ) -> AppResult<String> {
169 let ask = self.ask(prompt);
170 kinds::text::run(&mut self.terminal, ask, default, Some(validator))
171 }
172}
173
174fn resolve_terminal(is_tty: bool) -> Box<dyn Terminal> {
177 #[cfg(feature = "interactive")]
178 {
179 if is_tty && let Ok(terminal) = super::terminal::RichTerminal::stderr() {
180 return Box::new(terminal);
181 }
182 }
183 #[cfg(not(feature = "interactive"))]
184 let _ = is_tty;
185 Box::new(LineTerminal::stdio())
186}
187
188#[cfg(test)]
189mod tests {
190 use super::{Choice, ChoiceId, Palette, PromptMode, Prompter};
191 use crate::prompt::key::Key;
192 use crate::prompt::terminal::ScriptedTerminal;
193 use crate::prompt::validate::non_empty;
194 use crate::theme::{ColorChoice, Glyphs};
195
196 fn plain_choices() -> Vec<Choice> {
197 vec![
198 Choice::new("go", "Go"),
199 Choice::new("rust", "Rust").recommended(),
200 Choice::new("node", "Node.js"),
201 ]
202 }
203
204 fn no_default_choices() -> Vec<Choice> {
205 vec![Choice::new("go", "Go"), Choice::new("rust", "Rust")]
206 }
207
208 fn line_prompter(
209 terminal: ScriptedTerminal,
210 mode: PromptMode,
211 color: bool,
212 ) -> Prompter<ScriptedTerminal> {
213 Prompter::new(terminal, mode, Palette::new(color))
214 }
215
216 #[test]
219 fn non_interactive_select_resolves_to_recommended() {
220 let choice = line_prompter(
221 ScriptedTerminal::line_driven(),
222 PromptMode::NonInteractive,
223 false,
224 )
225 .select("Ecosystem?", &plain_choices())
226 .expect("recommended default resolves");
227 assert_eq!(choice, ChoiceId::new("rust"));
228 }
229
230 #[test]
231 fn non_interactive_select_without_default_errors() {
232 let choices = vec![Choice::new("go", "Go"), Choice::new("rust", "Rust")];
233 let err = line_prompter(
234 ScriptedTerminal::line_driven(),
235 PromptMode::NonInteractive,
236 false,
237 )
238 .select("Ecosystem?", &choices)
239 .expect_err("no recommended default must error");
240 assert!(err.message().contains("non-interactive"));
241 }
242
243 #[test]
244 fn non_interactive_multi_select_returns_recommended_set() {
245 let selected = line_prompter(
246 ScriptedTerminal::line_driven(),
247 PromptMode::NonInteractive,
248 false,
249 )
250 .multi_select("Tasks?", &plain_choices())
251 .expect("recommended set resolves");
252 assert_eq!(selected, vec![ChoiceId::new("rust")]);
253 }
254
255 #[test]
256 fn non_interactive_multi_select_allows_empty_default() {
257 let choices = vec![Choice::new("a", "A"), Choice::new("b", "B")];
258 let selected = line_prompter(
259 ScriptedTerminal::line_driven(),
260 PromptMode::NonInteractive,
261 false,
262 )
263 .multi_select("Tasks?", &choices)
264 .expect("empty selection is valid");
265 assert!(selected.is_empty());
266 }
267
268 #[test]
269 fn non_interactive_confirm_returns_default() {
270 let value = line_prompter(
271 ScriptedTerminal::line_driven(),
272 PromptMode::NonInteractive,
273 false,
274 )
275 .confirm("Proceed?", true)
276 .expect("confirm resolves to default");
277 assert!(value);
278 }
279
280 #[test]
281 fn non_interactive_text_uses_default_and_errors_without_one() {
282 let value = line_prompter(
283 ScriptedTerminal::line_driven(),
284 PromptMode::NonInteractive,
285 false,
286 )
287 .text("Name?", Some("toven"))
288 .expect("text resolves to default");
289 assert_eq!(value, "toven");
290
291 let err = line_prompter(
292 ScriptedTerminal::line_driven(),
293 PromptMode::NonInteractive,
294 false,
295 )
296 .text("Name?", None)
297 .expect_err("no default must error");
298 assert!(err.message().contains("non-interactive"));
299 }
300
301 #[test]
302 fn non_interactive_text_with_rejects_invalid_default() {
303 let err = line_prompter(
304 ScriptedTerminal::line_driven(),
305 PromptMode::NonInteractive,
306 false,
307 )
308 .text_with("Name?", Some(" "), &non_empty("required"))
309 .expect_err("invalid default must error");
310 assert!(err.message().contains("invalid"));
311 }
312
313 #[test]
316 fn line_select_reads_a_numbered_answer() {
317 let choice = line_prompter(
318 ScriptedTerminal::line_driven().with_line("1"),
319 PromptMode::Interactive,
320 false,
321 )
322 .select("Ecosystem?", &plain_choices())
323 .expect("first choice selected");
324 assert_eq!(choice, ChoiceId::new("go"));
325 }
326
327 #[test]
328 fn line_select_empty_line_accepts_recommended() {
329 let choice = line_prompter(
330 ScriptedTerminal::line_driven().with_line(""),
331 PromptMode::Interactive,
332 false,
333 )
334 .select("Ecosystem?", &plain_choices())
335 .expect("blank accepts recommended");
336 assert_eq!(choice, ChoiceId::new("rust"));
337 }
338
339 #[test]
340 fn line_select_reprompts_on_invalid_then_succeeds() {
341 let mut prompter = line_prompter(
342 ScriptedTerminal::line_driven().with_lines(["9", "x", "3"]),
343 PromptMode::Interactive,
344 false,
345 );
346 let choice = prompter
347 .select("Ecosystem?", &plain_choices())
348 .expect("valid choice after retries");
349 assert_eq!(choice, ChoiceId::new("node"));
350 assert!(prompter.terminal().output().contains("between 1 and 3"));
351 }
352
353 #[test]
354 fn line_select_errors_when_input_closes() {
355 let choices = vec![Choice::new("go", "Go"), Choice::new("rust", "Rust")];
356 let err = line_prompter(
357 ScriptedTerminal::line_driven(),
358 PromptMode::Interactive,
359 false,
360 )
361 .select("Ecosystem?", &choices)
362 .expect_err("closed input must error, not hang");
363 assert!(err.message().contains("input closed"));
364 }
365
366 #[test]
367 fn line_multi_select_parses_and_dedupes() {
368 let selected = line_prompter(
369 ScriptedTerminal::line_driven().with_line("3, 1, 1"),
370 PromptMode::Interactive,
371 false,
372 )
373 .multi_select("Tasks?", &plain_choices())
374 .expect("comma list parses");
375 assert_eq!(selected, vec![ChoiceId::new("node"), ChoiceId::new("go")]);
376 }
377
378 #[test]
379 fn line_multi_select_empty_line_uses_defaults() {
380 let selected = line_prompter(
381 ScriptedTerminal::line_driven().with_line(""),
382 PromptMode::Interactive,
383 false,
384 )
385 .multi_select("Tasks?", &plain_choices())
386 .expect("blank uses recommended defaults");
387 assert_eq!(selected, vec![ChoiceId::new("rust")]);
388 }
389
390 #[test]
391 fn line_confirm_parses_yes_no_and_default() {
392 assert!(
393 line_prompter(
394 ScriptedTerminal::line_driven().with_line("y"),
395 PromptMode::Interactive,
396 false
397 )
398 .confirm("Proceed?", false)
399 .expect("yes")
400 );
401 assert!(
402 !line_prompter(
403 ScriptedTerminal::line_driven().with_line("no"),
404 PromptMode::Interactive,
405 false
406 )
407 .confirm("Proceed?", true)
408 .expect("no")
409 );
410 assert!(
411 line_prompter(
412 ScriptedTerminal::line_driven().with_line(""),
413 PromptMode::Interactive,
414 false
415 )
416 .confirm("Proceed?", true)
417 .expect("blank uses default")
418 );
419 }
420
421 #[test]
422 fn line_text_reads_value_and_falls_back_to_default() {
423 let value = line_prompter(
424 ScriptedTerminal::line_driven().with_line("custom"),
425 PromptMode::Interactive,
426 false,
427 )
428 .text("Name?", Some("toven"))
429 .expect("typed value");
430 assert_eq!(value, "custom");
431
432 let value = line_prompter(
433 ScriptedTerminal::line_driven().with_line(""),
434 PromptMode::Interactive,
435 false,
436 )
437 .text("Name?", Some("toven"))
438 .expect("blank uses default");
439 assert_eq!(value, "toven");
440 }
441
442 #[test]
443 fn line_text_with_reasks_until_valid() {
444 let mut prompter = line_prompter(
445 ScriptedTerminal::line_driven().with_lines([" ", "ok"]),
446 PromptMode::Interactive,
447 false,
448 );
449 let value = prompter
450 .text_with("Name?", None, &non_empty("required"))
451 .expect("valid after retry");
452 assert_eq!(value, "ok");
453 assert!(prompter.terminal().output().contains("required"));
454 }
455
456 #[test]
459 fn key_select_navigates_and_confirms() {
460 let choice = line_prompter(
461 ScriptedTerminal::key_driven().with_keys([Key::Down, Key::Enter]),
462 PromptMode::Interactive,
463 false,
464 )
465 .select("Ecosystem?", &plain_choices())
466 .expect("arrow navigation selects");
467 assert_eq!(choice, ChoiceId::new("node"));
468 }
469
470 #[test]
471 fn key_select_starts_on_recommended_default() {
472 let choice = line_prompter(
473 ScriptedTerminal::key_driven().with_key(Key::Enter),
474 PromptMode::Interactive,
475 false,
476 )
477 .select("Ecosystem?", &plain_choices())
478 .expect("enter accepts default");
479 assert_eq!(choice, ChoiceId::new("rust"));
480 }
481
482 #[test]
483 fn key_select_escape_cancels() {
484 let err = line_prompter(
485 ScriptedTerminal::key_driven().with_key(Key::Escape),
486 PromptMode::Interactive,
487 false,
488 )
489 .select("Ecosystem?", &plain_choices())
490 .expect_err("escape cancels");
491 assert!(err.message().contains("cancelled"));
492 }
493
494 #[test]
495 fn key_multi_select_toggles_selection() {
496 let selected = line_prompter(
499 ScriptedTerminal::key_driven().with_keys([
500 Key::Space, Key::Down,
502 Key::Space, Key::Down,
504 Key::Space, Key::Enter,
506 ]),
507 PromptMode::Interactive,
508 false,
509 )
510 .multi_select("Tasks?", &plain_choices())
511 .expect("space toggles");
512 assert_eq!(selected, vec![ChoiceId::new("go"), ChoiceId::new("node")]);
513 }
514
515 #[test]
516 fn key_confirm_reads_letter() {
517 assert!(
518 line_prompter(
519 ScriptedTerminal::key_driven().with_key(Key::Char('y')),
520 PromptMode::Interactive,
521 false
522 )
523 .confirm("Proceed?", false)
524 .expect("y confirms")
525 );
526 }
527
528 #[test]
529 fn key_text_edits_with_backspace() {
530 let value = line_prompter(
531 ScriptedTerminal::key_driven().with_keys([
532 Key::Char('h'),
533 Key::Char('i'),
534 Key::Char('x'),
535 Key::Backspace,
536 Key::Enter,
537 ]),
538 PromptMode::Interactive,
539 false,
540 )
541 .text("Name?", None)
542 .expect("typed value");
543 assert_eq!(value, "hi");
544 }
545
546 #[test]
547 fn key_text_inserts_literal_space() {
548 let value = line_prompter(
552 ScriptedTerminal::key_driven().with_keys([
553 Key::Char('h'),
554 Key::Char('i'),
555 Key::Space,
556 Key::Char('t'),
557 Key::Char('h'),
558 Key::Char('e'),
559 Key::Char('r'),
560 Key::Char('e'),
561 Key::Enter,
562 ]),
563 PromptMode::Interactive,
564 false,
565 )
566 .text("Name?", None)
567 .expect("typed value");
568 assert_eq!(value, "hi there");
569 }
570
571 #[test]
572 fn key_select_runs_in_raw_mode_and_restores() {
573 let mut prompter = line_prompter(
574 ScriptedTerminal::key_driven().with_key(Key::Enter),
575 PromptMode::Interactive,
576 false,
577 );
578 let _ = prompter
579 .select("Ecosystem?", &plain_choices())
580 .expect("select");
581 assert!(!prompter.terminal().is_interactive(), "raw mode restored");
582 }
583
584 #[test]
587 fn choice_metadata_round_trips_into_rendered_output() {
588 let choices = vec![
589 Choice::new("rust", "Rust")
590 .with_annotation("detected in dev-deps")
591 .recommended(),
592 Choice::new("go", "Go"),
593 ];
594 let mut prompter = line_prompter(
595 ScriptedTerminal::line_driven().with_line("1"),
596 PromptMode::Interactive,
597 false,
598 );
599 let _ = prompter.select("Ecosystem?", &choices).expect("select");
600 let rendered = prompter.terminal().output();
601 assert!(rendered.contains("Rust"));
602 assert!(rendered.contains("detected in dev-deps"));
603 assert!(rendered.contains("(recommended)"));
604 }
605
606 #[test]
607 fn disabled_palette_renders_without_ansi_escapes() {
608 let mut prompter = line_prompter(
609 ScriptedTerminal::line_driven().with_line("1"),
610 PromptMode::Interactive,
611 false,
612 );
613 let _ = prompter
614 .select("Ecosystem?", &plain_choices())
615 .expect("select");
616 assert!(
617 !prompter.terminal().output().contains('\u{1b}'),
618 "no color must be byte-clean"
619 );
620 }
621
622 #[test]
623 fn enabled_palette_emits_ansi_escapes() {
624 let mut prompter = line_prompter(
625 ScriptedTerminal::line_driven().with_line("1"),
626 PromptMode::Interactive,
627 true,
628 );
629 let _ = prompter
630 .select("Ecosystem?", &plain_choices())
631 .expect("select");
632 assert!(
633 prompter.terminal().output().contains('\u{1b}'),
634 "color must emit SGR escapes"
635 );
636 }
637
638 #[test]
639 fn empty_choice_set_is_rejected() {
640 let err = line_prompter(
641 ScriptedTerminal::line_driven(),
642 PromptMode::Interactive,
643 false,
644 )
645 .select("Ecosystem?", &[])
646 .expect_err("empty choices rejected");
647 assert!(err.message().contains("at least one choice"));
648 }
649
650 #[test]
651 fn multi_select_empty_choice_set_is_rejected() {
652 let err = line_prompter(
653 ScriptedTerminal::line_driven(),
654 PromptMode::Interactive,
655 false,
656 )
657 .multi_select("Tasks?", &[])
658 .expect_err("empty choices rejected");
659 assert!(err.message().contains("at least one choice"));
660 }
661
662 #[test]
665 fn key_confirm_enter_default_letter_no_and_escape() {
666 assert!(
668 line_prompter(
669 ScriptedTerminal::key_driven().with_key(Key::Enter),
670 PromptMode::Interactive,
671 false
672 )
673 .confirm("Proceed?", true)
674 .expect("enter accepts default")
675 );
676 assert!(
678 !line_prompter(
679 ScriptedTerminal::key_driven().with_key(Key::Char('n')),
680 PromptMode::Interactive,
681 false
682 )
683 .confirm("Proceed?", true)
684 .expect("n declines")
685 );
686 assert!(
688 line_prompter(
689 ScriptedTerminal::key_driven().with_keys([Key::Left, Key::Char('Y')]),
690 PromptMode::Interactive,
691 false
692 )
693 .confirm("Proceed?", false)
694 .expect("Y confirms after an ignored key")
695 );
696 let err = line_prompter(
698 ScriptedTerminal::key_driven().with_key(Key::Escape),
699 PromptMode::Interactive,
700 false,
701 )
702 .confirm("Proceed?", true)
703 .expect_err("escape cancels");
704 assert!(err.message().contains("cancelled"));
705 }
706
707 #[test]
708 fn line_confirm_reprompts_on_invalid_and_errors_on_close() {
709 let mut prompter = line_prompter(
710 ScriptedTerminal::line_driven().with_lines(["maybe", "yes"]),
711 PromptMode::Interactive,
712 false,
713 );
714 assert!(
715 prompter
716 .confirm("Proceed?", false)
717 .expect("yes after retry")
718 );
719 assert!(prompter.terminal().output().contains("'y' or 'n'"));
720
721 let err = line_prompter(
722 ScriptedTerminal::line_driven(),
723 PromptMode::Interactive,
724 false,
725 )
726 .confirm("Proceed?", true)
727 .expect_err("closed input must error");
728 assert!(err.message().contains("input closed"));
729 }
730
731 #[test]
734 fn key_select_home_end_and_up_navigate_and_ignore_unrelated_keys() {
735 let choice = line_prompter(
738 ScriptedTerminal::key_driven().with_keys([
739 Key::End,
740 Key::Home,
741 Key::Down,
742 Key::Up,
743 Key::Tab,
744 Key::Left,
745 Key::Enter,
746 ]),
747 PromptMode::Interactive,
748 false,
749 )
750 .select("Ecosystem?", &plain_choices())
751 .expect("navigation resolves");
752 assert_eq!(choice, ChoiceId::new("rust"));
753 }
754
755 #[test]
756 fn line_select_empty_without_default_requires_a_choice() {
757 let mut prompter = line_prompter(
758 ScriptedTerminal::line_driven().with_lines(["", "2"]),
759 PromptMode::Interactive,
760 false,
761 );
762 let choice = prompter
763 .select("Ecosystem?", &no_default_choices())
764 .expect("valid choice after the required notice");
765 assert_eq!(choice, ChoiceId::new("rust"));
766 assert!(
767 prompter
768 .terminal()
769 .output()
770 .contains("a choice is required")
771 );
772 }
773
774 #[test]
777 fn key_multi_select_escape_cancels_and_navigation_wraps() {
778 let err = line_prompter(
779 ScriptedTerminal::key_driven().with_key(Key::Escape),
780 PromptMode::Interactive,
781 false,
782 )
783 .multi_select("Tasks?", &plain_choices())
784 .expect_err("escape cancels");
785 assert!(err.message().contains("cancelled"));
786
787 let selected = line_prompter(
789 ScriptedTerminal::key_driven().with_keys([
790 Key::End,
791 Key::Home,
792 Key::Up,
793 Key::Tab,
794 Key::Left,
795 Key::Space,
796 Key::Enter,
797 ]),
798 PromptMode::Interactive,
799 false,
800 )
801 .multi_select("Tasks?", &plain_choices())
802 .expect("navigation and toggle resolve");
803 assert_eq!(selected, vec![ChoiceId::new("go"), ChoiceId::new("rust")]);
804 }
805
806 #[test]
807 fn line_multi_select_notice_none_hint_and_close() {
808 let mut prompter = line_prompter(
811 ScriptedTerminal::line_driven().with_lines(["x", "1,2"]),
812 PromptMode::Interactive,
813 false,
814 );
815 let selected = prompter
816 .multi_select("Tasks?", &no_default_choices())
817 .expect("valid list after the notice");
818 assert_eq!(selected, vec![ChoiceId::new("go"), ChoiceId::new("rust")]);
819 let out = prompter.terminal().output();
820 assert!(out.contains("[none]"));
821 assert!(out.contains("comma-separated"));
822
823 let err = line_prompter(
824 ScriptedTerminal::line_driven(),
825 PromptMode::Interactive,
826 false,
827 )
828 .multi_select("Tasks?", &no_default_choices())
829 .expect_err("closed input must error");
830 assert!(err.message().contains("input closed"));
831 }
832
833 #[test]
836 fn key_text_required_then_validator_reason_then_value() {
837 let reject_short = |value: &str| {
838 if value.len() >= 2 {
839 Ok(())
840 } else {
841 Err("too short".to_string())
842 }
843 };
844
845 let value = line_prompter(
848 ScriptedTerminal::key_driven().with_keys([
849 Key::Enter,
850 Key::Char('h'),
851 Key::Char('i'),
852 Key::Enter,
853 ]),
854 PromptMode::Interactive,
855 false,
856 )
857 .text("Name?", None)
858 .expect("value after the required notice");
859 assert_eq!(value, "hi");
860
861 let mut prompter = line_prompter(
863 ScriptedTerminal::key_driven().with_keys([
864 Key::Char('a'),
865 Key::Enter,
866 Key::Char('b'),
867 Key::Enter,
868 ]),
869 PromptMode::Interactive,
870 false,
871 );
872 let value = prompter
873 .text_with("Name?", None, &reject_short)
874 .expect("valid after the reason");
875 assert_eq!(value, "ab");
876 assert!(prompter.terminal().output().contains("too short"));
877 }
878
879 #[test]
880 fn key_text_ignores_unrelated_keys_and_escape_cancels() {
881 let err = line_prompter(
882 ScriptedTerminal::key_driven().with_keys([Key::Left, Key::Escape]),
883 PromptMode::Interactive,
884 false,
885 )
886 .text("Name?", None)
887 .expect_err("escape cancels");
888 assert!(err.message().contains("cancelled"));
889 }
890
891 #[test]
892 fn line_text_errors_when_input_closes() {
893 let err = line_prompter(
894 ScriptedTerminal::line_driven(),
895 PromptMode::Interactive,
896 false,
897 )
898 .text("Name?", None)
899 .expect_err("closed input must error");
900 assert!(err.message().contains("input closed"));
901 }
902
903 #[test]
904 fn line_text_with_shows_validator_reason_before_accepting() {
905 let reject_short = |value: &str| {
906 if value.len() >= 2 {
907 Ok(())
908 } else {
909 Err("too short".to_string())
910 }
911 };
912 let mut prompter = line_prompter(
913 ScriptedTerminal::line_driven().with_lines(["a", "ok"]),
914 PromptMode::Interactive,
915 false,
916 );
917 let value = prompter
918 .text_with("Name?", None, &reject_short)
919 .expect("valid after the reason");
920 assert_eq!(value, "ok");
921 assert!(prompter.terminal().output().contains("too short"));
922 }
923
924 #[test]
927 fn from_env_builds_a_prompter_and_reports_its_mode() {
928 let prompter = Prompter::from_env(ColorChoice::Never);
932 assert_eq!(prompter.mode(), PromptMode::NonInteractive);
933 }
934
935 #[test]
936 fn with_glyphs_overrides_symbols_and_preserves_mode() {
937 let prompter = line_prompter(
938 ScriptedTerminal::line_driven(),
939 PromptMode::NonInteractive,
940 false,
941 )
942 .with_glyphs(Glyphs::new(true));
943 assert_eq!(prompter.mode(), PromptMode::NonInteractive);
944 }
945}