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 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<keepass::Database, String>),
SelectionChanged(usize, db::BasicField),
ChangeSelection(usize, db::BasicField),
SelectionLeft,
SelectionDown,
SelectionUp,
SelectionRight,
ChangeQuery(String),
Copy,
FocusNext,
@ -42,9 +44,11 @@ struct State {
db: Option<keepass::Database>,
db_loaded: bool,
db_view: Vec<keepass::db::Entry>,
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(

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> {
Ok(fs::read_to_string(
dirs::data_dir()