Skip to content

Commit 8a47efb

Browse files
committed
feat: improve interactive prompts
Enhance Input trait with new methods: - prompt_text_with_default(): text input with editable default values - multi_select(): multi-item selection with Space toggle and Enter confirm Add non-interactive terminal detection: - Detect non-TTY stdin via std::io::IsTerminal - Auto-switch to non-interactive mode when piped/redirected - Debug message when TTY detection triggers mode change Improve non-interactive error messages: - Suggest using --help to see available flags - Clearer messaging about providing values via CLI flags Ctrl+C handling is already graceful via inquire's error propagation. Selection filtering and highlighting are native inquire features.
1 parent 8bac9f3 commit 8a47efb

File tree

5 files changed

+79
-5
lines changed

5 files changed

+79
-5
lines changed

src/input/interactive.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use crate::input::Input;
66
use anyhow::Result;
77
use inquire::validator::Validation;
8-
use inquire::{Confirm, Select, Text};
8+
use inquire::{Confirm, MultiSelect, Select, Text};
99

1010
#[derive(Debug, Default, Clone, Copy)]
1111
pub struct InteractiveInput;
@@ -55,4 +55,25 @@ impl Input for InteractiveInput {
5555

5656
Ok(c.prompt()?)
5757
}
58+
59+
fn prompt_text_with_default(
60+
&self,
61+
prompt: &str,
62+
default: &str,
63+
validator: Option<&dyn Fn(&str) -> Result<Validation, inquire::CustomUserError>>,
64+
) -> Result<String> {
65+
let mut t = Text::new(prompt).with_default(default);
66+
67+
if let Some(v) = validator {
68+
t = t.with_validator(v);
69+
}
70+
71+
Ok(t.prompt()?)
72+
}
73+
74+
fn multi_select(&self, prompt: &str, options: &[String]) -> Result<Vec<String>> {
75+
let ms = MultiSelect::new(prompt, options.to_vec())
76+
.with_help_message("Use Space to toggle, Enter to confirm");
77+
Ok(ms.prompt()?)
78+
}
5879
}

src/input/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ pub trait Input: Send + Sync {
6666

6767
/// Prompt the user for confirmation (yes/no).
6868
fn confirm(&self, prompt: &str, default: Option<bool>) -> Result<bool>;
69+
70+
/// Prompt the user for text input with a default value pre-filled.
71+
///
72+
/// The default value is editable by the user. If they press Enter without
73+
/// changes, the default is used.
74+
fn prompt_text_with_default(
75+
&self,
76+
prompt: &str,
77+
default: &str,
78+
validator: Option<&dyn Fn(&str) -> Result<Validation, inquire::CustomUserError>>,
79+
) -> Result<String>;
80+
81+
/// Prompt the user to select multiple items from a list.
82+
///
83+
/// Returns the selected items' labels.
84+
fn multi_select(&self, prompt: &str, options: &[String]) -> Result<Vec<String>>;
6985
}
7086

7187
/// Create an `Input` implementation based on `InputMode`.

src/input/non_interactive.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ impl NonInteractiveInput {
2121

2222
fn blocked(&self, kind: &str, prompt: &str) -> anyhow::Error {
2323
anyhow::anyhow!(
24-
"Interactive {} '{}' blocked: non-interactive mode is enabled. \
25-
Please provide the required input via command-line arguments instead.",
24+
"Interactive {} '{}' blocked: non-interactive mode is active. \
25+
Provide the required value via command-line flags instead. \
26+
Use --help on the command to see available flags.",
2627
kind,
2728
prompt
2829
)
@@ -47,4 +48,17 @@ impl Input for NonInteractiveInput {
4748
fn confirm(&self, prompt: &str, _default: Option<bool>) -> Result<bool> {
4849
Err(self.blocked("confirmation", prompt))
4950
}
51+
52+
fn prompt_text_with_default(
53+
&self,
54+
prompt: &str,
55+
_default: &str,
56+
_validator: Option<&dyn Fn(&str) -> Result<Validation, inquire::CustomUserError>>,
57+
) -> Result<String> {
58+
Err(self.blocked("prompt", prompt))
59+
}
60+
61+
fn multi_select(&self, prompt: &str, _options: &[String]) -> Result<Vec<String>> {
62+
Err(self.blocked("multi-select", prompt))
63+
}
5064
}

src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,12 @@ async fn async_main() -> anyhow::Result<()> {
147147
let output = create_output(output_mode);
148148

149149
// Create input handler based on flags.
150-
// Rule: --json implies non-interactive input.
151-
let input_mode = if cli.json || cli.non_interactive || cli.quiet {
150+
// Rule: --json, --quiet, --non-interactive, or non-TTY stdin all imply non-interactive input.
151+
let is_tty = std::io::IsTerminal::is_terminal(&std::io::stdin());
152+
let input_mode = if cli.json || cli.non_interactive || cli.quiet || !is_tty {
153+
if !is_tty && !cli.json && !cli.non_interactive && !cli.quiet {
154+
debug!("stdin is not a terminal — using non-interactive mode. Use --non-interactive to suppress this message.");
155+
}
152156
InputMode::NonInteractive
153157
} else {
154158
InputMode::Interactive

tests/feature_template_test.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,25 @@ impl Input for MockInput {
144144
fn confirm(&self, _prompt: &str, _default: Option<bool>) -> anyhow::Result<bool> {
145145
Ok(self.confirm_response)
146146
}
147+
148+
fn prompt_text_with_default(
149+
&self,
150+
prompt: &str,
151+
_default: &str,
152+
_validator: Option<&dyn Fn(&str) -> anyhow::Result<Validation, inquire::CustomUserError>>,
153+
) -> anyhow::Result<String> {
154+
Err(anyhow::anyhow!(
155+
"MockInput: prompt_text_with_default not implemented for '{}'",
156+
prompt
157+
))
158+
}
159+
160+
fn multi_select(&self, prompt: &str, _options: &[String]) -> anyhow::Result<Vec<String>> {
161+
Err(anyhow::anyhow!(
162+
"MockInput: multi_select not implemented for '{}'",
163+
prompt
164+
))
165+
}
147166
}
148167

149168
// Safety: MockInput is only used in single-threaded tests

0 commit comments

Comments
 (0)