refactor(cli): move prompt helpers into prompt.rs

This commit is contained in:
adlee-was-taken
2026-05-06 18:20:33 -04:00
parent 02e05f7a05
commit 272b6a3845
2 changed files with 64 additions and 60 deletions

View File

@@ -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<String> {
/// Check for test item secret override (debug builds only; stripped from release).
#[cfg(debug_assertions)]
fn test_item_secret_override() -> Option<String> {
pub(crate) fn test_item_secret_override() -> Option<String> {
std::env::var("RELICARIO_TEST_ITEM_SECRET").ok()
}
#[cfg(not(debug_assertions))]
fn test_item_secret_override() -> Option<String> {
pub(crate) fn test_item_secret_override() -> Option<String> {
None
}
/// Check for test backup passphrase override (debug builds only; stripped from release).
#[cfg(debug_assertions)]
fn test_backup_passphrase_override() -> Option<String> {
pub(crate) fn test_backup_passphrase_override() -> Option<String> {
std::env::var("RELICARIO_TEST_BACKUP_PASSPHRASE").ok()
}
#[cfg(not(debug_assertions))]
fn test_backup_passphrase_override() -> Option<String> {
pub(crate) fn test_backup_passphrase_override() -> Option<String> {
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<String> {
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<String> {
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<Option<String>> {
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<relicario_core::MonthYear> {
// 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<Option<String>> {
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<Option<String>> {
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<bool> {
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<relicario_core::FieldId, Vec<relicario_core::item::FieldHistoryEntry>>,
synthetic_key: &str,

View File

@@ -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<String> {
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<String> {
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<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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<bool> {
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"))
}