Add fuzzy-search for vault items.

This commit is contained in:
Christoph Heiss 2019-12-24 15:07:33 +01:00
parent a00a8fa54d
commit a4e10bd6ea
Signed by: c8h4
GPG key ID: 73D5E7FDEE3DE49A
3 changed files with 120 additions and 23 deletions

19
Cargo.lock generated
View file

@ -171,6 +171,7 @@ dependencies = [
"cursive_table_view 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"fuzzy-matcher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
@ -647,6 +648,14 @@ dependencies = [
"num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuzzy-matcher"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.12.3"
@ -1681,6 +1690,14 @@ dependencies = [
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.42"
@ -2089,6 +2106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef"
"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
"checksum fuzzy-matcher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d860c6f043a7f367ffcbdb5833c36f7dc85fa8bc6e7898e2530f643ec90f9f3"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462"
@ -2204,6 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327"
"checksum termion 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "818ef3700c2a7b447dca1a1dd28341fe635e6ee103c806c636bb9c929991b2cd"
"checksum thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "88ddf1ad580c7e3d1efff877d972bcc93f995556b9087a5a259630985c88ceab"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
"checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46"

View file

@ -20,6 +20,7 @@ cursive_buffered_backend = "0.3.1"
cursive_table_view = "0.10.0"
directories = "2.0.2"
failure = "0.1.6"
fuzzy-matcher = "0.3.1"
hkdf = "0.8.0"
hmac = "0.7.1"
log = "0.4.8"

View file

@ -1,20 +1,21 @@
// SPDX-License-Identifier: MIT
use std::cmp;
use std::cmp::Ordering;
use clipboard::ClipboardProvider;
use clipboard::ClipboardContext;
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use cursive::Cursive;
use cursive::direction::Orientation;
use cursive::event::Event;
use cursive::event::{Event, Key};
use cursive::traits::*;
use cursive::views::{Dialog, LinearLayout, OnEventView, TextView};
use cursive::views::{Dialog, DummyView, EditView, LinearLayout, OnEventView, TextView};
use cursive_table_view::{TableView, TableViewItem};
use unicase::UniCase;
use crate::api::{self, CipherEntry};
use crate::api::{AuthData, CipherEntry, VaultData};
use crate::cipher::CipherSuite;
@ -33,6 +34,9 @@ struct VaultEntry {
favorite: String,
}
type VaultTableView = TableView::<VaultEntry, VaultColumn>;
impl VaultEntry {
fn from_cipher_entry(entry: &CipherEntry, cipher: &CipherSuite) -> Option<VaultEntry> {
let favorite = if entry.favorite { "\u{2605}" } else { "\u{2606}" };
@ -55,7 +59,7 @@ impl TableViewItem<VaultColumn> for VaultEntry {
}
}
fn cmp(&self, other: &Self, column: VaultColumn) -> cmp::Ordering
fn cmp(&self, other: &Self, column: VaultColumn) -> Ordering
where Self: Sized,
{
match column {
@ -66,29 +70,26 @@ impl TableViewItem<VaultColumn> for VaultEntry {
}
}
type VaultTableView = TableView::<VaultEntry, VaultColumn>;
pub fn show(siv: &mut Cursive, auth_data: api::AuthData, vault_data: api::VaultData) {
pub fn show(siv: &mut Cursive, auth_data: AuthData, vault_data: VaultData) {
let items = vault_data.ciphers
.iter()
.map(|c| VaultEntry::from_cipher_entry(&c, &auth_data.cipher).unwrap())
.collect();
.collect::<Vec<VaultEntry>>();
let mut table = VaultTableView::new()
.column(VaultColumn::Favorite, "", |c| c.width(1))
.column(VaultColumn::Name, "Name", |c| c.width_percent(25))
.column(VaultColumn::Username, "Username", |c| c)
.items(items);
.items(items.clone());
table.sort_by(VaultColumn::Name, cmp::Ordering::Less);
table.sort_by(VaultColumn::Favorite, cmp::Ordering::Less);
table.sort_by(VaultColumn::Name, Ordering::Less);
table.sort_by(VaultColumn::Favorite, Ordering::Less);
let view = OnEventView::new(
let table_view = OnEventView::new(
table
.with_id("password_table")
.min_size((100, 50))
.full_screen()
)
.on_event('j', |siv| {
siv.call_on_id("password_table", |view: &mut VaultTableView| {
@ -98,7 +99,7 @@ pub fn show(siv: &mut Cursive, auth_data: api::AuthData, vault_data: api::VaultD
}
}
})
.unwrap()
.unwrap();
})
.on_event('k', |siv| {
siv.call_on_id("password_table", |view: &mut VaultTableView| {
@ -108,7 +109,7 @@ pub fn show(siv: &mut Cursive, auth_data: api::AuthData, vault_data: api::VaultD
}
}
})
.unwrap()
.unwrap();
})
.on_event(Event::CtrlChar('u'), |siv| {
siv.call_on_id("password_table", |view: &mut VaultTableView| {
@ -123,7 +124,7 @@ pub fn show(siv: &mut Cursive, auth_data: api::AuthData, vault_data: api::VaultD
}
}
})
.unwrap()
.unwrap();
})
.on_event(Event::CtrlChar('p'), |siv| {
siv.call_on_id("password_table", |view: &mut VaultTableView| {
@ -138,17 +139,93 @@ pub fn show(siv: &mut Cursive, auth_data: api::AuthData, vault_data: api::VaultD
}
}
})
.unwrap()
.unwrap();
})
.on_event(Event::CtrlChar('f'), |siv| {
siv.focus_id("search_field").unwrap();
});
let layout = LinearLayout::new(Orientation::Vertical)
let search_field =
EditView::new()
.on_edit(move |siv, content, _| {
fuzzy_match_on_edit(siv, &items, content);
})
.with_id("search_field")
.full_width();
let search_view = LinearLayout::horizontal()
.child(TextView::new("search: "))
.child(
Dialog::around(view)
OnEventView::new(search_field)
.on_event(Event::CtrlChar('f'), |siv| {
siv.focus_id("password_table").unwrap();
})
.on_event(Key::Esc, |siv| {
siv.focus_id("password_table").unwrap();
})
.on_event(Key::Enter, |siv| {
siv.focus_id("password_table").unwrap();
})
.on_event(Event::CtrlChar('u'), |siv| {
if let Some(mut view) = siv.find_id::<EditView>("search_field") {
view.set_content("")(siv);
}
})
);
let main_view = LinearLayout::vertical()
.child(search_view)
.child(DummyView)
.child(table_view);
let layout = LinearLayout::vertical()
.child(
Dialog::around(main_view)
.title("bitwarden vault")
.padding_top(1)
)
.child(
TextView::new("^U: Copy username ^P: Copy password")
LinearLayout::horizontal()
.child(TextView::new("^U: Copy username ^P: Copy password").full_width())
.child(TextView::new("^F: fuzzy-search"))
);
siv.add_layer(layout);
siv.focus_id("password_table").unwrap();
}
fn fuzzy_match_on_edit(siv: &mut Cursive, items: &Vec<VaultEntry>, content: &str) {
let mut table = siv.find_id::<VaultTableView>("password_table").unwrap();
// If no search term is present, sort by name and favorite by default
if content.len() == 0 {
table.set_items(items.clone());
table.sort_by(VaultColumn::Name, Ordering::Less);
table.sort_by(VaultColumn::Favorite, Ordering::Less);
return;
}
let matcher = SkimMatcherV2::default();
let mut items: Vec<(i64, VaultEntry)> = items
.iter()
.map(|entry| {
(matcher.fuzzy_match(&entry.name, content), entry.clone())
})
.filter(|(score, _)| score.is_some())
.map(|(score, entry)| (score.unwrap(), entry))
.collect();
items.sort_by(|a, b| a.0.cmp(&b.0).reverse());
let items = items
.iter()
.map(|(_, entry)| entry.clone())
.collect();
table.set_selected_row(0);
table.set_items(items);
}