diff --git a/README.md b/README.md index 6a7d43a..4ca9926 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ Anyrun requires plugins to function, as they provide the results for input. The - Calculator & unit conversion. - [Shell](plugins/shell/README.md) - Run shell commands. +- [Translate](plugins/translate/README.md) + - Quickly translate text. - [Kidex](plugins/kidex/README.md) - File search provided by [Kidex](https://github.com/Kirottu/kidex). - [Randr](plugins/randr/README.md) diff --git a/default.nix b/default.nix index 6227553..7fa392f 100644 --- a/default.nix +++ b/default.nix @@ -29,9 +29,6 @@ in cargoLock = { lockFile = ./Cargo.lock; - outputHashes = { - "kidex-common-0.1.0" = "sha256-sPzCTK0gdIYkKWxrtoPJ/F2zrG2ZKHOSmANW2g00fSQ="; - }; }; checkInputs = [cargo rustc]; diff --git a/plugins/applications/README.md b/plugins/applications/README.md index 43db6a6..5d19b1e 100644 --- a/plugins/applications/README.md +++ b/plugins/applications/README.md @@ -6,6 +6,8 @@ Launch applications. Simply search for the application you wish to launch. +*NOTE: The applications plugin does not look for executables in your $PATH, it looks for [desktop entries](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html) in standard locations (`XDG_DATA_DIRS`).* + ## Configuration ```ron diff --git a/plugins/applications/src/scrubber.rs b/plugins/applications/src/scrubber.rs index b2948c1..33131d3 100644 --- a/plugins/applications/src/scrubber.rs +++ b/plugins/applications/src/scrubber.rs @@ -179,7 +179,11 @@ pub fn scrubber(config: &Config) -> Result, Box return None, }; let entries = DesktopEntry::from_dir_entry(&entry, config); - Some(entries.into_iter().map(|entry| (entry.name.clone(), entry))) + Some( + entries + .into_iter() + .map(|entry| (format!("{}{}", entry.name, entry.icon), entry)), + ) }) .flatten() .collect(); @@ -195,7 +199,11 @@ pub fn scrubber(config: &Config) -> Result, Box return None, }; let entries = DesktopEntry::from_dir_entry(&entry, config); - Some(entries.into_iter().map(|entry| (entry.name.clone(), entry))) + Some( + entries + .into_iter() + .map(|entry| (format!("{}{}", entry.name, entry.icon), entry)), + ) }) .flatten(), ), diff --git a/plugins/translate/README.md b/plugins/translate/README.md index c7d5e1a..55d79ef 100644 --- a/plugins/translate/README.md +++ b/plugins/translate/README.md @@ -4,7 +4,8 @@ Quickly translate text using the Google Translate API. ## Usage -Type in ` `, where prefix is the configured prefix (default is in [Configuration](#Configuration)) and the rest are pretty obvious. +Type in ` ` or ` `, +where the `prefix` and `language_delimiter` are config options (defaults are in [Configuration](#Configuration)) and the rest are pretty obvious. ## Configuration @@ -12,6 +13,7 @@ Type in ` `, where prefix is the configu // /translate.ron Config( prefix: ":", + language_delimiter: ">", max_entries: 3, ) ``` \ No newline at end of file diff --git a/plugins/translate/src/lib.rs b/plugins/translate/src/lib.rs index 2816a6c..d4af44f 100644 --- a/plugins/translate/src/lib.rs +++ b/plugins/translate/src/lib.rs @@ -3,11 +3,14 @@ use std::fs; use abi_stable::std_types::{ROption, RString, RVec}; use anyrun_plugin::*; use fuzzy_matcher::FuzzyMatcher; +use reqwest::Client; use serde::Deserialize; +use tokio::runtime::Runtime; #[derive(Deserialize)] struct Config { prefix: String, + language_delimiter: String, max_entries: usize, } @@ -15,6 +18,7 @@ impl Default for Config { fn default() -> Self { Self { prefix: ":".to_string(), + language_delimiter: ">".to_string(), max_entries: 3, } } @@ -22,6 +26,8 @@ impl Default for Config { struct State { config: Config, + client: Client, + runtime: Runtime, langs: Vec<(&'static str, &'static str)>, } @@ -32,6 +38,8 @@ fn init(config_dir: RString) -> State { Ok(content) => ron::from_str(&content).unwrap_or_default(), Err(_) => Config::default(), }, + client: Client::new(), + runtime: Runtime::new().expect("Failed to create tokio runtime"), langs: vec![ ("af", "Afrikaans"), ("sq", "Albanian"), @@ -151,51 +159,94 @@ fn info() -> PluginInfo { } #[get_matches] -fn get_matches(input: RString, data: &State) -> RVec { - if !input.starts_with(&data.config.prefix) { +fn get_matches(input: RString, state: &State) -> RVec { + if !input.starts_with(&state.config.prefix) { return RVec::new(); } // Ignore the prefix - let input = &input[data.config.prefix.len()..]; - let (lang, text) = match input.split_once(' ') { + let input = &input[state.config.prefix.len()..]; + let (lang_split, text) = match input.split_once(' ') { Some(split) => split, None => return RVec::new(), }; + let (src, dest) = match lang_split.split_once(&state.config.language_delimiter) { + Some(split) => (Some(split.0), split.1), + None => (None, lang_split), + }; + if text.is_empty() { return RVec::new(); } let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().ignore_case(); - // Fuzzy match the input language with the languages in the Vec - let mut matches = data + let dest_matches = state .langs .clone() .into_iter() .filter_map(|(code, name)| { matcher - .fuzzy_match(code, lang) - .max(matcher.fuzzy_match(name, lang)) + .fuzzy_match(code, dest) + .max(matcher.fuzzy_match(name, dest)) .map(|score| (code, name, score)) }) .collect::>(); - matches.sort_by(|a, b| b.2.cmp(&a.2)); + // Fuzzy match the input language with the languages in the Vec + let mut matches = match src { + Some(src) => { + let src_matches = state + .langs + .clone() + .into_iter() + .filter_map(|(code, name)| { + matcher + .fuzzy_match(code, src) + .max(matcher.fuzzy_match(name, src)) + .map(|score| (code, name, score)) + }) + .collect::>(); + + let mut matches = src_matches + .into_iter() + .flat_map(|src| dest_matches.clone().into_iter().map(move |dest| (Some(src), dest))) + .collect::>(); + + matches.sort_by(|a, b| (b.1 .2 + b.0.unwrap().2).cmp(&(a.1 .2 + a.0.unwrap().2))); + matches + } + None => { + let mut matches = dest_matches + .into_iter() + .map(|dest| (None, dest)) + .collect::>(); + + matches.sort_by(|a, b| b.1 .2.cmp(&a.1 .2)); + matches + } + }; // We only want 3 matches - matches.truncate(data.config.max_entries); + matches.truncate(state.config.max_entries); - tokio::runtime::Runtime::new().expect("Failed to spawn tokio runtime!").block_on(async move { + state.runtime.block_on(async move { // Create the futures for fetching the translation results let futures = matches .into_iter() - .map(|(code, name, _)| async move { - (name, reqwest::get(format!("https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={}&dt=t&q={}", code, text)).await) + .map(|(src, dest)| async move { + match src { + Some(src) => + (dest.1, state.client.get(format!("https://translate.googleapis.com/translate_a/single?client=gtx&sl={}&tl={}&dt=t&q={}", src.0, dest.0, text)).send().await), + None => (dest.1, state.client.get(format!("https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={}&dt=t&q={}", dest.0, text)).send().await) + } }); - futures::future::join_all(futures) // Wait for all futures to complete - .await + + let res = futures::future::join_all(futures) // Wait for all futures to complete + .await; + + res .into_iter() .filter_map(|(name, res)| res .ok() @@ -215,7 +266,7 @@ fn get_matches(input: RString, data: &State) -> RVec { description: ROption::RSome( format!( "{} -> {}", - data.langs.iter() + state.langs.iter() .find_map(|(code, name)| if *code == json[2].as_str().expect("Malformed JSON!") { Some(*name) } else {