Files

150 lines
4.2 KiB
Rust

//! Interactive terminal adapter for the password manager.
//!
//! Clap owns command parsing and help text. This module maps parsed commands to
//! [`PasswordManager`] operations while leaving SQL and `λSQL` concerns in the
//! domain module.
use std::io::{self, Write as _};
use clap::{Parser, Subcommand};
use logos_sql::TxId;
use crate::{
AppResult,
passwords::{Credential, CredentialSummary, PasswordManager},
};
#[derive(Debug, Parser)]
#[command(name = "password-manager")]
struct Input {
#[command(subcommand)]
command: Command,
}
/// A command accepted by the running password manager.
#[derive(Debug, Subcommand)]
enum Command {
/// Adds a credential.
Add {
label: String,
account: String,
#[arg(required = true, num_args = 1..)]
password: Vec<String>,
},
/// Replaces the password stored under a label.
Update {
label: String,
#[arg(required = true, num_args = 1..)]
password: Vec<String>,
},
/// Shows one credential from the local database.
Show { label: String },
/// Removes one credential.
Remove { label: String },
/// Lists credential labels and accounts.
List,
/// Stops the application.
#[command(alias = "quit")]
Exit,
}
impl Command {
/// Parses one line entered at the password-manager prompt.
fn parse(input: &str) -> Result<Self, clap::Error> {
let args = std::iter::once("password-manager").chain(input.split_whitespace());
Input::try_parse_from(args).map(|input| input.command)
}
}
/// Reads commands from the terminal until the user exits or input closes.
pub async fn run(manager: &PasswordManager) -> AppResult<()> {
println!("Password manager is running. Enter `help` to list commands.");
println!("WARNING: passwords are replicated in plaintext; do not enter real credentials.");
while let Some(input) = read_input().await? {
if input.trim().is_empty() {
continue;
}
let command = match Command::parse(&input) {
Ok(command) => command,
Err(error) => {
error.print()?;
continue;
}
};
if matches!(command, Command::Exit) {
break;
}
match handle_command(manager, command).await {
Ok(Some(tx_id)) => {
println!("committed locally as {tx_id}");
}
Ok(None) => {}
Err(error) => eprintln!("error: {error}"),
}
}
Ok(())
}
async fn handle_command(manager: &PasswordManager, command: Command) -> AppResult<Option<TxId>> {
let tx_id = match command {
Command::Add {
label,
account,
password,
} => manager.add(label, account, password.join(" ")).await?,
Command::Update { label, password } => {
manager.update_password(label, password.join(" ")).await?
}
Command::Show { label } => {
print_credential(manager.credential(&label)?);
return Ok(None);
}
Command::Remove { label } => manager.remove(label).await?,
Command::List => {
print_credentials(manager.credentials()?);
return Ok(None);
}
Command::Exit => return Ok(None),
};
Ok(Some(tx_id))
}
/// Reads terminal input without blocking the runtime that drives `λSQL`.
async fn read_input() -> AppResult<Option<String>> {
let input = tokio::task::spawn_blocking(|| -> io::Result<Option<String>> {
print!("password-manager> ");
io::stdout().flush()?;
let mut input = String::new();
let bytes_read = io::stdin().read_line(&mut input)?;
Ok((bytes_read > 0).then_some(input))
})
.await??;
Ok(input)
}
fn print_credential(credential: Option<Credential>) {
let Some(credential) = credential else {
println!("credential not found");
return;
};
println!("{}", credential.label);
println!(" account: {}", credential.account);
println!(" password: {}", credential.password);
}
fn print_credentials(credentials: Vec<CredentialSummary>) {
for credential in credentials {
println!("{} ({})", credential.label, credential.account);
}
}