From 65eaa0d557adfc2f4f48376177f3ecffc0c162b6 Mon Sep 17 00:00:00 2001 From: Mike Lloyd Date: Mon, 3 Jun 2024 20:21:00 -0700 Subject: [PATCH] Add fuzzy select --- Cargo.lock | 51 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 ++- src/command/handlers.rs | 40 +++++++++++++++++++++++++++++--- src/main.rs | 8 +++++++ src/types/secret.rs | 2 +- src/types/session.rs | 5 +--- 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 885f3dd..397e5d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.31" @@ -447,6 +453,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" +[[package]] +name = "ctrlc" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +dependencies = [ + "nix 0.28.0", + "windows-sys 0.52.0", +] + [[package]] name = "der" version = "0.7.8" @@ -474,6 +490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" dependencies = [ "console", + "fuzzy-matcher", "shell-words", "tempfile", "thiserror", @@ -688,6 +705,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -983,6 +1009,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1339,7 +1377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ff60778f96fb5a48adbe421d21bf6578ed58c0872d712e7e08593c195adff8" dependencies = [ "comma", - "nix", + "nix 0.25.1", "regex", "tempfile", "thiserror", @@ -1375,6 +1413,7 @@ dependencies = [ "clap", "clap_complete", "colored_json", + "ctrlc", "dialoguer", "duration-str", "orion", @@ -1892,6 +1931,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" diff --git a/Cargo.toml b/Cargo.toml index 77f16db..08a983f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ sqlx = { version = "0.7.4", features = [ tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } orion = { version = "0.17.6", features = ["serde"] } xdg = "2.5.2" -dialoguer = "0.11.0" +dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } serde = "1.0.200" colored_json = "4.1.0" uuid = { version = "1.8.0", features = ["v4"] } @@ -35,6 +35,7 @@ time = "0.3.36" clap_complete = "4.5.2" duration-str = "0.10.0" toml = "0.8.12" +ctrlc = "3.4.4" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/src/command/handlers.rs b/src/command/handlers.rs index fa24ba4..1f0cd3e 100644 --- a/src/command/handlers.rs +++ b/src/command/handlers.rs @@ -7,6 +7,8 @@ use std::{ use anyhow::{bail, Context, Result}; use clap::CommandFactory; use clap_complete::{generate, shells}; +use dialoguer::theme::ColorfulTheme; +use sqlx::SqlitePool; use tabled::{ settings::{style::BorderColor, Color, Style}, Table, Tabled, @@ -100,7 +102,7 @@ pub async fn handle_create( pub async fn handle_get(config_dir: &Path, name: String, json: bool) -> Result<()> { let app = App::new(config_dir, true).await?; - let sec = Secret::get(&app.db, &name).await?; + let sec = select_secret(&app.db, &name).await?; let cleartext = sec.to_cleartext(&app.master_key)?; if json { @@ -115,7 +117,7 @@ pub async fn handle_get(config_dir: &Path, name: String, json: bool) -> Result<( pub async fn handle_edit(config_dir: &Path, name: String, description: bool) -> Result<()> { let app = App::new(config_dir, true).await?; - let mut sec = Secret::get(&app.db, &name).await?; + let mut sec = select_secret(&app.db, &name).await?; if description { let old_desc = sec.description.unwrap_or_default(); @@ -157,7 +159,7 @@ pub async fn handle_edit(config_dir: &Path, name: String, description: bool) -> pub async fn handle_delete(config_dir: &Path, name: String) -> Result<()> { let app = App::new(config_dir, true).await?; - let sec = Secret::get(&app.db, &name).await?; + let sec = select_secret(&app.db, &name).await?; let prompt_msg = format!("Delete secret '{}'?", sec.name); let confirm = prompt::confirm(&prompt_msg, false)?; @@ -312,3 +314,35 @@ pub fn handle_generate_completions(shell: ShellType) -> Result<()> { Ok(()) } + +/// Prompts the user to select a secret if multiple secrets match the inputted name +pub async fn select_secret(db: &SqlitePool, search_str: &str) -> Result { + let mut secrets = vec![]; + for secret in Secret::get_all(db).await? { + if secret.name == search_str { + return Ok(secret); + } + if secret + .name + .to_lowercase() + .contains(&search_str.to_lowercase()) + { + secrets.push(secret) + } + } + + if secrets.len() == 1 { + Ok(secrets.pop().unwrap()) + } else { + let items: Vec<&str> = secrets.iter().map(|s| s.name.as_str()).collect(); + let selection = dialoguer::FuzzySelect::with_theme(&ColorfulTheme::default()) + .items(&items) + .with_prompt("Select secret") + .vim_mode(true) + .interact()?; + Ok(secrets + .get(selection) + .expect("Selected option should be in bounds") + .clone()) + } +} diff --git a/src/main.rs b/src/main.rs index 818a910..61545a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use anyhow::{bail, Result}; use clap::Parser; +use dialoguer::console::Term; use rudric::{ command::{ cli::{Cli, Command}, @@ -13,6 +14,9 @@ use rudric::{ #[tokio::main] async fn main() -> Result<()> { + // Ignore SIGINT so we can handle it ourselves + ctrlc::set_handler(move || {}).expect("Error setting Ctrl-C handler"); + let cli = Cli::parse(); let config_dir = match cli.config_dir { @@ -44,4 +48,8 @@ async fn main() -> Result<()> { Command::ChangePassword => handle_change_password(&config_dir).await, Command::GenerateCompletions { shell } => handle_generate_completions(shell), } + .map_err(|e| { + let _ = Term::stdout().show_cursor(); + e + }) } diff --git a/src/types/secret.rs b/src/types/secret.rs index 42f9353..c77734f 100644 --- a/src/types/secret.rs +++ b/src/types/secret.rs @@ -9,7 +9,7 @@ use crate::crypto; pub const SECRET_NOT_FOUND: &str = "Secret not found"; -#[derive(Debug, FromRow)] +#[derive(Debug, FromRow, Clone)] pub struct Secret { pub id: Option, pub name: String, diff --git a/src/types/session.rs b/src/types/session.rs index a728c03..0ae8c61 100644 --- a/src/types/session.rs +++ b/src/types/session.rs @@ -3,7 +3,7 @@ use std::{env, fmt::Display}; use anyhow::{bail, Context, Result}; use base64::{engine::general_purpose::STANDARD_NO_PAD as b64, Engine}; use orion::aead::SecretKey; -use sqlx::{sqlite::SqliteRow, Execute, FromRow, Row, SqlitePool}; +use sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}; use time::OffsetDateTime; use uuid::Uuid; @@ -98,9 +98,6 @@ impl SessionKey { .await .context("Failed to delete expired session key")?; - println!("{}",sqlx::query!("delete from session_keys where expire_time < ?", now).sql()); - - Ok(()) } }