From 6ce81a56d59de25fb4ef63a2e7a28ae08d558bc7 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sun, 22 Jun 2025 22:10:14 +0200 Subject: [PATCH] breaking api changes: improve API for gui and reduce cloning * change signature of gui::show to include option with factory trait to create new element instead of boolean * change signature of get_[sub_]elements the newer signature makes it way clearer what the semantic behind the value is. * improve how completion with tab is handled * added config option, so tab can be used to submit --- Cargo.lock | 153 +++++----- Cargo.toml | 1 + examples/worf-hyprswitch/Cargo.toml | 4 +- examples/worf-hyprswitch/src/main.rs | 52 +++- examples/worf-warden/src/main.rs | 31 +- worf/src/lib/config.rs | 21 +- worf/src/lib/gui.rs | 438 +++++++++++++++------------ worf/src/lib/modes/auto.rs | 106 ++++--- worf/src/lib/modes/dmenu.rs | 36 ++- worf/src/lib/modes/drun.rs | 41 ++- worf/src/lib/modes/emoji.rs | 37 ++- worf/src/lib/modes/file.rs | 35 ++- worf/src/lib/modes/math.rs | 42 ++- worf/src/lib/modes/run.rs | 50 ++- worf/src/lib/modes/search.rs | 35 ++- worf/src/lib/modes/ssh.rs | 41 ++- worf/src/main.rs | 21 +- 17 files changed, 700 insertions(+), 444 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58411ab..92c22ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -361,9 +371,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.38" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -371,9 +381,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -383,9 +393,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -1462,6 +1472,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -1941,6 +1961,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" @@ -2069,6 +2095,25 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.101", +] + [[package]] name = "syn" version = "1.0.109" @@ -2093,15 +2138,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.34.2" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", - "windows 0.57.0", + "objc2-io-kit", + "windows", ] [[package]] @@ -2131,7 +2177,7 @@ checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml", "thiserror 2.0.12", - "windows 0.61.1", + "windows", "windows-version", ] @@ -2501,16 +2547,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.1" @@ -2518,7 +2554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link", "windows-numerics", @@ -2530,19 +2566,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -2551,10 +2575,10 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.3.4", + "windows-result", "windows-strings", ] @@ -2564,22 +2588,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link", "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -2591,17 +2604,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "windows-interface" version = "0.59.1" @@ -2625,19 +2627,10 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -2867,7 +2860,7 @@ dependencies = [ [[package]] name = "worf" -version = "0.2.0" +version = "0.3.0" dependencies = [ "clap", "crossbeam", @@ -2896,15 +2889,29 @@ dependencies = [ "wl-clipboard-rs", ] +[[package]] +name = "worf-hyprspace" +version = "0.1.0" +dependencies = [ + "Inflector", + "clap", + "env_logger", + "hyprland", + "log", + "regex", + "serde", + "strum", + "strum_macros", + "worf", +] + [[package]] name = "worf-hyprswitch" version = "0.1.0" dependencies = [ - "dirs 6.0.0", "env_logger", "freedesktop-icons", "hyprland", - "log", "rayon", "sysinfo", "toml", diff --git a/Cargo.toml b/Cargo.toml index fbd68fa..fc39350 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "worf", "examples/worf-warden", "examples/worf-hyprswitch", + "examples/worf-hyprspace", ] resolver = "3" diff --git a/examples/worf-hyprswitch/Cargo.toml b/examples/worf-hyprswitch/Cargo.toml index c065309..8961cfd 100644 --- a/examples/worf-hyprswitch/Cargo.toml +++ b/examples/worf-hyprswitch/Cargo.toml @@ -7,9 +7,7 @@ edition = "2024" worf = {path = "../../worf"} env_logger = "0.11.8" hyprland = "0.4.0-beta.2" -sysinfo = "0.34.2" +sysinfo = "0.35.2" freedesktop-icons = "0.4.0" rayon = "1.10.0" toml = "0.8.22" -log = "0.4.27" -dirs = "6.0.0" diff --git a/examples/worf-hyprswitch/src/main.rs b/examples/worf-hyprswitch/src/main.rs index a886623..100950f 100644 --- a/examples/worf-hyprswitch/src/main.rs +++ b/examples/worf-hyprswitch/src/main.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc, thread}; +use std::{ + collections::HashMap, + env, fs, + path::PathBuf, + sync::{Arc, Mutex, RwLock}, + thread, +}; use hyprland::{ dispatch::{DispatchType, WindowIdentifier}, @@ -12,7 +18,7 @@ use worf::{ config::{self, Config}, desktop, desktop::EntryType, - gui::{self, ItemProvider, MenuItem}, + gui::{self, ExpandMode, ItemProvider, MenuItem, ProviderData}, }; #[derive(Clone)] @@ -108,12 +114,18 @@ impl WindowProvider { } impl ItemProvider for WindowProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { - (false, self.windows.clone()) + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: Some(self.windows.clone()), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -132,15 +144,21 @@ fn main() -> Result<(), String> { .init(); let args = config::parse_args(); - let config = config::load_config(Some(&args)).unwrap_or(args); + let config = Arc::new(RwLock::new( + config::load_config(Some(&args)).unwrap_or(args), + )); - let cache_path = - desktop::cache_file_path(&config, "worf-hyprswitch").map_err(|err| err.to_string())?; + let cache_path = desktop::cache_file_path(&config.read().unwrap(), "worf-hyprswitch") + .map_err(|err| err.to_string())?; let mut cache = load_icon_cache(&cache_path).map_err(|e| e.to_string())?; - let provider = WindowProvider::new(&config, &cache)?; - let windows = provider.windows.clone(); - let result = gui::show(config, provider, false, None, None).map_err(|e| e.to_string())?; + let provider = Arc::new(Mutex::new(WindowProvider::new( + &config.read().unwrap(), + &cache, + )?)); + let windows = provider.lock().unwrap().windows.clone(); + let result = gui::show(config, provider, None, None, ExpandMode::Verbatim, None) + .map_err(|e| e.to_string())?; let update_cache = thread::spawn(move || { windows.iter().for_each(|item| { if let Some(window) = &item.data { @@ -158,13 +176,15 @@ fn main() -> Result<(), String> { } }); - if let Some(window) = result.menu.data { + let return_value = if let Some(window) = result.menu.data { hyprland::dispatch::Dispatch::call(DispatchType::FocusWindow(WindowIdentifier::Address( window.address, ))) - .map_err(|e| e.to_string())?; - Ok(update_cache.join().unwrap().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string()) } else { Err("No window data found".to_owned()) - } + }; + + update_cache.join().unwrap().map_err(|e| e.to_string())?; + return_value } diff --git a/examples/worf-warden/src/main.rs b/examples/worf-warden/src/main.rs index 001f8c1..125c3aa 100644 --- a/examples/worf-warden/src/main.rs +++ b/examples/worf-warden/src/main.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, env, process::Command, thread::sleep, time::Duration}; +use std::{collections::HashMap, env, process::Command, thread::sleep, time::Duration, sync::{Arc, Mutex, RwLock}}; use worf::{ config::{self, Config, CustomKeyHintLocation, Key}, desktop::{copy_to_clipboard, spawn_fork}, - gui::{self, CustomKeyHint, CustomKeys, ItemProvider, KeyBinding, MenuItem, Modifier}, + gui::{self, CustomKeyHint, CustomKeys, ItemProvider, KeyBinding, MenuItem, Modifier, ProviderData, ExpandMode}, }; #[derive(Clone)] @@ -75,15 +75,21 @@ impl PasswordProvider { } impl ItemProvider for PasswordProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { - (false, self.items.clone()) + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: Some(self.items.clone()), + } + } } fn get_sub_elements( &mut self, _: &MenuItem, - ) -> (bool, Option>>) { - (false, None) + ) -> ProviderData { + ProviderData { items: None } } } @@ -265,12 +271,13 @@ fn key_lock() -> KeyBinding { } } -fn show(config: Config, provider: PasswordProvider) -> Result<(), String> { +fn show(config: Arc>, provider: Arc>) -> Result<(), String> { match gui::show( - config.clone(), + Arc::clone(&config), provider, - false, None, + None, + ExpandMode::Verbatim, Some(CustomKeys { bindings: vec![ key_type_all(), @@ -294,7 +301,7 @@ fn show(config: Config, provider: PasswordProvider) -> Result<(), String> { Ok(selection) => { if let Some(meta) = selection.menu.data { if meta.ids.len() > 1 { - return show(config, PasswordProvider::sub_provider(meta.ids)?); + return show(config, Arc::new(Mutex::new(PasswordProvider::sub_provider(meta.ids)?))); } let id = meta.ids.first().unwrap_or(&selection.menu.label); @@ -344,7 +351,7 @@ fn main() -> Result<(), String> { .init(); let args = config::parse_args(); - let config = config::load_config(Some(&args)).unwrap_or(args); + let config = Arc::new(RwLock::new(config::load_config(Some(&args)).unwrap_or(args))); if !groups().contains("input") { log::error!( @@ -359,6 +366,6 @@ fn main() -> Result<(), String> { } // todo eventually use a propper rust client for this, for now rbw is good enough - let provider = PasswordProvider::new(&config)?; + let provider = Arc::new(Mutex::new(PasswordProvider::new(&config.read().unwrap())?)); show(config, provider) } diff --git a/worf/src/lib/config.rs b/worf/src/lib/config.rs index ab4a0fa..edd47eb 100644 --- a/worf/src/lib/config.rs +++ b/worf/src/lib/config.rs @@ -420,7 +420,9 @@ impl FromStr for Key { } #[derive(Debug, Deserialize, Serialize, Clone, Parser)] -#[clap(about = "Worf is a wofi clone written in rust, it aims to be a drop-in replacement")] +#[clap( + about = "Worf is a wofi like launcher, written in rust, it aims to be a drop-in replacement" +)] #[derive(Default)] pub struct Config { /// Forks the menu so you can close the terminal @@ -681,6 +683,10 @@ pub struct Config { /// Defaults to false. #[clap(long = "blurred-background-fullscreen")] blurred_background_fullscreen: Option, + + /// Allow submitting selected entry with expand key if there is only 1 item left. + #[clap(long = "submit-with-expand")] + submit_with_expand: Option, } impl Config { @@ -786,6 +792,10 @@ impl Config { } } + pub fn set_prompt(&mut self, val: String) { + self.prompt = Some(val); + } + #[must_use] pub fn height(&self) -> String { self.height.clone().unwrap_or("40%".to_owned()) @@ -978,16 +988,17 @@ impl Config { pub fn blurred_background_fullscreen(&self) -> bool { self.blurred_background_fullscreen.unwrap_or(false) } + + #[must_use] + pub fn submit_with_expand(&self) -> bool { + self.submit_with_expand.unwrap_or(true) + } } fn default_false() -> bool { false } -// fn default_true() -> bool { -// true -// } - #[must_use] pub fn parse_args() -> Config { Config::parse() diff --git a/worf/src/lib/gui.rs b/worf/src/lib/gui.rs index 6e54a8f..7090123 100644 --- a/worf/src/lib/gui.rs +++ b/worf/src/lib/gui.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + marker::PhantomData, rc::Rc, sync::{Arc, Mutex, RwLock}, thread, @@ -39,8 +40,9 @@ use crate::{ desktop::known_image_extension_regex_pattern, }; -type ArcMenuMap = Arc>>>; -type ArcProvider = Arc + Send>>; +pub type ArcMenuMap = Arc>>>; +pub type ArcProvider = Arc + Send>>; +pub type ArcFactory = Arc + Send>>; pub struct Selection { pub menu: MenuItem, @@ -48,9 +50,42 @@ pub struct Selection { } type SelectionSender = Sender, Error>>; +pub struct ProviderData { + pub items: Option>>, +} + pub trait ItemProvider { - fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>); - fn get_sub_elements(&mut self, item: &MenuItem) -> (bool, Option>>); + fn get_elements(&mut self, search: Option<&str>) -> ProviderData; + + /// Get elements below the given menu entry. + /// Will be called for completion + /// If (true, None) is returned and submit-accept is set in the config, this + /// will be handled the name way as pressing enter (or the configured submit key). + fn get_sub_elements(&mut self, item: &MenuItem) -> ProviderData; +} + +pub trait ItemFactory { + fn new_menu_item(&self, label: String) -> Option>; +} + +/// Default generic item factory that creates an almost empty menu item +/// Without data, no icon, and sort score of 0. +pub struct DefaultItemFactory { + _marker: PhantomData, +} + +impl DefaultItemFactory { + pub fn new() -> DefaultItemFactory { + DefaultItemFactory:: { + _marker: Default::default(), + } + } +} + +impl ItemFactory for DefaultItemFactory { + fn new_menu_item(&self, label: String) -> Option> { + Some(MenuItem::new(label, None, None, vec![], None, 0.0, None)) + } } impl From<&Anchor> for Edge { @@ -121,6 +156,10 @@ pub struct MenuItem { /// Allows to store arbitrary additional information pub data: Option, + // /// If set to true, the item is _not_ an intermediate thing + // /// and is acceptable, i.e. will close the UI + // pub allow_submit: bool, + // todo /// Score the item got in the current search search_sort_score: f64, /// True if the item is visible @@ -351,6 +390,12 @@ pub enum Modifier { None, } +#[derive(PartialEq)] +pub enum ExpandMode { + Verbatim, + WithSpace, +} + fn modifiers_from_mask(mask: gdk4::ModifierType) -> HashSet { let mut modifiers = HashSet::new(); @@ -421,6 +466,7 @@ impl MenuItem { working_dir: Option, initial_sort_score: f64, data: Option, + //allow_submit: bool, ) -> Self { MenuItem { label, @@ -430,6 +476,7 @@ impl MenuItem { working_dir, initial_sort_score, data, + //allow_submit, search_sort_score: 0.0, visible: true, } @@ -444,10 +491,11 @@ impl AsRef> for MenuItem { struct MetaData { item_provider: ArcProvider, + item_factory: Option>, selected_sender: SelectionSender, - config: Rc, - new_on_empty: bool, + config: Arc>, search_ignored_words: Option>, + expand_mode: ExpandMode, } struct UiElements { @@ -468,20 +516,20 @@ struct UiElements { /// # Errors /// /// Will return Err when the channel between the UI and this is broken -pub fn show( - config: Config, - item_provider: P, - new_on_empty: bool, +pub fn show( + config: Arc>, + item_provider: ArcProvider, + item_factory: Option>, search_ignored_words: Option>, + expand_mode: ExpandMode, custom_keys: Option, ) -> Result, Error> where T: Clone + 'static + Send, - P: ItemProvider + 'static + Clone + Send, { gtk4::init().map_err(|e| Error::Graphics(e.to_string()))?; log::debug!("Starting GUI"); - if let Some(ref css) = config.style() { + if let Some(ref css) = config.read().unwrap().style() { log::debug!("loading css from {css}"); let provider = CssProvider::new(); let css_file_path = File::for_path(css); @@ -498,16 +546,18 @@ where let app = Application::builder().application_id("worf").build(); let (sender, receiver) = channel::bounded(1); + let meta = Rc::new(MetaData { + item_provider, + item_factory, + selected_sender: sender, + config: config.clone(), + search_ignored_words, + expand_mode, + }); + + let connect_cfg = Arc::clone(&config); app.connect_activate(move |app| { - build_ui( - &config, - item_provider.clone(), - sender.clone(), - app.clone(), - new_on_empty, - search_ignored_words.clone(), - custom_keys.as_ref(), - ); + build_ui::(&connect_cfg, &meta, app.clone(), custom_keys.as_ref()); }); let gtk_args: [&str; 0] = []; @@ -524,28 +574,16 @@ where receiver_result? } -fn build_ui( - config: &Config, - item_provider: P, - sender: Sender, Error>>, +fn build_ui( + config: &Arc>, + meta: &Rc>, app: Application, - new_on_empty: bool, - search_ignored_words: Option>, custom_keys: Option<&CustomKeys>, ) where T: Clone + 'static + Send, - P: ItemProvider + 'static + Send, { let start = Instant::now(); - let meta = Rc::new(MetaData { - item_provider: Arc::new(Mutex::new(item_provider)), - selected_sender: sender, - config: Rc::new(config.clone()), - new_on_empty, - search_ignored_words, - }); - let provider_clone = Arc::clone(&meta.item_provider); let get_provider_elements = thread::spawn(move || { log::debug!("getting items"); @@ -560,7 +598,7 @@ fn build_ui( .default_height(1) .build(); - let background = create_background(config); + let background = create_background(&config.read().unwrap()); let ui_elements = Rc::new(UiElements { app, @@ -571,20 +609,22 @@ fn build_ui( menu_rows: Arc::new(RwLock::new(HashMap::new())), search_text: Arc::new(Mutex::new(String::new())), search_delete_event: Arc::new(Mutex::new(None)), - outer_box: gtk4::Box::new(config.orientation().into(), 0), + outer_box: gtk4::Box::new(config.read().unwrap().orientation().into(), 0), scroll: ScrolledWindow::new(), custom_key_box: gtk4::Box::new(Orientation::Vertical, 0), }); // handle keys as soon as possible - setup_key_event_handler(&ui_elements, &meta, custom_keys); + setup_key_event_handler(&ui_elements, meta, custom_keys); log::debug!("keyboard ready after {:?}", start.elapsed()); - if !config.normal_window() { + if !config.read().unwrap().normal_window() { // Initialize the window as a layer ui_elements.window.init_layer_shell(); - ui_elements.window.set_layer(config.layer().into()); + ui_elements + .window + .set_layer(config.read().unwrap().layer().into()); ui_elements .window .set_keyboard_mode(KeyboardMode::Exclusive); @@ -593,7 +633,7 @@ fn build_ui( ui_elements.window.set_widget_name("window"); ui_elements.window.set_namespace(Some("worf")); - if let Some(location) = config.location() { + if let Some(location) = config.read().unwrap().location() { for anchor in location { ui_elements.window.set_anchor(anchor.into(), true); } @@ -615,31 +655,33 @@ fn build_ui( ui_elements.scroll.set_hexpand(true); ui_elements.scroll.set_vexpand(true); - if config.hide_scroll() { + if config.read().unwrap().hide_scroll() { ui_elements .scroll .set_policy(PolicyType::External, PolicyType::External); } ui_elements.outer_box.append(&ui_elements.scroll); - build_main_box(config, &ui_elements); - build_search_entry(config, &ui_elements, &meta); + build_main_box(&config.read().unwrap(), &ui_elements); + build_search_entry(&config.read().unwrap(), &ui_elements, meta); let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0); wrapper_box.append(&ui_elements.main_box); ui_elements.scroll.set_child(Some(&wrapper_box)); let wait_for_items = Instant::now(); - let (_changed, provider_elements) = get_provider_elements.join().unwrap(); + let provider_elements = get_provider_elements.join().unwrap(); log::debug!("got items after {:?}", wait_for_items.elapsed()); let cfg = config.clone(); let ui = Rc::clone(&ui_elements); ui_elements.window.connect_is_active_notify(move |_| { - window_show_resize(&cfg.clone(), &ui); + window_show_resize(&cfg.read().unwrap(), &ui); }); - build_ui_from_menu_items(&ui_elements, &meta, provider_elements); + if let Some(elements) = provider_elements.items { + build_ui_from_menu_items(&ui_elements, meta, elements); + } let window_start = Instant::now(); ui_elements.window.present(); @@ -833,7 +875,7 @@ fn set_search_text( search_stop_listen_delete_event(ui); let mut lock = ui.search_text.lock().unwrap(); query.clone_into(&mut lock); - if let Some(pw) = meta.config.password() { + if let Some(pw) = meta.config.read().unwrap().password() { let mut ui_text = String::new(); for _ in 0..query.len() { ui_text += &pw; @@ -850,7 +892,7 @@ fn build_ui_from_menu_items( meta: &Rc>, mut items: Vec>, ) { - if meta.config.sort_order() != SortOrder::Default { + if meta.config.read().unwrap().sort_order() != SortOrder::Default { items.reverse(); } let start = Instant::now(); @@ -968,7 +1010,7 @@ fn handle_key_press( // hide search let propagate = if is_key_match( - meta.config.key_hide_search(), + meta.config.read().unwrap().key_hide_search(), &detection_type, key_code, keyboard_key, @@ -976,7 +1018,7 @@ fn handle_key_press( handle_key_hide_search(ui) // submit } else if is_key_match( - Some(meta.config.key_submit()), + Some(meta.config.read().unwrap().key_submit()), &detection_type, key_code, keyboard_key, @@ -985,7 +1027,7 @@ fn handle_key_press( } // exit else if is_key_match( - Some(meta.config.key_exit()), + Some(meta.config.read().unwrap().key_exit()), &detection_type, key_code, keyboard_key, @@ -993,7 +1035,7 @@ fn handle_key_press( handle_key_exit(ui, meta) // copy } else if is_key_match( - meta.config.key_copy(), + meta.config.read().unwrap().key_copy(), &detection_type, key_code, keyboard_key, @@ -1001,7 +1043,7 @@ fn handle_key_press( handle_key_copy(ui, meta) // expand } else if is_key_match( - Some(meta.config.key_expand()), + Some(meta.config.read().unwrap().key_expand()), &detection_type, key_code, keyboard_key, @@ -1029,7 +1071,7 @@ fn handle_key_press( } else { pos }; - if let Some((start, ch)) = query.char_indices().nth((del_pos) as usize) { + if let Some((start, ch)) = query.char_indices().nth(del_pos as usize) { let end = start + ch.len_utf8(); query.replace_range(start..end, ""); } @@ -1084,7 +1126,7 @@ fn handle_custom_keys( modifier_type: gdk4::ModifierType, custom_keys: Option<&CustomKeys>, ) -> KeyDetectionType { - let detection_type = meta.config.key_detection_type(); + let detection_type = meta.config.read().unwrap().key_detection_type(); if let Some(custom_keys) = custom_keys { let mods = modifiers_from_mask(modifier_type); for custom_key in &custom_keys.bindings { @@ -1098,14 +1140,9 @@ fn handle_custom_keys( if custom_key_match { let search_lock = ui.search_text.lock().unwrap(); - if let Err(e) = handle_selected_item( - ui, - Rc::>::clone(meta), - Some(&search_lock), - None, - meta.new_on_empty, - Some(custom_key), - ) { + if let Err(e) = + handle_selected_item(ui, meta, Some(&search_lock), None, Some(custom_key)) + { log::error!("{e}"); } } @@ -1118,8 +1155,8 @@ fn update_view_from_provider(ui: &Rc>, meta: &Rc>, where T: Clone + Send + 'static, { - let (changed, filtered_list) = meta.item_provider.lock().unwrap().get_elements(Some(query)); - if changed { + let data = meta.item_provider.lock().unwrap().get_elements(Some(query)); + if let Some(filtered_list) = data.items { build_ui_from_menu_items(ui, meta, filtered_list); } update_view(ui, meta, query); @@ -1139,9 +1176,10 @@ where select_first_visible_child(&*lock, &ui.main_box); drop(lock); - if meta.config.dynamic_lines() { + if meta.config.read().unwrap().dynamic_lines() { if let Some(geometry) = get_monitor_geometry(ui.window.surface().as_ref()) { - let height = calculate_dynamic_lines_window_height(&meta.config, ui, geometry); + let height = + calculate_dynamic_lines_window_height(&meta.config.read().unwrap(), ui, geometry); ui.window.set_height_request(height); } } @@ -1168,7 +1206,7 @@ where if let Some(expander) = expander { expander.set_expanded(true); } else { - let opt_changed = { + let data = { let lock = ui.menu_rows.read().unwrap(); let menu_item = lock.get(fb); menu_item.map(|menu_item| { @@ -1177,20 +1215,30 @@ where .lock() .unwrap() .get_sub_elements(menu_item), - menu_item.label.clone(), + menu_item.clone(), ) }) }; - if let Some(changed) = opt_changed { - let items = changed.0.1.unwrap_or_default(); - if changed.0.0 { + if let Some((provider_data, menu_item)) = data { + if let Some(items) = provider_data.items { build_ui_from_menu_items(ui, meta, items); - } + let query = match meta.expand_mode { + ExpandMode::Verbatim => menu_item.label.clone(), + ExpandMode::WithSpace => format!("{} ", menu_item.label.clone()), + }; - let query = changed.1; - set_search_text(ui, meta, &query); - update_view(ui, meta, &query); + set_search_text(ui, meta, &query); + if let Ok(new_pos) = i32::try_from(query.len() + 1) { + ui.search.set_position(new_pos); + } + + update_view(ui, meta, &query); + } else if let Err(e) = + handle_selected_item(ui, meta, None, Some(menu_item), None) + { + log::error!("{e}"); + } } } } @@ -1221,14 +1269,7 @@ where T: Clone + Send + 'static, { let search_lock = ui.search_text.lock().unwrap(); - if let Err(e) = handle_selected_item( - ui, - Rc::>::clone(meta), - Some(&search_lock), - None, - meta.new_on_empty, - None, - ) { + if let Err(e) = handle_selected_item(ui, meta, Some(&search_lock), None, None) { log::error!("{e}"); } Propagation::Stop @@ -1431,10 +1472,9 @@ where } fn handle_selected_item( ui: &Rc>, - meta: Rc>, + meta: &Rc>, query: Option<&str>, item: Option>, - new_on_empty: bool, custom_key: Option<&KeyBinding>, ) -> Result<(), String> where @@ -1448,37 +1488,31 @@ where return Ok(()); } - if new_on_empty { - let item = MenuItem { - label: query.unwrap_or("").to_owned(), - icon_path: None, - action: None, - sub_elements: Vec::new(), - working_dir: None, - initial_sort_score: 0.0, - search_sort_score: 0.0, - data: None, - visible: true, - }; - - send_selected_item(ui, meta, custom_key.cloned(), item); - Ok(()) - } else { - Err("selected item cannot be resolved".to_owned()) + if let Some(factory) = meta.item_factory.as_ref() { + let factory = factory.lock().unwrap(); + let label = filtered_query(meta.search_ignored_words.as_ref(), query.unwrap_or("")); + let item = factory.new_menu_item(label); + if let Some(item) = item { + send_selected_item(ui, meta, custom_key.cloned(), item); + return Ok(()); + } } + + Err("selected item cannot be resolved".to_owned()) } fn send_selected_item( ui: &Rc>, - meta: Rc>, + meta: &Rc>, custom_key: Option, selected_item: MenuItem, ) where T: Clone + Send + 'static, { let ui_clone = Rc::clone(ui); + let meta_clone = Rc::clone(meta); ui.window.connect_hide(move |_| { - if let Err(e) = meta.selected_sender.send(Ok(Selection { + if let Err(e) = meta_clone.selected_sender.send(Ok(Selection { menu: selected_item.clone(), custom_key: custom_key.clone(), })) { @@ -1548,7 +1582,7 @@ fn create_menu_row( row.set_halign(Align::Fill); row.set_widget_name("row"); - let row_box = gtk4::Box::new(meta.config.row_box_orientation().into(), 0); + let row_box = gtk4::Box::new(meta.config.read().unwrap().row_box_orientation().into(), 0); row_box.set_hexpand(true); row_box.set_vexpand(false); row_box.set_halign(Align::Fill); @@ -1557,15 +1591,13 @@ fn create_menu_row( let (label_img, label_text) = parse_label(&element_to_add.label); - if meta.config.allow_images() { + let config = meta.config.read().unwrap(); + if meta.config.read().unwrap().allow_images() { let img = lookup_icon( element_to_add.icon_path.as_ref().map(AsRef::as_ref), - &meta.config, + &config, ) - .or(lookup_icon( - label_img.as_ref().map(AsRef::as_ref), - &meta.config, - )); + .or(lookup_icon(label_img.as_ref().map(AsRef::as_ref), &config)); if let Some(image) = img { image.set_widget_name("img"); @@ -1574,16 +1606,16 @@ fn create_menu_row( } let label = Label::new(label_text.as_ref().map(AsRef::as_ref)); - label.set_use_markup(meta.config.allow_markup()); - label.set_natural_wrap_mode(meta.config.line_wrap().into()); + label.set_use_markup(meta.config.read().unwrap().allow_markup()); + label.set_natural_wrap_mode(meta.config.read().unwrap().line_wrap().into()); label.set_hexpand(true); label.set_widget_name("text"); label.set_wrap(true); - if let Some(max_width_chars) = meta.config.line_max_width_chars() { + if let Some(max_width_chars) = meta.config.read().unwrap().line_max_width_chars() { label.set_max_width_chars(max_width_chars); } - if let Some(max_len) = meta.config.line_max_chars() { + if let Some(max_len) = meta.config.read().unwrap().line_max_chars() { if let Some(text) = label_text.as_ref() { if text.chars().count() > max_len { let end = text @@ -1597,8 +1629,18 @@ fn create_menu_row( row_box.append(&label); - if meta.config.content_halign().eq(&config::Align::Start) - || meta.config.content_halign().eq(&config::Align::Fill) + if meta + .config + .read() + .unwrap() + .content_halign() + .eq(&config::Align::Start) + || meta + .config + .read() + .unwrap() + .content_halign() + .eq(&config::Align::Fill) { label.set_xalign(0.0); } @@ -1610,16 +1652,19 @@ fn create_menu_row( let click = GestureClick::new(); click.set_button(gtk4::gdk::BUTTON_PRIMARY); - let presses = if meta.config.single_click() { 1 } else { 2 }; + let presses = if meta.config.read().unwrap().single_click() { + 1 + } else { + 2 + }; click.connect_pressed(move |_gesture, n_press, _x, _y| { if n_press == presses { if let Err(e) = handle_selected_item( &click_ui, - Rc::>::clone(&click_meta), + &click_meta, None, Some(element_clone.clone()), - false, None, ) { log::error!("{e}"); @@ -1703,82 +1748,89 @@ fn lookup_icon(icon_path: Option<&str>, config: &Config) -> Option { fn set_menu_visibility_for_search( query: &str, items: &mut HashMap>, - config: &Config, + config: &Arc>, search_ignored_words: Option<&Vec>, ) { - { - if query.is_empty() { - for (fb, menu_item) in items.iter_mut() { - menu_item.search_sort_score = 0.0; - menu_item.visible = true; - fb.set_visible(menu_item.visible); - } - return; - } - - let mut query = if config.insensitive() { - query.to_owned().to_lowercase() - } else { - query.to_owned() - }; - - if let Some(s) = search_ignored_words.as_ref() { - s.iter().for_each(|rgx| { - query = rgx.replace_all(&query, "").to_string(); - }); - } - + if query.is_empty() { for (fb, menu_item) in items.iter_mut() { - let menu_item_search = format!( - "{} {}", - menu_item - .action - .as_ref() - .map(|a| { - if config.insensitive() { - a.to_lowercase() - } else { - a.clone() - } - }) - .unwrap_or_default(), - if config.insensitive() { - menu_item.label.to_lowercase() - } else { - menu_item.label.clone() - } - ); - - let (search_sort_score, visible) = match config.match_method() { - MatchMethod::Fuzzy => { - let mut score = strsim::jaro_winkler(&query, &menu_item_search); - if score == 0.0 { - score = -1.0; - } - - (score, score > config.fuzzy_min_score() && score > 0.0) - } - MatchMethod::Contains => { - if menu_item_search.contains(&query) { - (1.0, true) - } else { - (0.0, false) - } - } - MatchMethod::MultiContains => { - let contains = query.split(' ').all(|x| menu_item_search.contains(x)); - (if contains { 1.0 } else { 0.0 }, contains) - } - MatchMethod::None => { - (1.0, true) // items are always shown - } - }; - - menu_item.search_sort_score = search_sort_score + menu_item.initial_sort_score; - menu_item.visible = visible; + menu_item.search_sort_score = 0.0; + menu_item.visible = true; fb.set_visible(menu_item.visible); } } + + let mut query = if config.read().unwrap().insensitive() { + query.to_owned().to_lowercase() + } else { + query.to_owned() + }; + + query = filtered_query(search_ignored_words, &query); + + for (fb, menu_item) in items.iter_mut() { + let menu_item_search = format!( + "{} {}", + menu_item + .action + .as_ref() + .map(|a| { + if config.read().unwrap().insensitive() { + a.to_lowercase() + } else { + a.clone() + } + }) + .unwrap_or_default(), + if config.read().unwrap().insensitive() { + menu_item.label.to_lowercase() + } else { + menu_item.label.clone() + } + ); + + let (search_sort_score, visible) = match config.read().unwrap().match_method() { + MatchMethod::Fuzzy => { + let mut score = strsim::jaro_winkler(&query, &menu_item_search); + if score == 0.0 { + score = -1.0; + } + + ( + score, + score > config.read().unwrap().fuzzy_min_score() && score > 0.0, + ) + } + MatchMethod::Contains => { + if menu_item_search.contains(&query) { + (1.0, true) + } else { + (0.0, false) + } + } + MatchMethod::MultiContains => { + let contains = query.split(' ').all(|x| menu_item_search.contains(x)); + (if contains { 1.0 } else { 0.0 }, contains) + } + MatchMethod::None => { + (1.0, true) // items are always shown + } + }; + + menu_item.search_sort_score = search_sort_score + menu_item.initial_sort_score; + menu_item.visible = visible; + fb.set_visible(menu_item.visible); + } +} + +#[must_use] +pub fn filtered_query(search_ignored_words: Option<&Vec>, query: &str) -> String { + let mut query = query.to_owned(); + if let Some(s) = search_ignored_words.as_ref() { + s.iter().for_each(|rgx| { + query = rgx.replace_all(&query, "").to_string(); + }); + } + query } fn select_first_visible_child( diff --git a/worf/src/lib/modes/auto.rs b/worf/src/lib/modes/auto.rs index eac176d..ffe9a41 100644 --- a/worf/src/lib/modes/auto.rs +++ b/worf/src/lib/modes/auto.rs @@ -1,10 +1,11 @@ use regex::Regex; +use std::sync::{Arc, Mutex, RwLock}; use crate::{ Error, config::Config, desktop::spawn_fork, - gui::{self, ItemProvider, MenuItem}, + gui::{self, DefaultItemFactory, ExpandMode, ItemProvider, MenuItem, ProviderData}, modes::{ drun::{DRunProvider, update_drun_cache_and_run}, file::FileItemProvider, @@ -14,6 +15,7 @@ use crate::{ ssh::SshProvider, }, }; +use crate::gui::ArcProvider; #[derive(Debug, Clone, PartialEq)] enum AutoRunType { @@ -22,6 +24,7 @@ enum AutoRunType { File, Ssh, WebSearch, + Auto, } #[derive(Clone)] @@ -46,18 +49,25 @@ impl AutoItemProvider { } } - fn default_auto_elements( - &mut self, - search_opt: Option<&str>, - ) -> (bool, Vec>) { + fn default_auto_elements(&mut self) -> ProviderData { // return ssh and drun items - let (changed, mut items) = self.drun.get_elements(search_opt); - items.append(&mut self.ssh.get_elements(search_opt).1); - if self.last_mode == Some(AutoRunType::DRun) { - (changed, items) + if self.last_mode.is_none() + || self + .last_mode + .as_ref() + .is_some_and(|t| t != &AutoRunType::Auto) + { + let mut data = self.drun.get_elements(None); + if let Some(items) = data.items.as_mut() { + if let Some(mut ssh) = self.ssh.get_elements(None).items { + items.append(&mut ssh); + } + } + + self.last_mode = Some(AutoRunType::Auto); + data } else { - self.last_mode = Some(AutoRunType::DRun); - (true, items) + ProviderData { items: None } } } } @@ -76,13 +86,13 @@ fn contains_math_functions_or_starts_with_number(input: &str) -> bool { } impl ItemProvider for AutoItemProvider { - fn get_elements(&mut self, search_opt: Option<&str>) -> (bool, Vec>) { + fn get_elements(&mut self, search_opt: Option<&str>) -> ProviderData { let search = match search_opt { Some(s) if !s.trim().is_empty() => s.trim(), - _ => return self.default_auto_elements(search_opt), + _ => "", }; - let (mode, (changed, items)) = if contains_math_functions_or_starts_with_number(search) { + let (mode, provider_data) = if contains_math_functions_or_starts_with_number(search) { (AutoRunType::Math, self.math.get_elements(search_opt)) } else if search.starts_with('$') || search.starts_with('/') || search.starts_with('~') { (AutoRunType::File, self.file.get_elements(search_opt)) @@ -96,23 +106,34 @@ impl ItemProvider for AutoItemProvider { self.search.get_elements(Some(&query)), ) } else { - return self.default_auto_elements(search_opt); + (AutoRunType::Auto, self.default_auto_elements()) }; - if self.last_mode.as_ref().is_some_and(|m| m == &mode) { - (changed, items) - } else { - self.last_mode = Some(mode); - (true, items) - } + self.last_mode = Some(mode); + provider_data + + // if mode == AutoRunType::DRun && self.last_mode.as_ref().is_some_and(|l| l == &mode) { + // ProviderData { + // items: None, + // } + // } else { + // self.default_auto_elements() + // } } - fn get_sub_elements( - &mut self, - item: &MenuItem, - ) -> (bool, Option>>) { - let (changed, items) = self.get_elements(Some(item.label.as_ref())); - (changed, Some(items)) + fn get_sub_elements(&mut self, item: &MenuItem) -> ProviderData { + if let Some(auto_run_type) = item.data.as_ref() { + match auto_run_type { + AutoRunType::Math => self.math.get_sub_elements(item), + AutoRunType::DRun => self.drun.get_sub_elements(item), + AutoRunType::File => self.file.get_sub_elements(item), + AutoRunType::Ssh => self.ssh.get_sub_elements(item), + AutoRunType::WebSearch => self.search.get_sub_elements(item), + AutoRunType::Auto => ProviderData { items: None }, + } + } else { + ProviderData { items: None } + } } } @@ -124,23 +145,24 @@ impl ItemProvider for AutoItemProvider { /// /// # Panics /// Panics if an internal static regex cannot be passed anymore, should never happen -pub fn show(config: &Config) -> Result<(), Error> { - let mut provider = AutoItemProvider::new(config); - let cache_path = provider.drun.cache_path.clone(); - let mut cache = provider.drun.cache.clone(); +pub fn show(config: &Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(AutoItemProvider::new(&config.read().unwrap()))); + let arc_provider = Arc::clone(&provider) as ArcProvider; + let cache_path = provider.lock().unwrap().drun.cache_path.clone(); + let mut cache = provider.lock().unwrap().drun.cache.clone(); loop { - // todo ues a arc instead of cloning the config let selection_result = gui::show( - config.clone(), - provider.clone(), - true, + Arc::clone(&config), + Arc::clone(&arc_provider), + Some(Arc::new(Mutex::new(DefaultItemFactory::new()))), Some( vec!["ssh", "emoji", "^\\$\\w+", "^\\?\\s*"] .into_iter() .map(|s| Regex::new(s).unwrap()) .collect(), ), + ExpandMode::Verbatim, None, ); @@ -149,7 +171,12 @@ pub fn show(config: &Config) -> Result<(), Error> { if let Some(data) = &selection_result.data { match data { AutoRunType::Math => { - provider.math.elements.push(selection_result); + provider + .lock() + .unwrap() + .math + .elements + .push(selection_result); } AutoRunType::DRun => { update_drun_cache_and_run(&cache_path, &mut cache, selection_result)?; @@ -162,7 +189,7 @@ pub fn show(config: &Config) -> Result<(), Error> { break; } AutoRunType::Ssh => { - ssh::launch(&selection_result, config)?; + ssh::launch(&selection_result, &config.read().unwrap())?; break; } AutoRunType::WebSearch => { @@ -171,10 +198,13 @@ pub fn show(config: &Config) -> Result<(), Error> { } break; } + AutoRunType::Auto => { + unreachable!("Auto mode should never be set for show.") + } } } else if selection_result.label.starts_with("ssh") { selection_result.label = selection_result.label.chars().skip(4).collect(); - ssh::launch(&selection_result, config)?; + ssh::launch(&selection_result, &config.read().unwrap())?; } } else { log::error!("No item selected"); diff --git a/worf/src/lib/modes/dmenu.rs b/worf/src/lib/modes/dmenu.rs index a7929df..0aeff05 100644 --- a/worf/src/lib/modes/dmenu.rs +++ b/worf/src/lib/modes/dmenu.rs @@ -1,9 +1,12 @@ -use std::io::{self, Read}; +use std::{ + io::{self, Read}, + sync::{Arc, Mutex, RwLock}, +}; use crate::{ Error, config::{Config, SortOrder}, - gui::{self, ItemProvider, MenuItem}, + gui::{self, DefaultItemFactory, ExpandMode, ItemProvider, MenuItem, ProviderData}, }; #[derive(Clone)] @@ -30,12 +33,18 @@ impl DMenuProvider { } } impl ItemProvider for DMenuProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { - (false, self.items.clone()) + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: Some(self.items.clone()), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -43,10 +52,19 @@ impl ItemProvider for DMenuProvider { /// # Errors /// /// Forwards errors from the gui. See `gui::show` for details. -pub fn show(config: &Config) -> Result<(), Error> { - let provider = DMenuProvider::new(&config.sort_order()); +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(DMenuProvider::new( + &config.read().unwrap().sort_order(), + ))); - let selection_result = gui::show(config.clone(), provider, true, None, None); + let selection_result = gui::show( + config, + provider, + Some(Arc::new(Mutex::new(DefaultItemFactory::new()))), + None, + ExpandMode::Verbatim, + None, + ); match selection_result { Ok(s) => { println!("{}", s.menu.label); diff --git a/worf/src/lib/modes/drun.rs b/worf/src/lib/modes/drun.rs index 1cf6995..c4c0b99 100644 --- a/worf/src/lib/modes/drun.rs +++ b/worf/src/lib/modes/drun.rs @@ -1,6 +1,7 @@ use std::{ collections::{HashMap, HashSet}, path::PathBuf, + sync::{Arc, Mutex, RwLock}, time::Instant, }; @@ -8,6 +9,7 @@ use freedesktop_file_parser::EntryType; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use crate::gui::ArcProvider; use crate::{ Error, config::{Config, SortOrder}, @@ -15,7 +17,7 @@ use crate::{ find_desktop_files, get_locale_variants, lookup_name_with_locale, save_cache_file, spawn_fork, }, - gui::{self, ItemProvider, MenuItem}, + gui::{self, ExpandMode, ItemProvider, MenuItem, ProviderData}, modes::load_cache, }; @@ -37,15 +39,21 @@ pub(crate) struct DRunProvider { } impl ItemProvider for DRunProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { if self.items.is_none() { self.items = Some(self.load().clone()); } - (false, self.items.clone().unwrap()) + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: self.items.clone(), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -211,15 +219,22 @@ pub(crate) fn update_drun_cache_and_run( /// # Errors /// /// Will return `Err` if it was not able to spawn the process -pub fn show(config: &Config) -> Result<(), Error> { - let provider = DRunProvider::new(0, config); - let cache_path = provider.cache_path.clone(); - let mut cache = provider.cache.clone(); - - // todo ues a arc instead of cloning the config - let selection_result = gui::show(config.clone(), provider, false, None, None); +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(DRunProvider::new((), &config.read().unwrap()))); + let arc_provider = Arc::clone(&provider) as ArcProvider<()>; + let selection_result = gui::show( + config.clone(), + arc_provider, + None, + None, + ExpandMode::Verbatim, + None, + ); match selection_result { - Ok(s) => update_drun_cache_and_run(&cache_path, &mut cache, s.menu)?, + Ok(s) => { + let p = provider.lock().unwrap(); + update_drun_cache_and_run(&p.cache_path, &mut p.cache.clone(), s.menu)?; + } Err(_) => { log::error!("No item selected"); } diff --git a/worf/src/lib/modes/emoji.rs b/worf/src/lib/modes/emoji.rs index 6d217fd..23802e8 100644 --- a/worf/src/lib/modes/emoji.rs +++ b/worf/src/lib/modes/emoji.rs @@ -1,8 +1,10 @@ +use std::sync::{Arc, Mutex, RwLock}; + use crate::{ Error, config::{Config, SortOrder}, desktop::copy_to_clipboard, - gui::{self, ItemProvider, MenuItem}, + gui::{self, ExpandMode, ItemProvider, MenuItem, ProviderData}, }; #[derive(Clone)] @@ -41,12 +43,18 @@ impl EmojiProvider { } impl ItemProvider for EmojiProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { - (false, self.elements.clone()) + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: Some(self.elements.clone()), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -54,9 +62,22 @@ impl ItemProvider for EmojiProvider { /// # Errors /// /// Forwards errors from the gui. See `gui::show` for details. -pub fn show(config: &Config) -> Result<(), Error> { - let provider = EmojiProvider::new(&config.sort_order(), config.emoji_hide_label()); - let selection_result = gui::show(config.clone(), provider, true, None, None)?; +pub fn show(config: Arc>) -> Result<(), Error> { + let cfg = config.read().unwrap(); + let provider = Arc::new(Mutex::new(EmojiProvider::new( + &cfg.sort_order(), + cfg.emoji_hide_label(), + ))); + drop(cfg); + + let selection_result = gui::show( + config.clone(), + provider, + None, + None, + ExpandMode::Verbatim, + None, + )?; match selection_result.menu.data { None => Err(Error::MissingAction), Some(action) => copy_to_clipboard(action, None), diff --git a/worf/src/lib/modes/file.rs b/worf/src/lib/modes/file.rs index 2a04066..c4b88d1 100644 --- a/worf/src/lib/modes/file.rs +++ b/worf/src/lib/modes/file.rs @@ -1,16 +1,17 @@ +use regex::Regex; +use std::sync::Mutex; use std::{ fs, os::unix::fs::FileTypeExt, path::{Path, PathBuf}, + sync::{Arc, RwLock}, }; -use regex::Regex; - use crate::{ Error, config::{Config, SortOrder, expand_path}, desktop::spawn_fork, - gui::{self, ItemProvider, MenuItem}, + gui::{self, ExpandMode, ItemProvider, MenuItem, ProviderData}, }; #[derive(Clone)] @@ -98,7 +99,7 @@ impl FileItemProvider { } impl ItemProvider for FileItemProvider { - fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>) { + fn get_elements(&mut self, search: Option<&str>) -> ProviderData { let default_path = if let Some(home) = dirs::home_dir() { home.display().to_string() } else { @@ -117,11 +118,7 @@ impl ItemProvider for FileItemProvider { let mut items: Vec> = Vec::new(); if !path.exists() { - if let Some(last) = &self.last_result { - return (false, last.clone()); - } - - return (true, vec![]); + return ProviderData { items: None }; } if path.is_dir() { @@ -183,11 +180,15 @@ impl ItemProvider for FileItemProvider { gui::apply_sort(&mut items, &self.sort_order); self.last_result = Some(items.clone()); - (true, items) + ProviderData { items: Some(items) } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, self.last_result.clone()) + fn get_sub_elements(&mut self, item: &MenuItem) -> ProviderData { + if self.last_result.as_ref().is_some_and(|lr| lr.len() == 1) { + ProviderData { items: None } + } else { + self.get_elements(Some(&item.label)) + } } } @@ -199,15 +200,19 @@ impl ItemProvider for FileItemProvider { /// /// # Panics /// In case an internal regex does not parse anymore, this should never happen -pub fn show(config: &Config) -> Result<(), Error> { - let provider = FileItemProvider::new(0, config.sort_order()); +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(FileItemProvider::new( + 0, + config.read().unwrap().sort_order(), + ))); // todo ues a arc instead of cloning the config let selection_result = gui::show( config.clone(), provider, - false, + None, Some(vec![Regex::new("^\\$\\w+").unwrap()]), + ExpandMode::Verbatim, None, )?; if let Some(action) = selection_result.menu.action { diff --git a/worf/src/lib/modes/math.rs b/worf/src/lib/modes/math.rs index 92931c9..36aae2c 100644 --- a/worf/src/lib/modes/math.rs +++ b/worf/src/lib/modes/math.rs @@ -1,9 +1,16 @@ +use std::{ + collections::VecDeque, + sync::{Arc, Mutex, RwLock}, +}; + use regex::Regex; -use std::collections::VecDeque; use crate::{ config::Config, - gui::{self, ItemProvider, MenuItem}, + gui::{ + self, ArcFactory, ArcProvider, DefaultItemFactory, ExpandMode, ItemProvider, MenuItem, + ProviderData, + }, }; #[derive(Clone)] @@ -26,7 +33,7 @@ impl MathProvider { impl ItemProvider for MathProvider { #[allow(clippy::cast_possible_truncation)] - fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>) { + fn get_elements(&mut self, search: Option<&str>) -> ProviderData { if let Some(search_text) = search { let result = calc(search_text); @@ -41,14 +48,16 @@ impl ItemProvider for MathProvider { ); let mut result = vec![item]; result.append(&mut self.elements.clone()); - (true, result) + ProviderData { + items: Some(result), + } } else { - (false, self.elements.clone()) + ProviderData { items: None } } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -239,12 +248,21 @@ fn calc(input: &str) -> String { } /// Shows the math mode -pub fn show(config: &Config) { - let mut calc: Vec> = vec![]; +pub fn show(config: Arc>) { + let mut calc: Vec> = vec![]; + let provider = Arc::new(Mutex::new(MathProvider::new(()))); + let factory: ArcFactory<()> = Arc::new(Mutex::new(DefaultItemFactory::new())); + let arc_provider = Arc::clone(&provider) as ArcProvider<()>; loop { - let mut provider = MathProvider::new(String::new()); - provider.add_elements(&mut calc.clone()); - let selection_result = gui::show(config.clone(), provider, true, None, None); + provider.lock().unwrap().add_elements(&mut calc.clone()); + let selection_result = gui::show( + config.clone(), + Arc::clone(&arc_provider), + Some(Arc::clone(&factory)), + None, + ExpandMode::Verbatim, + None, + ); if let Ok(mi) = selection_result { calc.push(mi.menu); } else { diff --git a/worf/src/lib/modes/run.rs b/worf/src/lib/modes/run.rs index 4bfdfda..7a19b84 100644 --- a/worf/src/lib/modes/run.rs +++ b/worf/src/lib/modes/run.rs @@ -4,32 +4,42 @@ use std::{ ffi::CString, fs, path::PathBuf, + sync::{Arc, Mutex, RwLock}, }; +use crate::gui::ArcProvider; use crate::{ Error, config::{Config, SortOrder}, desktop::{is_executable, save_cache_file}, - gui::{self, ItemProvider, MenuItem}, + gui::{self, ExpandMode, ItemProvider, MenuItem, ProviderData}, modes::load_cache, }; -impl ItemProvider for RunProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { +impl ItemProvider<()> for RunProvider { + fn get_elements(&mut self, query: Option<&str>) -> ProviderData<()> { if self.items.is_none() { self.items = Some(self.load().clone()); } - (false, self.items.clone().unwrap()) + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: self.items.clone(), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem<()>) -> ProviderData<()> { + ProviderData { + items: self.items.clone(), + } } } #[derive(Clone)] struct RunProvider { - items: Option>>, + items: Option>>, cache_path: PathBuf, cache: HashMap, sort_order: SortOrder, @@ -48,7 +58,7 @@ impl RunProvider { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_precision_loss)] - fn load(&self) -> Vec> { + fn load(&self) -> Vec> { let path_var = env::var("PATH").unwrap_or_default(); let paths = env::split_paths(&path_var); @@ -82,7 +92,7 @@ impl RunProvider { .collect(); let mut seen_actions = HashSet::new(); - let mut entries: Vec> = entries + let mut entries: Vec> = entries .into_iter() .filter(|entry| { entry @@ -124,13 +134,23 @@ fn update_run_cache_and_run( /// # Errors /// /// Will return `Err` if it was not able to spawn the process -pub fn show(config: &Config) -> Result<(), Error> { - let provider = RunProvider::new(config)?; - let cache_path = provider.cache_path.clone(); - let mut cache = provider.cache.clone(); - let selection_result = gui::show(config.clone(), provider, false, None, None); +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(RunProvider::new(&config.read().unwrap())?)); + let arc_provider = Arc::clone(&provider) as ArcProvider<()>; + + let selection_result = gui::show( + config, + arc_provider, + None, + None, + ExpandMode::Verbatim, + None, + ); match selection_result { - Ok(s) => update_run_cache_and_run(&cache_path, &mut cache, s.menu)?, + Ok(s) => { + let prov = provider.lock().unwrap(); + update_run_cache_and_run(&prov.cache_path, &mut prov.cache.clone(), s.menu)?; + } Err(_) => { log::error!("No item selected"); } diff --git a/worf/src/lib/modes/search.rs b/worf/src/lib/modes/search.rs index ba68f1b..35e755c 100644 --- a/worf/src/lib/modes/search.rs +++ b/worf/src/lib/modes/search.rs @@ -1,10 +1,11 @@ +use std::sync::{Arc, Mutex, RwLock}; use urlencoding::encode; -use crate::desktop::spawn_fork; use crate::{ Error, config::Config, - gui::{self, ItemProvider, MenuItem}, + desktop::spawn_fork, + gui::{self, ArcFactory, DefaultItemFactory, ExpandMode, ItemProvider, MenuItem, ProviderData}, }; #[derive(Clone)] @@ -23,7 +24,7 @@ impl SearchProvider { } impl ItemProvider for SearchProvider { - fn get_elements(&mut self, query: Option<&str>) -> (bool, Vec>) { + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { if let Some(query) = query { let url = format!("{}{}", self.search_query, encode(query)); let run_search = MenuItem::new( @@ -35,14 +36,17 @@ impl ItemProvider for SearchProvider { 0.0, Some(self.data.clone()), ); - (true, vec![run_search]) + + ProviderData { + items: Some(vec![run_search]), + } } else { - (false, vec![]) + ProviderData { items: None } } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -50,9 +54,20 @@ impl ItemProvider for SearchProvider { /// # Errors /// /// Forwards errors from the gui. See `gui::show` for details. -pub fn show(config: &Config) -> Result<(), Error> { - let provider = SearchProvider::new(String::new(), config.search_query()); - let selection_result = gui::show(config.clone(), provider, true, None, None)?; +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(SearchProvider::new( + (), + config.read().unwrap().search_query(), + ))); + let factory: ArcFactory<()> = Arc::new(Mutex::new(DefaultItemFactory::new())); + let selection_result = gui::show( + config.clone(), + provider, + Some(factory), + None, + ExpandMode::Verbatim, + None, + )?; match selection_result.menu.action { None => Err(Error::MissingAction), Some(action) => spawn_fork(&action, None), diff --git a/worf/src/lib/modes/ssh.rs b/worf/src/lib/modes/ssh.rs index 3e82557..0b54d78 100644 --- a/worf/src/lib/modes/ssh.rs +++ b/worf/src/lib/modes/ssh.rs @@ -1,7 +1,8 @@ -use std::fs; - use regex::Regex; +use std::fs; +use std::sync::{Arc, Mutex, RwLock}; +use crate::gui::{ExpandMode, ProviderData}; use crate::{ Error, config::{Config, SortOrder}, @@ -11,7 +12,7 @@ use crate::{ #[derive(Clone)] pub(crate) struct SshProvider { - elements: Vec>, + items: Vec>, } impl SshProvider { @@ -46,17 +47,23 @@ impl SshProvider { .collect(); gui::apply_sort(&mut items, order); - Self { elements: items } + Self { items } } } impl ItemProvider for SshProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { - (false, self.elements.clone()) + fn get_elements(&mut self, query: Option<&str>) -> ProviderData { + if query.is_some() { + ProviderData { items: None } + } else { + ProviderData { + items: Some(self.items.clone()), + } + } } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { - (false, None) + fn get_sub_elements(&mut self, _: &MenuItem) -> ProviderData { + ProviderData { items: None } } } @@ -87,11 +94,21 @@ pub(crate) fn launch(menu_item: &MenuItem, config: &Config) -> Resu /// Will return `Err` /// * if it was not able to spawn the process /// * if it didn't find a terminal -pub fn show(config: &Config) -> Result<(), Error> { - let provider = SshProvider::new(0, &config.sort_order()); - let selection_result = gui::show(config.clone(), provider, true, None, None); +pub fn show(config: Arc>) -> Result<(), Error> { + let provider = Arc::new(Mutex::new(SshProvider::new( + 0, + &config.read().unwrap().sort_order(), + ))); + let selection_result = gui::show( + Arc::clone(&config), + provider, + None, + None, + ExpandMode::Verbatim, + None, + ); if let Ok(mi) = selection_result { - launch(&mi.menu, config)?; + launch(&mi.menu, &config.read().unwrap())?; } else { log::error!("No item selected"); } diff --git a/worf/src/main.rs b/worf/src/main.rs index c38f2e5..c1cf239 100644 --- a/worf/src/main.rs +++ b/worf/src/main.rs @@ -1,5 +1,5 @@ use std::env; - +use std::sync::{Arc, RwLock}; use worf::{Error, config, config::Mode, desktop::fork_if_configured, modes}; fn main() { @@ -25,21 +25,22 @@ fn main() { } fork_if_configured(&config); // may exit the program - + if let Some(show) = &config.show() { + let config = Arc::new(RwLock::new(config)); let result = match show { - Mode::Run => modes::run::show(&config), - Mode::Drun => modes::drun::show(&config), - Mode::Dmenu => modes::dmenu::show(&config), - Mode::File => modes::file::show(&config), + Mode::Run => modes::run::show(config), + Mode::Drun => modes::drun::show(config), + Mode::Dmenu => modes::dmenu::show(config), + Mode::File => modes::file::show(config), Mode::Math => { - modes::math::show(&config); + modes::math::show(config); Ok(()) } - Mode::Ssh => modes::ssh::show(&config), - Mode::Emoji => modes::emoji::show(&config), + Mode::Ssh => modes::ssh::show(config), + Mode::Emoji => modes::emoji::show(config), Mode::Auto => modes::auto::show(&config), - Mode::WebSearch => modes::search::show(&config), + Mode::WebSearch => modes::search::show(config), }; if let Err(err) = result {