Skip to main content

systemprompt_cli/
interactive.rs

1//! Interactive prompting helpers shared across commands.
2//!
3//! Each helper bridges flag-driven and interactive input: in interactive mode
4//! it prompts the operator, and in non-interactive mode it falls back to a
5//! default or fails with a "flag required" error. Used to resolve missing
6//! arguments, confirmations, and selections uniformly.
7
8use crate::CliConfig;
9use anyhow::{Result, anyhow};
10use dialoguer::theme::ColorfulTheme;
11use dialoguer::{Confirm, Select};
12
13pub fn require_confirmation(
14    message: &str,
15    skip_confirmation: bool,
16    config: &CliConfig,
17) -> Result<()> {
18    if skip_confirmation {
19        return Ok(());
20    }
21
22    if !config.is_interactive() {
23        return Err(anyhow!("--yes is required in non-interactive mode"));
24    }
25
26    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
27        .with_prompt(message)
28        .default(false)
29        .interact()?;
30
31    if confirmed {
32        Ok(())
33    } else {
34        Err(anyhow!("Operation cancelled"))
35    }
36}
37
38pub fn require_confirmation_default_yes(
39    message: &str,
40    skip_confirmation: bool,
41    config: &CliConfig,
42) -> Result<()> {
43    if skip_confirmation {
44        return Ok(());
45    }
46
47    if !config.is_interactive() {
48        return Err(anyhow!("--yes is required in non-interactive mode"));
49    }
50
51    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
52        .with_prompt(message)
53        .default(true)
54        .interact()?;
55
56    if confirmed {
57        Ok(())
58    } else {
59        Err(anyhow!("Operation cancelled"))
60    }
61}
62
63pub fn resolve_required<T, F>(
64    value: Option<T>,
65    flag_name: &str,
66    config: &CliConfig,
67    prompt_fn: F,
68) -> Result<T>
69where
70    F: FnOnce() -> Result<T>,
71{
72    match value {
73        Some(v) => Ok(v),
74        None if config.is_interactive() => prompt_fn(),
75        None => Err(anyhow!(
76            "--{} is required in non-interactive mode",
77            flag_name
78        )),
79    }
80}
81
82pub fn select_from_list<T: ToString + Clone>(
83    prompt: &str,
84    items: &[T],
85    flag_name: &str,
86    config: &CliConfig,
87) -> Result<T> {
88    if items.is_empty() {
89        return Err(anyhow!("No items available for selection"));
90    }
91
92    if !config.is_interactive() {
93        return Err(anyhow!(
94            "--{} is required in non-interactive mode",
95            flag_name
96        ));
97    }
98
99    let display: Vec<String> = items.iter().map(ToString::to_string).collect();
100
101    let idx = Select::with_theme(&ColorfulTheme::default())
102        .with_prompt(prompt)
103        .items(&display)
104        .default(0)
105        .interact()?;
106
107    Ok(items[idx].clone())
108}
109
110pub fn select_index(prompt: &str, items: &[&str], config: &CliConfig) -> Result<Option<usize>> {
111    if !config.is_interactive() {
112        return Ok(None);
113    }
114
115    let idx = Select::with_theme(&ColorfulTheme::default())
116        .with_prompt(prompt)
117        .items(items)
118        .default(0)
119        .interact()?;
120
121    Ok(Some(idx))
122}
123
124pub fn prompt_input(prompt: &str, flag_name: &str, config: &CliConfig) -> Result<String> {
125    if !config.is_interactive() {
126        return Err(anyhow!(
127            "--{} is required in non-interactive mode",
128            flag_name
129        ));
130    }
131
132    let input = dialoguer::Input::<String>::with_theme(&ColorfulTheme::default())
133        .with_prompt(prompt)
134        .interact_text()?;
135
136    Ok(input)
137}
138
139pub fn prompt_input_with_default(
140    prompt: &str,
141    default: &str,
142    config: &CliConfig,
143) -> Result<String> {
144    if !config.is_interactive() {
145        return Ok(default.to_owned());
146    }
147
148    let input = dialoguer::Input::<String>::with_theme(&ColorfulTheme::default())
149        .with_prompt(prompt)
150        .default(default.to_owned())
151        .interact_text()?;
152
153    Ok(input)
154}
155
156pub fn confirm_optional(message: &str, default: bool, config: &CliConfig) -> Result<bool> {
157    if !config.is_interactive() {
158        return Ok(default);
159    }
160
161    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
162        .with_prompt(message)
163        .default(default)
164        .interact()?;
165
166    Ok(confirmed)
167}