// Lprs - A local CLI vaults manager. For human and machine use // Copyright (C) 2024 Awiteb // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use clap::Args; use inquire::{InquireError, Select}; use crate::{ vault::{cipher, Vaults}, LprsCommand, LprsError, LprsResult, RESERVED_FIELD_PREFIX, }; #[derive(Debug, Args)] /// List command, used to list the vaults and search pub struct List { /// Filter the select list #[arg(short, long, value_name = "TEXT")] filter: Option, /// Enable regex when use `--filter` option #[arg(short, long)] regex: bool, /// Returns the output as `json` list of vaults #[arg(long)] json: bool, } impl LprsCommand for List { fn run(self, mut vault_manager: Vaults) -> LprsResult<()> { if vault_manager.vaults.is_empty() { return Err(LprsError::Other( "Looks like there is no vaults to list".to_owned(), )); } let pattern = if self.regex { self.filter .expect("Is required if the `regex` option is `true`") } else { format!( ".*{}.*", regex::escape(self.filter.as_deref().unwrap_or_default()) ) }; log::debug!("Listing vaults filtered by: {pattern}"); let re = regex::Regex::new(&pattern.to_lowercase())?; let vaults_list = vault_manager.vaults.iter().enumerate().filter(|(_, v)| { re.is_match(&v.name.to_lowercase()) || v.username .as_deref() .is_some_and(|u| re.is_match(&u.to_lowercase())) || v.service .as_deref() .is_some_and(|s| re.is_match(&s.to_lowercase())) || v.note .as_deref() .is_some_and(|n| re.is_match(&n.to_lowercase())) }); if self.json { print!( "{}", serde_json::to_string(&vaults_list.map(|(_, v)| v).collect::>())? ) } else { let vault_idx = Select::new( "Select a vault to view:", vaults_list .map(|(idx, v)| format!("{}) {}", idx + 1, v.list_name())) .collect(), ) .with_formatter(&|s| { s.value .split_once(") ") .expect("The bracket are hard coded above") .1 .to_owned() }) .prompt() .map_err(|err| { if matches!(err, InquireError::InvalidConfiguration(_)) { return LprsError::Other("There is no result match your filter".to_owned()); } err.into() })? .split_once(')') .expect("The bracket are hard coded above") .0 .parse::() .unwrap_or_default(); log::debug!("The user selected the vault at index: {vault_idx}"); let vault = vault_manager .vaults .get_mut(vault_idx - 1) .expect("The index is correct"); if let Some(ref totp_secret) = vault.totp_secret { let (code, remaining) = cipher::totp_now(totp_secret, &vault.totp_hash)?; vault.custom_fields.insert( format!("{RESERVED_FIELD_PREFIX}TOTP Code"), format!("{code} ({remaining}s remaining)"), ); } println!("{vault}",); } Ok(()) } fn validate_args(&self) -> LprsResult<()> { if self.regex && self.filter.is_none() { return Err(LprsError::Other( "You cannot use the `--regex` flag if you did not use the search option".to_owned(), )); } Ok(()) } }