Compare commits
2 Commits
feature/cl
...
feature/cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e791e4853 | ||
|
|
bfec232f11 |
@@ -11,7 +11,7 @@ use anyhow::{Context, Result};
|
||||
|
||||
use crate::AddKind;
|
||||
use crate::parse::{base32_decode_lenient, guess_mime, parse_month_year};
|
||||
use crate::prompt::{prompt, prompt_optional, prompt_secret};
|
||||
use crate::prompt::{prompt, prompt_optional, prompt_or_flag, prompt_or_flag_optional, prompt_secret};
|
||||
|
||||
pub fn cmd_add(kind: AddKind) -> Result<()> {
|
||||
let vault = crate::session::UnlockedVault::unlock_interactive()?;
|
||||
@@ -69,9 +69,9 @@ fn build_login_item(
|
||||
use relicario_core::{Item, ItemCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let username = username.or_else(|| prompt_optional("Username").ok().flatten());
|
||||
let url = url.or_else(|| prompt_optional("URL").ok().flatten());
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let username = prompt_or_flag_optional(username, "Username", |s| Ok(s.to_string()))?;
|
||||
let url = prompt_or_flag_optional(url, "URL", |s| Ok(s.to_string()))?;
|
||||
let parsed_url = match url {
|
||||
Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
|
||||
None => None,
|
||||
@@ -115,7 +115,7 @@ fn build_secure_note_item(
|
||||
use relicario_core::{Item, ItemCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let body = if body_prompt {
|
||||
eprintln!("Enter note body; end with Ctrl-D on a blank line:");
|
||||
let mut s = String::new();
|
||||
@@ -144,7 +144,7 @@ fn build_identity_item(
|
||||
use relicario_core::item_types::IdentityCore;
|
||||
use relicario_core::{Item, ItemCore};
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let dob = match date_of_birth {
|
||||
Some(s) => Some(chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d")
|
||||
.with_context(|| format!("invalid date {s} (expected YYYY-MM-DD)"))?),
|
||||
@@ -170,7 +170,7 @@ fn build_card_item(
|
||||
use relicario_core::{Item, ItemCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let number = Zeroizing::new(prompt_secret("Card number: ")?);
|
||||
let cvv = Zeroizing::new(prompt_secret("CVV (blank to skip): ")?);
|
||||
let cvv = if cvv.is_empty() { None } else { Some(cvv) };
|
||||
@@ -209,7 +209,7 @@ fn build_key_item(
|
||||
use relicario_core::{Item, ItemCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
eprintln!("Paste key material; end with Ctrl-D on a blank line:");
|
||||
let mut key_material = String::new();
|
||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut key_material)?;
|
||||
@@ -236,7 +236,7 @@ fn build_document_item(
|
||||
use relicario_core::{encrypt_attachment, AttachmentRef, Item, ItemCore};
|
||||
use std::fs;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let bytes = fs::read(&file)
|
||||
.with_context(|| format!("failed to read {}", file.display()))?;
|
||||
let caps = vault.load_settings()?.attachment_caps;
|
||||
@@ -285,7 +285,7 @@ fn build_totp_item(
|
||||
use relicario_core::{Item, ItemCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
|
||||
let title = prompt_or_flag(title, "Title", |s| Ok(s.to_string()))?;
|
||||
let secret_b32 = match secret {
|
||||
Some(s) => s,
|
||||
None => prompt_secret("TOTP secret (base32): ")?,
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
//! 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.
|
||||
//! `prompt_or_flag` and `prompt_or_flag_optional` thread a CLI-flag value
|
||||
//! through the same path so command handlers can use one call site whether
|
||||
//! the value came from the command line or from an interactive prompt.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::io::BufRead;
|
||||
|
||||
/// `rpassword::prompt_password` wrapper that honours `RELICARIO_TEST_ITEM_SECRET`
|
||||
/// for integration-test use (rpassword reads /dev/tty by default, which is
|
||||
@@ -18,25 +22,37 @@ pub(crate) fn prompt_secret(label: &str) -> Result<String> {
|
||||
rpassword::prompt_password(label).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt(label: &str) -> Result<String> {
|
||||
fn read_required_line<R: BufRead>(reader: &mut R, 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)?;
|
||||
reader.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>> {
|
||||
fn read_optional_line<R: BufRead>(reader: &mut R, 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)?;
|
||||
reader.read_line(&mut s)?;
|
||||
let trimmed = s.trim().to_string();
|
||||
Ok(if trimmed.is_empty() { None } else { Some(trimmed) })
|
||||
}
|
||||
|
||||
pub(crate) fn prompt(label: &str) -> Result<String> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut reader = std::io::BufReader::new(stdin.lock());
|
||||
read_required_line(&mut reader, label)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_optional(label: &str) -> Result<Option<String>> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut reader = std::io::BufReader::new(stdin.lock());
|
||||
read_optional_line(&mut reader, label)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_keep(label: &str, current: &str) -> Result<Option<String>> {
|
||||
eprint!("{label} [{current}]: ");
|
||||
std::io::Write::flush(&mut std::io::stderr())?;
|
||||
@@ -63,3 +79,117 @@ pub(crate) fn prompt_yesno(label: &str) -> Result<bool> {
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
Ok(matches!(s.trim().to_ascii_lowercase().as_str(), "y" | "yes"))
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_or_flag<T>(
|
||||
flag: Option<T>,
|
||||
label: &str,
|
||||
parser: impl FnOnce(&str) -> Result<T>,
|
||||
) -> Result<T> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut reader = std::io::BufReader::new(stdin.lock());
|
||||
prompt_or_flag_with_reader(flag, label, parser, &mut reader)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_or_flag_optional<T>(
|
||||
flag: Option<T>,
|
||||
label: &str,
|
||||
parser: impl FnOnce(&str) -> Result<T>,
|
||||
) -> Result<Option<T>> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut reader = std::io::BufReader::new(stdin.lock());
|
||||
prompt_or_flag_optional_with_reader(flag, label, parser, &mut reader)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_or_flag_with_reader<T, R: BufRead>(
|
||||
flag: Option<T>,
|
||||
label: &str,
|
||||
parser: impl FnOnce(&str) -> Result<T>,
|
||||
reader: &mut R,
|
||||
) -> Result<T> {
|
||||
if let Some(t) = flag {
|
||||
return Ok(t);
|
||||
}
|
||||
let line = read_required_line(reader, label)?;
|
||||
parser(&line)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_or_flag_optional_with_reader<T, R: BufRead>(
|
||||
flag: Option<T>,
|
||||
label: &str,
|
||||
parser: impl FnOnce(&str) -> Result<T>,
|
||||
reader: &mut R,
|
||||
) -> Result<Option<T>> {
|
||||
if let Some(t) = flag {
|
||||
return Ok(Some(t));
|
||||
}
|
||||
match read_optional_line(reader, label)? {
|
||||
None => Ok(None),
|
||||
Some(line) => parser(&line).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn prompt_or_flag_uses_flag_value_when_some() {
|
||||
let mut reader = Cursor::new(Vec::<u8>::new());
|
||||
let got = prompt_or_flag_with_reader::<String, _>(
|
||||
Some("from-flag".to_string()),
|
||||
"Title",
|
||||
|_| panic!("parser must not run when flag is Some"),
|
||||
&mut reader,
|
||||
).expect("flag value path should succeed");
|
||||
assert_eq!(got, "from-flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_or_flag_prompts_when_none() {
|
||||
let mut reader = Cursor::new(b"prompted\n".to_vec());
|
||||
let got = prompt_or_flag_with_reader::<String, _>(
|
||||
None,
|
||||
"Title",
|
||||
|s| Ok(s.to_string()),
|
||||
&mut reader,
|
||||
).expect("prompt path should succeed");
|
||||
assert_eq!(got, "prompted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_or_flag_optional_returns_some_from_flag_without_reading() {
|
||||
let mut reader = Cursor::new(Vec::<u8>::new());
|
||||
let got = prompt_or_flag_optional_with_reader::<String, _>(
|
||||
Some("flag-val".to_string()),
|
||||
"URL",
|
||||
|_| panic!("parser must not run when flag is Some"),
|
||||
&mut reader,
|
||||
).expect("flag value path should succeed");
|
||||
assert_eq!(got, Some("flag-val".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_or_flag_optional_prompts_and_blank_yields_none() {
|
||||
let mut reader = Cursor::new(b"\n".to_vec());
|
||||
let got = prompt_or_flag_optional_with_reader::<String, _>(
|
||||
None,
|
||||
"URL",
|
||||
|_| panic!("parser must not run on blank input"),
|
||||
&mut reader,
|
||||
).expect("blank prompt should succeed with None");
|
||||
assert_eq!(got, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_or_flag_optional_prompts_and_value_runs_parser() {
|
||||
let mut reader = Cursor::new(b" 42 \n".to_vec());
|
||||
let got = prompt_or_flag_optional_with_reader::<u32, _>(
|
||||
None,
|
||||
"Number",
|
||||
|s| s.parse::<u32>().map_err(Into::into),
|
||||
&mut reader,
|
||||
).expect("value should parse");
|
||||
assert_eq!(got, Some(42));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user