feat: copying entry fields

This commit is contained in:
electria 2026-08-26 11:59:23 -07:00
commit b7c5a05c6e
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs
4 changed files with 420 additions and 476 deletions

715
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,4 +10,7 @@ keepass = "0.13.22"
rfd = "0.17.2" rfd = "0.17.2"
[dependencies.iced] [dependencies.iced]
version = "0.14.0" version = "0.15.0-dev"
[patch.crates-io]
iced.git = "https://github.com/iced-rs/iced"

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, fs}; use std::{borrow::Cow, fs};
use iced::{ use iced::{
Alignment, Color, Length, Subscription, Task, application, event, Alignment, Color, Length, Subscription, Task, application, clipboard, event,
keyboard::{self, Key, key}, keyboard::{self, Key, key},
theme, widget, theme, widget,
}; };
@ -22,9 +22,15 @@ enum Message {
OpenDb, OpenDb,
DbOpened(Result<keepass::Database, String>), DbOpened(Result<keepass::Database, String>),
SelectionChanged(usize), SelectionChanged(usize, db::BasicField),
SelectionUp,
SelectionLeft,
SelectionDown, SelectionDown,
SelectionUp,
SelectionRight,
Copy,
Copied(Result<(), clipboard::Error>),
FocusNext, FocusNext,
FocusPrevious, FocusPrevious,
@ -39,6 +45,7 @@ struct State {
db_view: Vec<keepass::db::Entry>, db_view: Vec<keepass::db::Entry>,
selected_entry: usize, selected_entry: usize,
selected_field: db::BasicField,
password: String, password: String,
@ -106,19 +113,45 @@ impl State {
self.status = "".into(); self.status = "".into();
} }
Message::SelectionChanged(val) => self.selected_entry = val, Message::SelectionChanged(i, field) => {
Message::SelectionUp => { self.selected_entry = i;
self.selected_entry = self.selected_field = field;
dbg!((self.selected_entry.wrapping_sub(1)).clamp(0, self.db_view.len() - 1)) return Task::done(Message::Copy);
} }
Message::SelectionLeft => self.selected_field.previous(),
Message::SelectionDown => { Message::SelectionDown => {
self.selected_entry = if self.selected_entry == self.db_view.len() - 1 { self.selected_entry = if self.selected_entry == self.db_view.len() - 1 {
// essentially do a wrapping_add up to the highest index in the view // essentially do a wrapping_add up to the highest index in the view
0 0
} else { } else {
dbg!((self.selected_entry.wrapping_add(1)).clamp(0, self.db_view.len() - 1)) (self.selected_entry.saturating_add(1)).clamp(0, self.db_view.len() - 1)
} }
} }
Message::SelectionUp => {
self.selected_entry =
(self.selected_entry.saturating_sub(1)).clamp(0, self.db_view.len() - 1)
}
Message::SelectionRight => self.selected_field.next(),
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()).map(Message::Copied);
} else {
self.error = "nothing to copy!".into();
}
}
Message::Copied(result) => {
self.error = result
.err()
.map(|e| format!("{e:?}").into())
.unwrap_or_default();
}
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(),
@ -131,34 +164,70 @@ impl State {
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)| {
[ [
widget::button(widget::text(entry.get_title().unwrap_or_default())) if let Some(title) = entry.get_title() {
.style(if i == self.selected_entry { let field = db::BasicField::Title;
widget::button::primary widget::button(widget::text(title))
} else { .style(
widget::button::subtle if i == self.selected_entry && field == self.selected_field {
}) widget::button::primary
.on_press(Message::SelectionChanged(i)) } else {
.into(), widget::button::subtle
widget::button(widget::text(entry.get_username().unwrap_or_default())) },
.style(if i == self.selected_entry { )
widget::button::primary .on_press(Message::SelectionChanged(i, field))
} else { .into()
widget::button::subtle } else {
}) widget::space().into()
.on_press(Message::SelectionChanged(i)) },
.into(), if let Some(title) = entry.get_username() {
widget::button(widget::text(entry.get_url().unwrap_or_default())) let field = db::BasicField::Username;
.style(if i == self.selected_entry { widget::button(widget::text(title))
widget::button::primary .style(
} else { if i == self.selected_entry && field == self.selected_field {
widget::button::subtle widget::button::primary
}) } else {
.on_press(Message::SelectionChanged(i)) widget::button::subtle
.into(), },
)
.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) .height(Length::Shrink)
.columns(3), .columns(4),
) )
.into() .into()
} else { } else {
@ -238,12 +307,20 @@ impl State {
} }
Key::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb), Key::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb),
Key::Character("k") | Key::Named(key::Named::ArrowUp) => { Key::Character("h") | Key::Named(key::Named::ArrowLeft) => {
Some(Message::SelectionUp) Some(Message::SelectionLeft)
} }
Key::Character("j") | Key::Named(key::Named::ArrowDown) => { Key::Character("j") | Key::Named(key::Named::ArrowDown) => {
Some(Message::SelectionDown) Some(Message::SelectionDown)
} }
Key::Character("k") | Key::Named(key::Named::ArrowUp) => {
Some(Message::SelectionUp)
}
Key::Character("l") | Key::Named(key::Named::ArrowRight) => {
Some(Message::SelectionRight)
}
Key::Character("y" | "c") if db_loaded => Some(Message::Copy),
// ignore unused keys // ignore unused keys
_ => None, _ => None,
@ -283,10 +360,10 @@ pub fn run() -> Result<(), iced::Error> {
.subscription(State::subscription) .subscription(State::subscription)
.theme(theme::Theme::custom( .theme(theme::Theme::custom(
"high-contrast-dark", "high-contrast-dark",
theme::Palette { theme::palette::Seed {
background: Color::from_rgb8(10, 10, 10), background: Color::from_rgb8(10, 10, 10),
text: Color::WHITE, text: Color::WHITE,
..theme::Palette::DARK ..theme::palette::Seed::DARK
}, },
)) ))
.run() .run()

View file

@ -1,5 +1,32 @@
use std::{fs, io}; use std::{fs, io};
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub enum BasicField {
Title,
#[default]
Username,
Password,
Url,
}
impl BasicField {
pub fn next(&mut self) {
*self = match self {
Self::Title => Self::Username,
Self::Username => Self::Password,
Self::Password => Self::Url,
Self::Url => Self::Title,
}
}
pub fn previous(&mut self) {
*self = match self {
Self::Title => Self::Url,
Self::Username => Self::Title,
Self::Password => Self::Username,
Self::Url => Self::Password,
}
}
}
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()