From 020c097c2f09c08c7d56eebc22bfac78b1faebeb Mon Sep 17 00:00:00 2001 From: rlespinasse Date: Sat, 19 Dec 2020 19:29:06 +0100 Subject: [PATCH 1/3] feat: support fuzzy-match search --- Cargo.lock | 19 ++++++++++++++++++ Cargo.toml | 3 ++- src/cfg.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 17 ++++++++++++---- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50eba1a..62e7589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,15 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "hermit-abi" version = "0.1.17" @@ -186,6 +195,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "unicode-width" version = "0.1.8" @@ -312,6 +330,7 @@ name = "wints" version = "0.1.0" dependencies = [ "clap", + "fuzzy-matcher", "serde", "serde_yaml", "webbrowser", diff --git a/Cargo.toml b/Cargo.toml index a780e9d..2b976b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2018" clap = "2.33.3" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" -webbrowser = "0.5.5" \ No newline at end of file +webbrowser = "0.5.5" +fuzzy-matcher = "0.3.7" \ No newline at end of file diff --git a/src/cfg.rs b/src/cfg.rs index d35a8ca..3258007 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -3,6 +3,8 @@ use std::fs::File; use std::io::Write; use std::path::Path; +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; use serde::{Deserialize, Serialize}; type Result = std::result::Result>; @@ -56,11 +58,61 @@ impl Config { .collect() } - pub(crate) fn urls_from_context(&self, context: String) -> Vec { + pub(crate) fn urls_from_context(&self, context: Vec) -> Vec { + let matcher = SkimMatcherV2::default(); self.elements .iter() - .filter(|element| element.context == context) + .filter(|element| { + let matching_terms_count = + Config::matching_term_count(&matcher, &context, &element); + matching_terms_count == context.capacity() + }) .flat_map(|element| element.urls.clone()) .collect() } + + pub(crate) fn nearest_context(&self, context: Vec) -> Option { + self.contexts_sorted_by_matching_accuracy(context) + .first() + .cloned() + } + + fn contexts_sorted_by_matching_accuracy(&self, context: Vec) -> Vec { + let matcher = SkimMatcherV2::default(); + let mut partially_matching_elements: Vec<&Element> = self + .elements + .iter() + .filter(|element| { + let matching_terms_count = + Config::matching_term_count(&matcher, &context, &element); + matching_terms_count != context.capacity() && matching_terms_count != 0 + }) + .collect(); + + partially_matching_elements.sort_by(|first, second| { + let first_count = Config::matching_term_count(&matcher, &context, &first); + let second_count = Config::matching_term_count(&matcher, &context, &second); + first_count.cmp(&second_count) + }); + + partially_matching_elements + .iter() + .map(|element| element.context.clone()) + .collect() + } + + fn matching_term_count( + matcher: &SkimMatcherV2, + context: &[String], + element: &Element, + ) -> usize { + context + .iter() + .filter(|term| { + matcher + .fuzzy_match(element.context.as_str(), term.as_str()) + .is_some() + }) + .count() + } } diff --git a/src/main.rs b/src/main.rs index 271a719..73b7929 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ fn main() { let config = load_configuration(); match matches.values_of_lossy("terms") { - Some(terms) => open_urls_based_on_terms(terms.join(" "), config), + Some(terms) => open_urls_based_on_terms(terms, config), None => terms_are_mandatory(config), } } @@ -40,9 +40,18 @@ fn terms_are_mandatory(config: Config) { } } -fn open_urls_based_on_terms(terms_search: String, config: Config) { - println!(" {} Search for '{}'", SEARCH, terms_search); - for url in config.urls_from_context(terms_search).iter() { +fn open_urls_based_on_terms(terms_search: Vec, config: Config) { + println!(" {} Search for '{}'", SEARCH, terms_search.join(" ")); + let urls = config.urls_from_context(terms_search.clone()); + if urls.is_empty() { + if let Some(nearest_context) = config.nearest_context(terms_search) { + println!( + " {} Narrowly missed. Try using terms from '{}'", + CAUTION, nearest_context + ) + } + } + for url in urls.iter() { match webbrowser::open(url) { Ok(_) => println!("open {}", url), Err(err) => eprintln!("can't open {}: {}", url, err), From bdedc5ba2d11b8f448197e800dd21f8e4e9f46ec Mon Sep 17 00:00:00 2001 From: rlespinasse Date: Sat, 19 Dec 2020 21:10:03 +0100 Subject: [PATCH 2/3] refactor: improve output messages --- Cargo.lock | 12 ++++++++++- Cargo.toml | 3 ++- src/main.rs | 59 +++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62e7589..81a2ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "atty" version = "0.2.14" @@ -50,7 +59,7 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term", + "ansi_term 0.11.0", "atty", "bitflags", "strsim", @@ -329,6 +338,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "wints" version = "0.1.0" dependencies = [ + "ansi_term 0.12.1", "clap", "fuzzy-matcher", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2b976b4..94af1e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ clap = "2.33.3" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" webbrowser = "0.5.5" -fuzzy-matcher = "0.3.7" \ No newline at end of file +fuzzy-matcher = "0.3.7" +ansi_term = "0.12.1" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 73b7929..e3fe59f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,20 +3,27 @@ mod cfg; #[macro_use] extern crate clap; +use ansi_term::Colour::{Green, Red}; +use ansi_term::Style; use cfg::Config; use clap::{App, Arg}; use std::path::Path; -// ⚠️ is two chars, so need an extra space after it to have the correct "⚠️" output +// ⚠️ is composed of two chars, so need an extra space after it to have the correct "⚠️" output static CAUTION: &str = "⚠️ "; static SEARCH: &str = "🔎"; static WRITE: &str = "📝"; +static SAD: &str = "😢"; fn main() { - let matches = App::new("wints") - .about("What I Need To See - a fuzzy term-based url opener") - .version(crate_version!()) - .author(crate_authors!()) + let cli_name = Style::new().bold().paint("wints"); + let display_name = Style::new().bold().paint("What I Need To See"); + let about = Green.bold().blink().paint("a fuzzy term-based url opener"); + let version = Green.bold().paint(crate_version!()); + + let matches = App::new(cli_name.to_string()) + .about(format!("{} - {}", display_name, about).as_str()) + .version(version.to_string().as_str()) .arg( Arg::with_name("terms") .help("Terms to search for") @@ -36,25 +43,35 @@ fn main() { fn terms_are_mandatory(config: Config) { println!(" {} No terms passed, can't search anything.", CAUTION); if let Some(possible_terms) = config.list_of_contexts().first() { - println!(" {} Try with '{}'", SEARCH, possible_terms) + println!( + " {} Try with {}.", + SEARCH, + Green.bold().paint(possible_terms) + ) } } fn open_urls_based_on_terms(terms_search: Vec, config: Config) { - println!(" {} Search for '{}'", SEARCH, terms_search.join(" ")); + println!( + " {} Searching for {}.", + SEARCH, + Green.bold().paint(terms_search.join(" ")) + ); let urls = config.urls_from_context(terms_search.clone()); if urls.is_empty() { - if let Some(nearest_context) = config.nearest_context(terms_search) { - println!( - " {} Narrowly missed. Try using terms from '{}'", - CAUTION, nearest_context - ) + match config.nearest_context(terms_search) { + Some(nearest_context) => println!( + " {} Missed, try with terms like in '{}'.", + SAD, + Green.bold().paint(nearest_context) + ), + None => println!(" {} Nothing found, try with another term.", SAD), } } for url in urls.iter() { match webbrowser::open(url) { Ok(_) => println!("open {}", url), - Err(err) => eprintln!("can't open {}: {}", url, err), + Err(why) => eprintln!("can't open {}: {}", url, Red.paint(why.to_string())), } } } @@ -66,8 +83,9 @@ fn load_configuration() -> Config { match Config::read_file(config_filename) { Err(why) => panic!( - "can't load configuration file '{}': {}", - config_filename, why + "can't load configuration file {}: {}", + Green.bold().paint(config_filename), + Red.paint(why.to_string()) ), Ok(config) => config, } @@ -79,10 +97,15 @@ fn ensure_configuration_file_exists(config_filename: &str) { println!(" {} Can't find any '{}' file.", CAUTION, config_filename); match Config::write_default_file(config_filename) { - Err(why) => panic!("couldn't create '{}': {}", config_filename, why), + Err(why) => panic!( + "couldn't create {}: {}", + Green.bold().paint(config_filename), + Red.paint(why.to_string()) + ), Ok(_) => println!( - " {} So an empty '{}' file has been created.", - WRITE, config_filename + " {} So an empty {} file has been created.", + WRITE, + Green.bold().paint(config_filename) ), }; } From 8d153462c7780df3ff5ef1fbe5e2cae012007e5b Mon Sep 17 00:00:00 2001 From: rlespinasse Date: Sat, 19 Dec 2020 21:19:52 +0100 Subject: [PATCH 3/3] build: setup editorconfig --- .editorconfig | 16 ++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f0735ce --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.yml] +indent_size = 2 diff --git a/Cargo.toml b/Cargo.toml index 94af1e0..6effdf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,4 @@ serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" webbrowser = "0.5.5" fuzzy-matcher = "0.3.7" -ansi_term = "0.12.1" \ No newline at end of file +ansi_term = "0.12.1"