From 272b6a3845ff38120186e91bc4456e9924c286e6 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Wed, 6 May 2026 18:20:33 -0400 Subject: [PATCH] refactor(cli): move prompt helpers into prompt.rs --- crates/relicario-cli/src/main.rs | 66 +++--------------------------- crates/relicario-cli/src/prompt.rs | 58 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 141e959..606d0c5 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -16,6 +16,8 @@ use anyhow::{bail, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell}; +use crate::prompt::{prompt, prompt_keep, prompt_keep_opt, prompt_optional, prompt_secret, prompt_yesno}; + #[derive(Parser)] #[command( name = "relicario", @@ -487,34 +489,24 @@ pub(crate) fn test_passphrase_override() -> Option { /// Check for test item secret override (debug builds only; stripped from release). #[cfg(debug_assertions)] -fn test_item_secret_override() -> Option { +pub(crate) fn test_item_secret_override() -> Option { std::env::var("RELICARIO_TEST_ITEM_SECRET").ok() } #[cfg(not(debug_assertions))] -fn test_item_secret_override() -> Option { +pub(crate) fn test_item_secret_override() -> Option { None } /// Check for test backup passphrase override (debug builds only; stripped from release). #[cfg(debug_assertions)] -fn test_backup_passphrase_override() -> Option { +pub(crate) fn test_backup_passphrase_override() -> Option { std::env::var("RELICARIO_TEST_BACKUP_PASSPHRASE").ok() } #[cfg(not(debug_assertions))] -fn test_backup_passphrase_override() -> Option { +pub(crate) fn test_backup_passphrase_override() -> Option { None } -/// `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). -fn prompt_secret(label: &str) -> Result { - if let Some(s) = test_item_secret_override() { - return Ok(s); - } - rpassword::prompt_password(label).map_err(Into::into) -} - fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { use std::fs; use rand::{rngs::OsRng, RngCore}; @@ -923,25 +915,6 @@ fn build_totp_item( Ok(item) } -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) -} - -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) }) -} - fn parse_month_year(s: &str) -> Result { // Accepts MM/YYYY or MM-YYYY or MM/YY. let (m_str, y_str) = s.split_once(['/', '-']) @@ -1310,33 +1283,6 @@ fn edit_totp(t: &mut relicario_core::item_types::TotpCore, history: &mut FieldHi Ok(()) } -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) }) -} - -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) }) -} - -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")) -} - fn push_history( history: &mut std::collections::HashMap>, synthetic_key: &str, diff --git a/crates/relicario-cli/src/prompt.rs b/crates/relicario-cli/src/prompt.rs index 3bd4d91..8e21f90 100644 --- a/crates/relicario-cli/src/prompt.rs +++ b/crates/relicario-cli/src/prompt.rs @@ -5,3 +5,61 @@ //! 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")) +}