//! Interactive prompt helpers for the CLI. //! //! The `prompt`/`prompt_optional`/`prompt_secret` family reads from stdin / //! the TTY; the `prompt_keep`/`prompt_keep_opt`/`prompt_yesno` variants are //! used by the edit handlers to keep current values when the user hits enter //! at a blank prompt. `prompt_secret` honours `RELICARIO_TEST_ITEM_SECRET` //! so integration tests (which don't have a TTY) can inject secrets. use anyhow::Result; /// `rpassword::prompt_password` wrapper that honours `RELICARIO_TEST_ITEM_SECRET` /// for integration-test use (rpassword reads /dev/tty by default, which is /// unavailable in assert_cmd-spawned children). pub(crate) fn prompt_secret(label: &str) -> Result { if let Some(s) = crate::test_item_secret_override() { return Ok(s); } rpassword::prompt_password(label).map_err(Into::into) } pub(crate) fn prompt(label: &str) -> Result { eprint!("{label}: "); std::io::Write::flush(&mut std::io::stderr())?; let mut s = String::new(); std::io::stdin().read_line(&mut s)?; let trimmed = s.trim().to_string(); if trimmed.is_empty() { anyhow::bail!("{label} required"); } Ok(trimmed) } pub(crate) fn prompt_optional(label: &str) -> Result> { eprint!("{label} (leave blank to skip): "); std::io::Write::flush(&mut std::io::stderr())?; let mut s = String::new(); std::io::stdin().read_line(&mut s)?; let trimmed = s.trim().to_string(); Ok(if trimmed.is_empty() { None } else { Some(trimmed) }) } pub(crate) fn prompt_keep(label: &str, current: &str) -> Result> { eprint!("{label} [{current}]: "); std::io::Write::flush(&mut std::io::stderr())?; let mut s = String::new(); std::io::stdin().read_line(&mut s)?; let trimmed = s.trim().to_string(); Ok(if trimmed.is_empty() { None } else { Some(trimmed) }) } pub(crate) fn prompt_keep_opt(label: &str, current: Option<&str>) -> Result> { let display = current.unwrap_or("(none)"); eprint!("{label} [{display}]: "); std::io::Write::flush(&mut std::io::stderr())?; let mut s = String::new(); std::io::stdin().read_line(&mut s)?; let trimmed = s.trim().to_string(); Ok(if trimmed.is_empty() { None } else { Some(trimmed) }) } pub(crate) fn prompt_yesno(label: &str) -> Result { eprint!("{label} [y/N] "); std::io::Write::flush(&mut std::io::stderr())?; let mut s = String::new(); std::io::stdin().read_line(&mut s)?; Ok(matches!(s.trim().to_ascii_lowercase().as_str(), "y" | "yes")) }