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]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
|
@ -3487,9 +3486,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -3500,19 +3499,23 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.76"
|
||||
version = "0.4.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -3520,9 +3523,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
|
|
@ -3533,9 +3536,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -3691,9 +3694,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
[package]
|
||||
name = "keepice"
|
||||
description = "simple KeePass database viewer (for now)"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
version = 1
|
||||
|
||||
[[annotations]]
|
||||
path = [
|
||||
"*",
|
||||
"src/*",
|
||||
]
|
||||
path = [ "**" ]
|
||||
SPDX-FileCopyrightText = "2026 Electria"
|
||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@
|
|||
doCheck = false;
|
||||
|
||||
postFixup = ''
|
||||
mkdir -p "$out/share/applications"
|
||||
ln -s "${desktopItem}"/share/applications/* "$out/share/applications/"
|
||||
cp -rs "${desktopItem}"/* "$out"
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
|
|
|||
328
src/app/mod.rs
328
src/app/mod.rs
|
|
@ -1,13 +1,13 @@
|
|||
use std::{borrow::Cow, fs};
|
||||
|
||||
use iced::{
|
||||
Alignment, Color, Length, Subscription, Task, application, event,
|
||||
Alignment, Color, Length, Subscription, Task, application, clipboard, event,
|
||||
keyboard::{self, Key, key},
|
||||
theme, widget,
|
||||
theme, widget, window,
|
||||
};
|
||||
use rfd::{AsyncFileDialog, FileHandle};
|
||||
|
||||
use crate::db;
|
||||
use crate::db::{self, EntrySearchExt};
|
||||
|
||||
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
|
||||
|
||||
|
|
@ -17,17 +17,34 @@ enum Message {
|
|||
PickDb,
|
||||
PickedDb(Option<FileHandle>),
|
||||
|
||||
ChangeKeyfilePath(String),
|
||||
PickKeyfile,
|
||||
PickedKeyfile(Option<FileHandle>),
|
||||
|
||||
ChangePassword(String),
|
||||
ChangeSecurity(bool),
|
||||
ToggleSecurity,
|
||||
OpenDb,
|
||||
DbOpened(Result<keepass::Database, String>),
|
||||
|
||||
SelectionChanged(usize),
|
||||
SelectionUp,
|
||||
ChangeSelection(usize, db::BasicField),
|
||||
|
||||
SelectionLeft,
|
||||
SelectionDown,
|
||||
SelectionUp,
|
||||
SelectionRight,
|
||||
|
||||
SelectionTop,
|
||||
SelectionBottom,
|
||||
|
||||
ChangeQuery(String),
|
||||
|
||||
Copy,
|
||||
|
||||
FocusNext,
|
||||
FocusPrevious,
|
||||
FocusSearch,
|
||||
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -35,14 +52,18 @@ struct State {
|
|||
db_path: String,
|
||||
db_path_exists: bool,
|
||||
|
||||
keyfile_path: String,
|
||||
keyfile_path_exists: bool,
|
||||
|
||||
is_secure: bool,
|
||||
password: String,
|
||||
|
||||
db: Option<keepass::Database>,
|
||||
|
||||
db_view: Vec<keepass::db::Entry>,
|
||||
selected_entry: usize,
|
||||
|
||||
password: String,
|
||||
|
||||
is_secure: bool,
|
||||
selected_field: db::BasicField,
|
||||
query: String,
|
||||
|
||||
status: Cow<'static, str>,
|
||||
error: Cow<'static, str>,
|
||||
|
|
@ -87,79 +108,173 @@ impl State {
|
|||
}
|
||||
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::ChangeSecurity(bool) => self.is_secure = bool,
|
||||
Message::ToggleSecurity => self.is_secure = !self.is_secure,
|
||||
Message::OpenDb => return self.open_db(),
|
||||
|
||||
Message::DbOpened(result) => {
|
||||
match result {
|
||||
Ok(db) => {
|
||||
self.error = "".into();
|
||||
self.db_view = db
|
||||
.iter_all_entries()
|
||||
.map(|entry_ref| entry_ref.clone())
|
||||
.collect();
|
||||
self.db = Some(db)
|
||||
self.db = Some(db);
|
||||
self.populate_view();
|
||||
}
|
||||
Err(e) => self.error = e.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 => {
|
||||
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 => {
|
||||
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
|
||||
0
|
||||
Message::SelectionRight => self.selected_field.next(),
|
||||
|
||||
Message::SelectionTop => self.selected_entry = 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 {
|
||||
self.error = "nothing to copy!".into();
|
||||
}
|
||||
} else {
|
||||
dbg!((self.selected_entry.wrapping_add(1)).clamp(0, self.db_view.len() - 1))
|
||||
self.error = "nothing selected!".into();
|
||||
}
|
||||
}
|
||||
|
||||
Message::FocusNext => return widget::operation::focus_next(),
|
||||
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()
|
||||
}
|
||||
fn view(&self) -> Element<'_> {
|
||||
let main = if !self.db_view.is_empty() {
|
||||
widget::scrollable(
|
||||
widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| {
|
||||
[
|
||||
widget::button(widget::text(entry.get_title().unwrap_or_default()))
|
||||
.style(if i == self.selected_entry {
|
||||
widget::button::primary
|
||||
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::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| {
|
||||
[
|
||||
if let Some(title) = entry.get_title() {
|
||||
let field = db::BasicField::Title;
|
||||
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::button::subtle
|
||||
})
|
||||
.on_press(Message::SelectionChanged(i))
|
||||
.into(),
|
||||
widget::button(widget::text(entry.get_username().unwrap_or_default()))
|
||||
.style(if i == self.selected_entry {
|
||||
widget::button::primary
|
||||
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
|
||||
} else {
|
||||
widget::button::subtle
|
||||
},
|
||||
)
|
||||
.on_press(Message::ChangeSelection(i, field))
|
||||
.into()
|
||||
} else {
|
||||
widget::button::subtle
|
||||
})
|
||||
.on_press(Message::SelectionChanged(i))
|
||||
.into(),
|
||||
widget::button(widget::text(entry.get_url().unwrap_or_default()))
|
||||
.style(if i == self.selected_entry {
|
||||
widget::button::primary
|
||||
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::ChangeSelection(i, field))
|
||||
.into()
|
||||
} else {
|
||||
widget::button::subtle
|
||||
})
|
||||
.on_press(Message::SelectionChanged(i))
|
||||
.into(),
|
||||
]
|
||||
}))
|
||||
.height(Length::Shrink)
|
||||
.columns(3),
|
||||
)
|
||||
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)
|
||||
.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()
|
||||
} else {
|
||||
widget::container(
|
||||
|
|
@ -168,7 +283,30 @@ impl State {
|
|||
widget::text_input("database path", &self.db_path)
|
||||
.on_input(Message::ChangeDbPath)
|
||||
.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(),
|
||||
widget::row([
|
||||
|
|
@ -178,13 +316,15 @@ impl State {
|
|||
.into(),
|
||||
widget::checkbox(self.is_secure)
|
||||
.label("security")
|
||||
.on_toggle(Message::ChangeSecurity)
|
||||
.on_toggle(|_| Message::ToggleSecurity)
|
||||
.into(),
|
||||
])
|
||||
.spacing(10)
|
||||
.into(),
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -227,23 +367,44 @@ impl State {
|
|||
.filter_map(move |((db_loaded, db_path_exists), event)| match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
|
||||
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() {
|
||||
Message::FocusPrevious
|
||||
} else {
|
||||
Message::FocusNext
|
||||
}),
|
||||
Key::Character("/" | "f") => Some(Message::FocusSearch),
|
||||
|
||||
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),
|
||||
|
||||
Key::Character("k") | Key::Named(key::Named::ArrowUp) => {
|
||||
Some(Message::SelectionUp)
|
||||
Key::Character("h") | Key::Named(key::Named::ArrowLeft) => {
|
||||
Some(Message::SelectionLeft)
|
||||
}
|
||||
Key::Character("j") | Key::Named(key::Named::ArrowDown) => {
|
||||
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
|
||||
_ => None,
|
||||
|
|
@ -260,12 +421,32 @@ impl State {
|
|||
|
||||
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(
|
||||
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(
|
||||
&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())?;
|
||||
|
||||
|
|
@ -276,6 +457,23 @@ impl State {
|
|||
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> {
|
||||
|
|
|
|||
38
src/db.rs
38
src/db.rs
|
|
@ -1,5 +1,43 @@
|
|||
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> {
|
||||
Ok(fs::read_to_string(
|
||||
dirs::data_dir()
|
||||
|
|
|
|||
Loading…
Reference in a new issue