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::{
Color, Length, Subscription, Task, application, event,
Alignment, Color, Length, Subscription, Task, application, event,
keyboard::{self, Key, key},
theme, widget,
};
@ -20,12 +20,10 @@ enum Message {
ChangePassword(String),
ChangeSecurity(bool),
OpenDb,
DbOpened(Result<keepass::Database, String>),
FocusNext,
FocusPrevious,
Enter,
Pick,
}
#[derive(Default)]
@ -38,6 +36,9 @@ struct State {
password: String,
is_secure: bool,
status: Cow<'static, str>,
error: Cow<'static, str>,
}
impl State {
@ -81,36 +82,27 @@ impl State {
Message::ChangePassword(password) => self.password = password,
Message::ChangeSecurity(bool) => self.is_secure = bool,
Message::OpenDb => {
match self.open_db() {
Err(e) => eprintln!("failed to open db '{}': {e}", self.db_path),
Ok(()) => {
#[allow(unused_must_use)]
db::set_last_opened(&self.db_path)
.inspect_err(|e| eprintln!("failed to set last-opened db: {e}"));
Message::OpenDb => return self.open_db(),
Message::DbOpened(result) => {
match result {
Ok(db) => {
self.error = "".into();
self.db = Some(db)
}
};
Err(e) => self.error = e.into(),
}
self.status = "".into();
}
Message::FocusNext => return widget::operation::focus_next(),
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()
}
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::column(db.iter_all_entries().map(|entry| {
widget::text(entry.get_title().unwrap_or_default().to_string()).into()
@ -163,11 +155,33 @@ impl State {
)
.center(Length::Fill)
.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> {
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, .. }) => {
match key.as_ref() {
Key::Named(key::Named::Tab) => Some(if modifiers.shift() {
@ -176,8 +190,10 @@ impl State {
Message::FocusNext
}),
Key::Named(key::Named::Enter) => Some(Message::Enter),
Key::Character("p" | "f" | "i") => Some(Message::Pick),
Key::Named(key::Named::Enter) if !db_loaded && db_path_exists => {
Some(Message::OpenDb)
}
Key::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb),
// ignore unused keys
_ => 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);
self.db = Some(
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())?,
);
self.status = "loading...".into();
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,
)
}
}