From 7c86e3d496d8e300a4b5818133f894726edb1b6f Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 26 Aug 2026 12:35:59 -0700 Subject: [PATCH] feat: entry search --- src/app/mod.rs | 201 +++++++++++++++++++++++++++++-------------------- src/db.rs | 11 +++ 2 files changed, 130 insertions(+), 82 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index fb70d3c..83990d8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -7,7 +7,7 @@ use iced::{ }; use rfd::{AsyncFileDialog, FileHandle}; -use crate::db; +use crate::db::{self, EntrySearchExt}; type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>; @@ -22,13 +22,15 @@ enum Message { OpenDb, DbOpened(Result), - SelectionChanged(usize, db::BasicField), + ChangeSelection(usize, db::BasicField), SelectionLeft, SelectionDown, SelectionUp, SelectionRight, + ChangeQuery(String), + Copy, FocusNext, @@ -42,9 +44,11 @@ struct State { db: Option, + db_loaded: bool, db_view: Vec, selected_entry: usize, selected_field: db::BasicField, + query: String, password: String, @@ -105,14 +109,15 @@ impl State { .iter_all_entries() .map(|entry_ref| entry_ref.clone()) .collect(); - self.db = Some(db) + self.db = Some(db); + self.db_loaded = true; } Err(e) => self.error = e.into(), } self.status = "".into(); } - Message::SelectionChanged(i, field) => { + Message::ChangeSelection(i, field) => { self.selected_entry = i; self.selected_field = field; return Task::done(Message::Copy); @@ -132,17 +137,36 @@ impl State { } Message::SelectionRight => self.selected_field.next(), + Message::ChangeQuery(str) => { + self.query = str; + self.db_view = self + .db + .as_ref() + .unwrap() + .iter_all_entries() + .filter_map(|entry_ref| { + entry_ref + .contains(&self.query) + .then(|| entry_ref.to_owned()) + }) + .collect(); + } + Message::Copy => { - let entry_ref = &self.db_view[self.selected_entry]; - if let Some(str) = match self.selected_field { - db::BasicField::Title => entry_ref.get_password(), - db::BasicField::Username => entry_ref.get_username(), - db::BasicField::Password => entry_ref.get_password(), - db::BasicField::Url => entry_ref.get_url(), - } { - return clipboard::write(str.to_owned()); + if let Some(entry_ref) = &self.db_view.get(self.selected_entry) { + if let Some(str) = match self.selected_field { + db::BasicField::Title => entry_ref.get_password(), + db::BasicField::Username => entry_ref.get_username(), + db::BasicField::Password => entry_ref.get_password(), + db::BasicField::Url => entry_ref.get_url(), + } { + self.error = "".into(); + return clipboard::write(str.to_owned()); + } else { + self.error = "nothing to copy!".into(); + } } else { - self.error = "nothing to copy!".into(); + self.error = "nothing selected!".into(); } } @@ -153,75 +177,88 @@ impl State { Task::none() } fn view(&self) -> Element<'_> { - let main = if !self.db_view.is_empty() { - widget::scrollable( - widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| { - [ - if let Some(title) = entry.get_title() { - let field = db::BasicField::Title; - widget::button(widget::text(title)) - .style( - if i == self.selected_entry && field == self.selected_field { - widget::button::primary - } else { - widget::button::subtle - }, - ) - .on_press(Message::SelectionChanged(i, field)) - .into() - } else { - widget::space().into() - }, - if let Some(title) = entry.get_username() { - let field = db::BasicField::Username; - widget::button(widget::text(title)) - .style( - if i == self.selected_entry && field == self.selected_field { - widget::button::primary - } else { - widget::button::subtle - }, - ) - .on_press(Message::SelectionChanged(i, field)) - .into() - } else { - widget::space().into() - }, - if entry.get_password().is_some() { - let field = db::BasicField::Password; - widget::button(widget::text("[password hidden]")) - .style( - if i == self.selected_entry && field == self.selected_field { - widget::button::primary - } else { - widget::button::subtle - }, - ) - .on_press(Message::SelectionChanged(i, field)) - .into() - } else { - widget::space().into() - }, - if let Some(title) = entry.get_url() { - let field = db::BasicField::Url; - widget::button(widget::text(title)) - .style( - if i == self.selected_entry && field == self.selected_field { - widget::button::primary - } else { - widget::button::subtle - }, - ) - .on_press(Message::SelectionChanged(i, field)) - .into() - } else { - widget::space().into() - }, - ] - })) - .height(Length::Shrink) - .columns(4), - ) + let main = if self.db_loaded { + widget::column([ + widget::text_input("search query", &self.query) + .on_input(Message::ChangeQuery) + .into(), + widget::scrollable( + widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| { + [ + if let Some(title) = entry.get_title() { + let field = db::BasicField::Title; + widget::button(widget::text(title)) + .style( + if i == self.selected_entry && field == self.selected_field + { + widget::button::primary + } else { + widget::button::subtle + }, + ) + .on_press(Message::ChangeSelection(i, field)) + .into() + } else { + widget::space().into() + }, + if let Some(title) = entry.get_username() { + let field = db::BasicField::Username; + widget::button(widget::text(title)) + .style( + if i == self.selected_entry && field == self.selected_field + { + widget::button::primary + } else { + widget::button::subtle + }, + ) + .on_press(Message::ChangeSelection(i, field)) + .into() + } else { + widget::space().into() + }, + if entry.get_password().is_some() { + let field = db::BasicField::Password; + widget::button(widget::text("[password hidden]")) + .style( + if i == self.selected_entry && field == self.selected_field + { + widget::button::primary + } else { + widget::button::subtle + }, + ) + .on_press(Message::ChangeSelection(i, field)) + .into() + } else { + widget::space().into() + }, + if let Some(title) = entry.get_url() { + let field = db::BasicField::Url; + widget::button(widget::text(title)) + .style( + if i == self.selected_entry && field == self.selected_field + { + widget::button::primary + } else { + widget::button::subtle + }, + ) + .on_press(Message::ChangeSelection(i, field)) + .into() + } else { + widget::space().into() + }, + ] + })) + .height(Length::Shrink) + .columns(4), + ) + // I don't know why this needs to be here, + // but without it the search bar isn't shown + .height(Length::Fill) + .into(), + ]) .into() } else { widget::container( diff --git a/src/db.rs b/src/db.rs index 2e5ce30..104f5cf 100644 --- a/src/db.rs +++ b/src/db.rs @@ -27,6 +27,17 @@ impl BasicField { } } +pub trait EntrySearchExt { + fn contains(&self, s: impl AsRef) -> bool; +} +impl EntrySearchExt for keepass::db::EntryRef<'_> { + fn contains(&self, s: impl AsRef) -> bool { + let str = s.as_ref(); + self.get_title().is_some_and(|title| title.contains(str)) + || self.get_url().is_some_and(|url| url.contains(str)) + } +} + pub fn get_last_opened() -> io::Result { Ok(fs::read_to_string( dirs::data_dir()