Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6695bc4fb9 |
|||
|
580276c568 |
|||
|
6e7d50a79d |
|||
|
f7340ffcb9 |
|||
|
2b41e36362 |
|||
|
7aafbe2207 |
|||
|
b4ac07ffc4 |
|||
|
7c86e3d496 |
|||
|
8e6fa9be2f |
|||
|
b7c5a05c6e |
6 changed files with 324 additions and 87 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -1704,12 +1704,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.103"
|
version = "0.3.85"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"once_cell",
|
||||||
"futures-util",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3487,9 +3486,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.126"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -3500,19 +3499,23 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.76"
|
version = "0.4.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.126"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
|
@ -3520,9 +3523,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.126"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -3533,9 +3536,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.126"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
@ -3691,9 +3694,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.103"
|
version = "0.3.85"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keepice"
|
name = "keepice"
|
||||||
|
description = "simple KeePass database viewer (for now)"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "AGPL-3.0-or-later"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
[[annotations]]
|
[[annotations]]
|
||||||
path = [
|
path = [ "**" ]
|
||||||
"*",
|
|
||||||
"src/*",
|
|
||||||
]
|
|
||||||
SPDX-FileCopyrightText = "2026 Electria"
|
SPDX-FileCopyrightText = "2026 Electria"
|
||||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,7 @@
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
|
|
||||||
postFixup = ''
|
postFixup = ''
|
||||||
mkdir -p "$out/share/applications"
|
cp -rs "${desktopItem}"/* "$out"
|
||||||
ln -s "${desktopItem}"/share/applications/* "$out/share/applications/"
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
302
src/app/mod.rs
302
src/app/mod.rs
|
|
@ -1,13 +1,13 @@
|
||||||
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, window,
|
||||||
};
|
};
|
||||||
use rfd::{AsyncFileDialog, FileHandle};
|
use rfd::{AsyncFileDialog, FileHandle};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db::{self, EntrySearchExt};
|
||||||
|
|
||||||
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
|
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
|
||||||
|
|
||||||
|
|
@ -17,17 +17,34 @@ enum Message {
|
||||||
PickDb,
|
PickDb,
|
||||||
PickedDb(Option<FileHandle>),
|
PickedDb(Option<FileHandle>),
|
||||||
|
|
||||||
|
ChangeKeyfilePath(String),
|
||||||
|
PickKeyfile,
|
||||||
|
PickedKeyfile(Option<FileHandle>),
|
||||||
|
|
||||||
ChangePassword(String),
|
ChangePassword(String),
|
||||||
ChangeSecurity(bool),
|
ToggleSecurity,
|
||||||
OpenDb,
|
OpenDb,
|
||||||
DbOpened(Result<keepass::Database, String>),
|
DbOpened(Result<keepass::Database, String>),
|
||||||
|
|
||||||
SelectionChanged(usize),
|
ChangeSelection(usize, db::BasicField),
|
||||||
SelectionUp,
|
|
||||||
|
SelectionLeft,
|
||||||
SelectionDown,
|
SelectionDown,
|
||||||
|
SelectionUp,
|
||||||
|
SelectionRight,
|
||||||
|
|
||||||
|
SelectionTop,
|
||||||
|
SelectionBottom,
|
||||||
|
|
||||||
|
ChangeQuery(String),
|
||||||
|
|
||||||
|
Copy,
|
||||||
|
|
||||||
FocusNext,
|
FocusNext,
|
||||||
FocusPrevious,
|
FocusPrevious,
|
||||||
|
FocusSearch,
|
||||||
|
|
||||||
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -35,14 +52,18 @@ struct State {
|
||||||
db_path: String,
|
db_path: String,
|
||||||
db_path_exists: bool,
|
db_path_exists: bool,
|
||||||
|
|
||||||
|
keyfile_path: String,
|
||||||
|
keyfile_path_exists: bool,
|
||||||
|
|
||||||
|
is_secure: bool,
|
||||||
|
password: String,
|
||||||
|
|
||||||
db: Option<keepass::Database>,
|
db: Option<keepass::Database>,
|
||||||
|
|
||||||
db_view: Vec<keepass::db::Entry>,
|
db_view: Vec<keepass::db::Entry>,
|
||||||
selected_entry: usize,
|
selected_entry: usize,
|
||||||
|
selected_field: db::BasicField,
|
||||||
password: String,
|
query: String,
|
||||||
|
|
||||||
is_secure: bool,
|
|
||||||
|
|
||||||
status: Cow<'static, str>,
|
status: Cow<'static, str>,
|
||||||
error: Cow<'static, str>,
|
error: Cow<'static, str>,
|
||||||
|
|
@ -87,79 +108,173 @@ impl State {
|
||||||
}
|
}
|
||||||
Message::PickedDb(None) => eprintln!("no path received from picker"),
|
Message::PickedDb(None) => eprintln!("no path received from picker"),
|
||||||
|
|
||||||
|
Message::ChangeKeyfilePath(keyfile_path) => {
|
||||||
|
self.keyfile_path_exists = fs::exists(&keyfile_path)
|
||||||
|
.inspect_err(|e| eprintln!("failed to check '{keyfile_path}': {e}"))
|
||||||
|
.is_ok_and(|exists| exists);
|
||||||
|
self.keyfile_path = keyfile_path
|
||||||
|
}
|
||||||
|
Message::PickKeyfile => {
|
||||||
|
return Task::perform(AsyncFileDialog::new().pick_file(), Message::PickedKeyfile);
|
||||||
|
}
|
||||||
|
Message::PickedKeyfile(Some(file)) => {
|
||||||
|
return Task::done(Message::ChangeKeyfilePath(
|
||||||
|
file.path().to_string_lossy().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Message::PickedKeyfile(None) => eprintln!("no path received from picker"),
|
||||||
|
|
||||||
Message::ChangePassword(password) => self.password = password,
|
Message::ChangePassword(password) => self.password = password,
|
||||||
Message::ChangeSecurity(bool) => self.is_secure = bool,
|
Message::ToggleSecurity => self.is_secure = !self.is_secure,
|
||||||
Message::OpenDb => return self.open_db(),
|
Message::OpenDb => return self.open_db(),
|
||||||
|
|
||||||
Message::DbOpened(result) => {
|
Message::DbOpened(result) => {
|
||||||
match result {
|
match result {
|
||||||
Ok(db) => {
|
Ok(db) => {
|
||||||
self.error = "".into();
|
self.error = "".into();
|
||||||
self.db_view = db
|
self.db = Some(db);
|
||||||
.iter_all_entries()
|
self.populate_view();
|
||||||
.map(|entry_ref| entry_ref.clone())
|
|
||||||
.collect();
|
|
||||||
self.db = Some(db)
|
|
||||||
}
|
}
|
||||||
Err(e) => self.error = e.into(),
|
Err(e) => self.error = e.into(),
|
||||||
}
|
}
|
||||||
self.status = "".into();
|
self.status = "".into();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SelectionChanged(val) => self.selected_entry = val,
|
Message::ChangeSelection(i, field) => {
|
||||||
|
self.selected_entry = i;
|
||||||
|
self.selected_field = field;
|
||||||
|
return Task::done(Message::Copy);
|
||||||
|
}
|
||||||
|
Message::SelectionLeft => self.selected_field.previous(),
|
||||||
|
Message::SelectionDown => {
|
||||||
|
self.selected_entry =
|
||||||
|
(self.selected_entry.saturating_add(1)).clamp(0, self.db_view.len() - 1)
|
||||||
|
}
|
||||||
Message::SelectionUp => {
|
Message::SelectionUp => {
|
||||||
self.selected_entry =
|
self.selected_entry =
|
||||||
dbg!((self.selected_entry.wrapping_sub(1)).clamp(0, self.db_view.len() - 1))
|
(self.selected_entry.saturating_sub(1)).clamp(0, self.db_view.len() - 1)
|
||||||
}
|
}
|
||||||
Message::SelectionDown => {
|
Message::SelectionRight => self.selected_field.next(),
|
||||||
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
|
Message::SelectionTop => self.selected_entry = 0,
|
||||||
0
|
Message::SelectionBottom => self.selected_entry = self.db_view.len() - 1,
|
||||||
|
|
||||||
|
Message::ChangeQuery(str) => {
|
||||||
|
self.query = str;
|
||||||
|
self.populate_view();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Copy => {
|
||||||
|
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 {
|
} else {
|
||||||
dbg!((self.selected_entry.wrapping_add(1)).clamp(0, self.db_view.len() - 1))
|
self.error = "nothing to copy!".into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.error = "nothing selected!".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::FocusSearch => return widget::operation::focus("search"),
|
||||||
|
|
||||||
|
Message::Quit => return window::latest().and_then(window::close),
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
fn view(&self) -> Element<'_> {
|
fn view(&self) -> Element<'_> {
|
||||||
let main = if !self.db_view.is_empty() {
|
let main = if self.db.is_some() {
|
||||||
|
widget::column([
|
||||||
|
widget::text_input("search query", &self.query)
|
||||||
|
.on_input(Message::ChangeQuery)
|
||||||
|
.id("search")
|
||||||
|
.into(),
|
||||||
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::ChangeSelection(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::ChangeSelection(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::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)
|
.height(Length::Shrink)
|
||||||
.columns(3),
|
.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()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::container(
|
widget::container(
|
||||||
|
|
@ -168,7 +283,30 @@ impl State {
|
||||||
widget::text_input("database path", &self.db_path)
|
widget::text_input("database path", &self.db_path)
|
||||||
.on_input(Message::ChangeDbPath)
|
.on_input(Message::ChangeDbPath)
|
||||||
.into(),
|
.into(),
|
||||||
widget::button("pick file").on_press(Message::PickDb).into(),
|
widget::button("pick db")
|
||||||
|
.style(if self.db_path_exists {
|
||||||
|
widget::button::primary
|
||||||
|
} else {
|
||||||
|
widget::button::danger
|
||||||
|
})
|
||||||
|
.on_press(Message::PickDb)
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.into(),
|
||||||
|
widget::row([
|
||||||
|
widget::text_input("keyfile path", &self.keyfile_path)
|
||||||
|
.on_input(Message::ChangeKeyfilePath)
|
||||||
|
.into(),
|
||||||
|
widget::button("pick keyfile")
|
||||||
|
.style(
|
||||||
|
if self.keyfile_path.is_empty() || self.keyfile_path_exists {
|
||||||
|
widget::button::primary
|
||||||
|
} else {
|
||||||
|
widget::button::danger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_press(Message::PickKeyfile)
|
||||||
|
.into(),
|
||||||
])
|
])
|
||||||
.into(),
|
.into(),
|
||||||
widget::row([
|
widget::row([
|
||||||
|
|
@ -178,13 +316,15 @@ impl State {
|
||||||
.into(),
|
.into(),
|
||||||
widget::checkbox(self.is_secure)
|
widget::checkbox(self.is_secure)
|
||||||
.label("security")
|
.label("security")
|
||||||
.on_toggle(Message::ChangeSecurity)
|
.on_toggle(|_| Message::ToggleSecurity)
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.into(),
|
.into(),
|
||||||
widget::container(widget::button("open database").on_press_maybe(
|
widget::container(widget::button("open database").on_press_maybe(
|
||||||
if self.db_path_exists {
|
if self.db_path_exists
|
||||||
|
&& (self.keyfile_path.is_empty() || self.keyfile_path_exists)
|
||||||
|
{
|
||||||
Some(Message::OpenDb)
|
Some(Message::OpenDb)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -227,23 +367,44 @@ impl State {
|
||||||
.filter_map(move |((db_loaded, db_path_exists), event)| match event {
|
.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::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb),
|
||||||
|
Key::Character("v" | "s") if !db_loaded => Some(Message::ToggleSecurity),
|
||||||
|
|
||||||
|
Key::Named(key::Named::Enter) if !db_loaded && db_path_exists => {
|
||||||
|
Some(Message::OpenDb)
|
||||||
|
}
|
||||||
|
|
||||||
Key::Named(key::Named::Tab) => Some(if modifiers.shift() {
|
Key::Named(key::Named::Tab) => Some(if modifiers.shift() {
|
||||||
Message::FocusPrevious
|
Message::FocusPrevious
|
||||||
} else {
|
} else {
|
||||||
Message::FocusNext
|
Message::FocusNext
|
||||||
}),
|
}),
|
||||||
|
Key::Character("/" | "f") => Some(Message::FocusSearch),
|
||||||
|
|
||||||
Key::Named(key::Named::Enter) if !db_loaded && db_path_exists => {
|
Key::Character("h") | Key::Named(key::Named::ArrowLeft) => {
|
||||||
Some(Message::OpenDb)
|
Some(Message::SelectionLeft)
|
||||||
}
|
|
||||||
Key::Character("p" | "f" | "i") if !db_loaded => Some(Message::PickDb),
|
|
||||||
|
|
||||||
Key::Character("k") | Key::Named(key::Named::ArrowUp) => {
|
|
||||||
Some(Message::SelectionUp)
|
|
||||||
}
|
}
|
||||||
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("g") => Some(if modifiers.shift() {
|
||||||
|
Message::SelectionBottom
|
||||||
|
} else {
|
||||||
|
Message::SelectionTop
|
||||||
|
}),
|
||||||
|
Key::Named(key::Named::Home) => Some(Message::SelectionTop),
|
||||||
|
Key::Named(key::Named::End) => Some(Message::SelectionBottom),
|
||||||
|
|
||||||
|
Key::Character("y" | "c") if db_loaded => Some(Message::Copy),
|
||||||
|
|
||||||
|
Key::Character("q") => Some(Message::Quit),
|
||||||
|
|
||||||
// ignore unused keys
|
// ignore unused keys
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -260,12 +421,32 @@ impl State {
|
||||||
|
|
||||||
self.status = "loading...".into();
|
self.status = "loading...".into();
|
||||||
|
|
||||||
let (db_path, password) = (self.db_path.clone(), self.password.clone());
|
let (db_path, keyfile_path, password) = (
|
||||||
|
self.db_path.clone(),
|
||||||
|
self.keyfile_path.clone(),
|
||||||
|
self.password.clone(),
|
||||||
|
);
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
|
let key = {
|
||||||
|
let mut key = keepass::DatabaseKey::new();
|
||||||
|
if !password.is_empty() {
|
||||||
|
key = key.with_password(&password);
|
||||||
|
}
|
||||||
|
if !keyfile_path.is_empty() {
|
||||||
|
key = key
|
||||||
|
.with_keyfile(
|
||||||
|
&mut fs::File::open(&keyfile_path).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
let db = keepass::Database::open(
|
let db = keepass::Database::open(
|
||||||
&mut fs::File::open(&db_path).map_err(|e| e.to_string())?,
|
&mut fs::File::open(&db_path).map_err(|e| e.to_string())?,
|
||||||
keepass::DatabaseKey::new().with_password(&password),
|
key,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
|
@ -276,6 +457,23 @@ impl State {
|
||||||
Message::DbOpened,
|
Message::DbOpened,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_view(&mut self) {
|
||||||
|
self.db_view = self
|
||||||
|
.db
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter_all_entries()
|
||||||
|
.filter_map(|entry_ref| {
|
||||||
|
// this matches everything when the query is an empty string,
|
||||||
|
// which is the desired behavior so that it displays everything
|
||||||
|
// (when search isn't being used)
|
||||||
|
entry_ref
|
||||||
|
.contains(&self.query)
|
||||||
|
.then(|| entry_ref.to_owned())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> Result<(), iced::Error> {
|
pub fn run() -> Result<(), iced::Error> {
|
||||||
|
|
|
||||||
38
src/db.rs
38
src/db.rs
|
|
@ -1,5 +1,43 @@
|
||||||
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 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> {
|
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