feat: entry search

This commit is contained in:
electria 2026-08-26 12:35:59 -07:00
commit 7c86e3d496
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs
2 changed files with 130 additions and 82 deletions

View file

@ -7,7 +7,7 @@ use iced::{
}; };
use rfd::{AsyncFileDialog, FileHandle}; use rfd::{AsyncFileDialog, FileHandle};
use crate::db; use crate::db::{self, EntrySearchExt};
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>; type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
@ -22,13 +22,15 @@ enum Message {
OpenDb, OpenDb,
DbOpened(Result<keepass::Database, String>), DbOpened(Result<keepass::Database, String>),
SelectionChanged(usize, db::BasicField), ChangeSelection(usize, db::BasicField),
SelectionLeft, SelectionLeft,
SelectionDown, SelectionDown,
SelectionUp, SelectionUp,
SelectionRight, SelectionRight,
ChangeQuery(String),
Copy, Copy,
FocusNext, FocusNext,
@ -42,9 +44,11 @@ struct State {
db: Option<keepass::Database>, db: Option<keepass::Database>,
db_loaded: bool,
db_view: Vec<keepass::db::Entry>, db_view: Vec<keepass::db::Entry>,
selected_entry: usize, selected_entry: usize,
selected_field: db::BasicField, selected_field: db::BasicField,
query: String,
password: String, password: String,
@ -105,14 +109,15 @@ impl State {
.iter_all_entries() .iter_all_entries()
.map(|entry_ref| entry_ref.clone()) .map(|entry_ref| entry_ref.clone())
.collect(); .collect();
self.db = Some(db) self.db = Some(db);
self.db_loaded = true;
} }
Err(e) => self.error = e.into(), Err(e) => self.error = e.into(),
} }
self.status = "".into(); self.status = "".into();
} }
Message::SelectionChanged(i, field) => { Message::ChangeSelection(i, field) => {
self.selected_entry = i; self.selected_entry = i;
self.selected_field = field; self.selected_field = field;
return Task::done(Message::Copy); return Task::done(Message::Copy);
@ -132,18 +137,37 @@ impl State {
} }
Message::SelectionRight => self.selected_field.next(), 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 => { Message::Copy => {
let entry_ref = &self.db_view[self.selected_entry]; if let Some(entry_ref) = &self.db_view.get(self.selected_entry) {
if let Some(str) = match self.selected_field { if let Some(str) = match self.selected_field {
db::BasicField::Title => entry_ref.get_password(), db::BasicField::Title => entry_ref.get_password(),
db::BasicField::Username => entry_ref.get_username(), db::BasicField::Username => entry_ref.get_username(),
db::BasicField::Password => entry_ref.get_password(), db::BasicField::Password => entry_ref.get_password(),
db::BasicField::Url => entry_ref.get_url(), db::BasicField::Url => entry_ref.get_url(),
} { } {
self.error = "".into();
return clipboard::write(str.to_owned()); return clipboard::write(str.to_owned());
} else { } else {
self.error = "nothing to copy!".into(); self.error = "nothing to copy!".into();
} }
} else {
self.error = "nothing selected!".into();
}
} }
Message::FocusNext => return widget::operation::focus_next(), Message::FocusNext => return widget::operation::focus_next(),
@ -153,7 +177,11 @@ impl State {
Task::none() Task::none()
} }
fn view(&self) -> Element<'_> { fn view(&self) -> Element<'_> {
let main = if !self.db_view.is_empty() { let main = if self.db_loaded {
widget::column([
widget::text_input("search query", &self.query)
.on_input(Message::ChangeQuery)
.into(),
widget::scrollable( widget::scrollable(
widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| { widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| {
[ [
@ -161,13 +189,14 @@ impl State {
let field = db::BasicField::Title; let field = db::BasicField::Title;
widget::button(widget::text(title)) widget::button(widget::text(title))
.style( .style(
if i == self.selected_entry && field == self.selected_field { if i == self.selected_entry && field == self.selected_field
{
widget::button::primary widget::button::primary
} else { } else {
widget::button::subtle widget::button::subtle
}, },
) )
.on_press(Message::SelectionChanged(i, field)) .on_press(Message::ChangeSelection(i, field))
.into() .into()
} else { } else {
widget::space().into() widget::space().into()
@ -176,13 +205,14 @@ impl State {
let field = db::BasicField::Username; let field = db::BasicField::Username;
widget::button(widget::text(title)) widget::button(widget::text(title))
.style( .style(
if i == self.selected_entry && field == self.selected_field { if i == self.selected_entry && field == self.selected_field
{
widget::button::primary widget::button::primary
} else { } else {
widget::button::subtle widget::button::subtle
}, },
) )
.on_press(Message::SelectionChanged(i, field)) .on_press(Message::ChangeSelection(i, field))
.into() .into()
} else { } else {
widget::space().into() widget::space().into()
@ -191,13 +221,14 @@ impl State {
let field = db::BasicField::Password; let field = db::BasicField::Password;
widget::button(widget::text("[password hidden]")) widget::button(widget::text("[password hidden]"))
.style( .style(
if i == self.selected_entry && field == self.selected_field { if i == self.selected_entry && field == self.selected_field
{
widget::button::primary widget::button::primary
} else { } else {
widget::button::subtle widget::button::subtle
}, },
) )
.on_press(Message::SelectionChanged(i, field)) .on_press(Message::ChangeSelection(i, field))
.into() .into()
} else { } else {
widget::space().into() widget::space().into()
@ -206,13 +237,14 @@ impl State {
let field = db::BasicField::Url; let field = db::BasicField::Url;
widget::button(widget::text(title)) widget::button(widget::text(title))
.style( .style(
if i == self.selected_entry && field == self.selected_field { if i == self.selected_entry && field == self.selected_field
{
widget::button::primary widget::button::primary
} else { } else {
widget::button::subtle widget::button::subtle
}, },
) )
.on_press(Message::SelectionChanged(i, field)) .on_press(Message::ChangeSelection(i, field))
.into() .into()
} else { } else {
widget::space().into() widget::space().into()
@ -222,6 +254,11 @@ impl State {
.height(Length::Shrink) .height(Length::Shrink)
.columns(4), .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() .into()
} else { } else {
widget::container( widget::container(

View file

@ -27,6 +27,17 @@ impl BasicField {
} }
} }
pub trait EntrySearchExt {
fn contains(&self, s: impl AsRef<str>) -> bool;
}
impl EntrySearchExt for keepass::db::EntryRef<'_> {
fn contains(&self, s: impl AsRef<str>) -> 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<String> { pub fn get_last_opened() -> io::Result<String> {
Ok(fs::read_to_string( Ok(fs::read_to_string(
dirs::data_dir() dirs::data_dir()