feat: display status and errors

This commit is contained in:
electria 2026-08-25 20:40:27 -07:00
commit 43d382c988
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs

View file

@ -1,7 +1,7 @@
use std::fs; use std::{borrow::Cow, fs};
use iced::{ use iced::{
Color, Length, Subscription, Task, application, event, Alignment, Color, Length, Subscription, Task, application, event,
keyboard::{self, Key, key}, keyboard::{self, Key, key},
theme, widget, theme, widget,
}; };
@ -20,12 +20,10 @@ enum Message {
ChangePassword(String), ChangePassword(String),
ChangeSecurity(bool), ChangeSecurity(bool),
OpenDb, OpenDb,
DbOpened(Result<keepass::Database, String>),
FocusNext, FocusNext,
FocusPrevious, FocusPrevious,
Enter,
Pick,
} }
#[derive(Default)] #[derive(Default)]
@ -38,6 +36,9 @@ struct State {
password: String, password: String,
is_secure: bool, is_secure: bool,
status: Cow<'static, str>,
error: Cow<'static, str>,
} }
impl State { impl State {
@ -81,36 +82,27 @@ impl State {
Message::ChangePassword(password) => self.password = password, Message::ChangePassword(password) => self.password = password,
Message::ChangeSecurity(bool) => self.is_secure = bool, Message::ChangeSecurity(bool) => self.is_secure = bool,
Message::OpenDb => { Message::OpenDb => return self.open_db(),
match self.open_db() {
Err(e) => eprintln!("failed to open db '{}': {e}", self.db_path), Message::DbOpened(result) => {
Ok(()) => { match result {
#[allow(unused_must_use)] Ok(db) => {
db::set_last_opened(&self.db_path) self.error = "".into();
.inspect_err(|e| eprintln!("failed to set last-opened db: {e}")); self.db = Some(db)
} }
}; Err(e) => self.error = e.into(),
}
self.status = "".into();
} }
Message::FocusNext => return widget::operation::focus_next(), Message::FocusNext => return widget::operation::focus_next(),
Message::FocusPrevious => return widget::operation::focus_previous(), Message::FocusPrevious => return widget::operation::focus_previous(),
Message::Enter => {
if self.db.is_none() && self.db_path_exists {
return Task::done(Message::OpenDb);
}
}
Message::Pick => {
if self.db.is_none() {
return Task::done(Message::PickDb);
}
}
} }
Task::none() Task::none()
} }
fn view(&self) -> Element<'_> { fn view(&self) -> Element<'_> {
if let Some(db) = self.db.as_ref() { let main = if let Some(db) = self.db.as_ref() {
widget::scrollable(widget::row([ widget::scrollable(widget::row([
widget::column(db.iter_all_entries().map(|entry| { widget::column(db.iter_all_entries().map(|entry| {
widget::text(entry.get_title().unwrap_or_default().to_string()).into() widget::text(entry.get_title().unwrap_or_default().to_string()).into()
@ -163,11 +155,33 @@ impl State {
) )
.center(Length::Fill) .center(Length::Fill)
.into() .into()
};
let mut bar = Vec::new();
if !self.error.is_empty() {
bar.push(
widget::container(widget::text(&self.error).style(widget::text::danger))
.align_x(Alignment::Start)
.width(Length::Fill)
.into(),
);
} }
if !self.status.is_empty() {
bar.push(
widget::container(widget::text(&self.status))
.align_x(Alignment::End)
.width(Length::Fill)
.into(),
);
}
widget::column([main, widget::row(bar).into()]).into()
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
event::listen().filter_map(|event| match event { event::listen()
.with((self.db.is_some(), self.db_path_exists))
.filter_map(move |((db_loaded, db_path_exists), event)| match event {
iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => { iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
match key.as_ref() { match key.as_ref() {
Key::Named(key::Named::Tab) => Some(if modifiers.shift() { Key::Named(key::Named::Tab) => Some(if modifiers.shift() {
@ -176,8 +190,10 @@ impl State {
Message::FocusNext Message::FocusNext
}), }),
Key::Named(key::Named::Enter) => Some(Message::Enter), Key::Named(key::Named::Enter) if !db_loaded && db_path_exists => {
Key::Character("p" | "f" | "i") => Some(Message::Pick), Some(Message::OpenDb)
}
Key::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb),
// ignore unused keys // ignore unused keys
_ => None, _ => None,
@ -189,18 +205,22 @@ impl State {
}) })
} }
fn open_db(&mut self) -> Result<(), String> { fn open_db(&mut self) -> Task<Message> {
debug_assert!(self.db_path_exists); debug_assert!(self.db_path_exists);
self.db = Some( self.status = "loading...".into();
keepass::Database::open(
&mut fs::File::open(&self.db_path).map_err(|e| e.to_string())?,
keepass::DatabaseKey::new().with_password(&self.password),
)
.map_err(|e| e.to_string())?,
);
Ok(()) let (db_path, password) = (self.db_path.clone(), self.password.clone());
Task::perform(
async move {
Ok(keepass::Database::open(
&mut fs::File::open(&db_path).map_err(|e| e.to_string())?,
keepass::DatabaseKey::new().with_password(&password),
)
.map_err(|e| e.to_string())?)
},
Message::DbOpened,
)
} }
} }