refactor(cli): move prompt helpers into prompt.rs
This commit is contained in:
@@ -16,6 +16,8 @@ use anyhow::{bail, Context, Result};
|
|||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{CommandFactory, Parser, Subcommand};
|
||||||
use clap_complete::{generate, Shell};
|
use clap_complete::{generate, Shell};
|
||||||
|
|
||||||
|
use crate::prompt::{prompt, prompt_keep, prompt_keep_opt, prompt_optional, prompt_secret, prompt_yesno};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "relicario",
|
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).
|
/// Check for test item secret override (debug builds only; stripped from release).
|
||||||
#[cfg(debug_assertions)]
|
#[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()
|
std::env::var("RELICARIO_TEST_ITEM_SECRET").ok()
|
||||||
}
|
}
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
fn test_item_secret_override() -> Option<String> {
|
pub(crate) fn test_item_secret_override() -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for test backup passphrase override (debug builds only; stripped from release).
|
/// Check for test backup passphrase override (debug builds only; stripped from release).
|
||||||
#[cfg(debug_assertions)]
|
#[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()
|
std::env::var("RELICARIO_TEST_BACKUP_PASSPHRASE").ok()
|
||||||
}
|
}
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
fn test_backup_passphrase_override() -> Option<String> {
|
pub(crate) fn test_backup_passphrase_override() -> Option<String> {
|
||||||
None
|
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<()> {
|
fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use rand::{rngs::OsRng, RngCore};
|
use rand::{rngs::OsRng, RngCore};
|
||||||
@@ -923,25 +915,6 @@ fn build_totp_item(
|
|||||||
Ok(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> {
|
fn parse_month_year(s: &str) -> Result<relicario_core::MonthYear> {
|
||||||
// Accepts MM/YYYY or MM-YYYY or MM/YY.
|
// Accepts MM/YYYY or MM-YYYY or MM/YY.
|
||||||
let (m_str, y_str) = s.split_once(['/', '-'])
|
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(())
|
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(
|
fn push_history(
|
||||||
history: &mut std::collections::HashMap<relicario_core::FieldId, Vec<relicario_core::item::FieldHistoryEntry>>,
|
history: &mut std::collections::HashMap<relicario_core::FieldId, Vec<relicario_core::item::FieldHistoryEntry>>,
|
||||||
synthetic_key: &str,
|
synthetic_key: &str,
|
||||||
|
|||||||
@@ -5,3 +5,61 @@
|
|||||||
//! used by the edit handlers to keep current values when the user hits enter
|
//! 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`
|
//! at a blank prompt. `prompt_secret` honours `RELICARIO_TEST_ITEM_SECRET`
|
||||||
//! so integration tests (which don't have a TTY) can inject secrets.
|
//! 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"))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user