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