1
+//! CI-aware interactive prompts.
2
+//!
3
+//! Thin wrappers over `dialoguer` that respect [`crate::ci::is_ci`]. In CI
4
+//! (non-interactive) mode:
5
+//!
6
+//! * [`confirm`] resolves to its default instead of asking.
7
+//! * [`input`] / [`input_optional`] return the supplied default, or fail.
8
+//! * [`password`] and the required [`input_required`] fail fast — secrets and
9
+//! free-form answers can never be guessed.
10
+//! * [`select`] / [`select_opt`] use an explicit CI default when the caller
11
+//! provides one, otherwise fail.
12
+//!
13
+//! Failures are plain [`anyhow::Error`]s with an actionable message, so a CI
14
+//! pipeline gets a clear exit code instead of a hung TTY.
15
+
16
+use {
17
+ crate::{
18
+ ci::{interactive_message, is_ci},
19
+ ui::fail_message,
20
+ },
21
+ anyhow::{anyhow, Result},
22
+ dialoguer::{console::Term, theme::ColorfulTheme, Confirm, Input, Password, Select},
23
+};
24
+
25
+fn ci_required(what: &str) -> anyhow::Error {
26
+ anyhow!(fail_message(&interactive_message(what)))
27
+}
28
+
29
+fn io_error(err: dialoguer::Error) -> anyhow::Error {
30
+ anyhow!(fail_message(&format!("Prompt failed: {err}")))
31
+}
32
+
33
+/// Yes/no confirmation. In CI mode returns `default` without prompting.
34
+pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
35
+ if is_ci() {
36
+ return Ok(default);
37
+ }
38
+ Confirm::with_theme(&ColorfulTheme::default())
39
+ .with_prompt(prompt)
40
+ .default(default)
41
+ .interact()
42
+ .map_err(io_error)
43
+}
44
+
45
+/// Free-text input with a default. In CI mode returns the default.
46
+pub fn input(prompt: &str, default: &str) -> Result<String> {
47
+ if is_ci() {
48
+ return Ok(default.to_string());
49
+ }
50
+ Input::<String>::with_theme(&ColorfulTheme::default())
51
+ .with_prompt(prompt)
52
+ .default(default.to_string())
53
+ .interact()
54
+ .map_err(io_error)
55
+}
56
+
57
+/// Free-text input that allows an empty value, with a default. In CI mode
58
+/// returns the default (which may be empty).
59
+pub fn input_optional(prompt: &str, default: &str) -> Result<String> {
60
+ if is_ci() {
61
+ return Ok(default.to_string());
62
+ }
63
+ Input::<String>::with_theme(&ColorfulTheme::default())
64
+ .with_prompt(prompt)
65
+ .default(default.to_string())
66
+ .allow_empty(true)
67
+ .interact()
68
+ .map_err(io_error)
69
+}
70
+
71
+/// Required free-text input with no default. Fails in CI mode.
72
+pub fn input_required(prompt: &str) -> Result<String> {
73
+ if is_ci() {
74
+ return Err(ci_required(prompt));
75
+ }
76
+ Input::<String>::with_theme(&ColorfulTheme::default())
77
+ .with_prompt(prompt)
78
+ .interact()
79
+ .map_err(io_error)
80
+}
81
+
82
+/// Hidden password input. Always fails in CI mode — secrets are never guessed.
83
+pub fn password(prompt: &str) -> Result<String> {
84
+ if is_ci() {
85
+ return Err(ci_required(prompt));
86
+ }
87
+ Password::with_theme(&ColorfulTheme::default())
88
+ .with_prompt(prompt)
89
+ .interact()
90
+ .map_err(io_error)
91
+}
92
+
93
+/// Single-choice selection. In CI mode returns `ci_default` when the caller
94
+/// supplies one, otherwise fails.
95
+pub fn select<T: ToString>(
96
+ prompt: &str,
97
+ items: &[T],
98
+ default: usize,
99
+ ci_default: Option<usize>,
100
+) -> Result<usize> {
101
+ if is_ci() {
102
+ return ci_default.ok_or_else(|| ci_required(prompt));
103
+ }
104
+ Select::with_theme(&ColorfulTheme::default())
105
+ .with_prompt(prompt)
106
+ .items(items)
107
+ .default(default)
108
+ .interact_on(&Term::stderr())
109
+ .map_err(io_error)
110
+}
111
+
112
+/// Single-choice selection that may be cancelled (returns `None`). In CI mode
113
+/// returns `Some(ci_default)` when supplied, otherwise fails.
114
+pub fn select_opt<T: ToString>(
115
+ prompt: &str,
116
+ items: &[T],
117
+ default: usize,
118
+ ci_default: Option<usize>,
119
+) -> Result<Option<usize>> {
120
+ if is_ci() {
121
+ return Ok(Some(ci_default.ok_or_else(|| ci_required(prompt))?));
122
+ }
123
+ Select::with_theme(&ColorfulTheme::default())
124
+ .with_prompt(prompt)
125
+ .items(items)
126
+ .default(default)
127
+ .interact_on_opt(&Term::stderr())
128
+ .map_err(io_error)
129
+}