feat: copying entry fields
This commit is contained in:
parent
95bfede1e0
commit
b7c5a05c6e
4 changed files with 420 additions and 476 deletions
715
Cargo.lock
generated
715
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
|
|
|
||||||
133
src/app/mod.rs
133
src/app/mod.rs
|
|
@ -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(widget::text(title))
|
||||||
|
.style(
|
||||||
|
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))
|
)
|
||||||
.into(),
|
.on_press(Message::SelectionChanged(i, field))
|
||||||
widget::button(widget::text(entry.get_username().unwrap_or_default()))
|
.into()
|
||||||
.style(if i == self.selected_entry {
|
} 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
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
})
|
},
|
||||||
.on_press(Message::SelectionChanged(i))
|
)
|
||||||
.into(),
|
.on_press(Message::SelectionChanged(i, field))
|
||||||
widget::button(widget::text(entry.get_url().unwrap_or_default()))
|
.into()
|
||||||
.style(if i == self.selected_entry {
|
} 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
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
})
|
},
|
||||||
.on_press(Message::SelectionChanged(i))
|
)
|
||||||
.into(),
|
.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()
|
||||||
|
|
|
||||||
27
src/db.rs
27
src/db.rs
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue