//! 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, }, /// Replaces the password stored under a label. Update { label: String, #[arg(required = true, num_args = 1..)] password: Vec, }, /// 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 { 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> { 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> { let input = tokio::task::spawn_blocking(|| -> io::Result> { 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) { 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) { for credential in credentials { println!("{} ({})", credential.label, credential.account); } }