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
This commit is contained in:
Alexander Mohr 2025-06-22 22:10:14 +02:00
parent d6e3d91c5c
commit 6ce81a56d5
17 changed files with 700 additions and 444 deletions

153
Cargo.lock generated
View file

@ -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",

View file

@ -3,6 +3,7 @@ members = [
"worf",
"examples/worf-warden",
"examples/worf-hyprswitch",
"examples/worf-hyprspace",
]
resolver = "3"

View file

@ -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"

View file

@ -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<Window> for WindowProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<Window>>) {
(false, self.windows.clone())
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<Window> {
if query.is_some() {
ProviderData { items: None }
} else {
ProviderData {
items: Some(self.windows.clone()),
}
}
}
fn get_sub_elements(&mut self, _: &MenuItem<Window>) -> (bool, Option<Vec<MenuItem<Window>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<Window>) -> ProviderData<Window> {
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
}

View file

@ -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<MenuItemMetaData> for PasswordProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<MenuItemMetaData>>) {
(false, self.items.clone())
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<MenuItemMetaData> {
if query.is_some() {
ProviderData { items: None }
} else {
ProviderData {
items: Some(self.items.clone()),
}
}
}
fn get_sub_elements(
&mut self,
_: &MenuItem<MenuItemMetaData>,
) -> (bool, Option<Vec<MenuItem<MenuItemMetaData>>>) {
(false, None)
) -> ProviderData<MenuItemMetaData> {
ProviderData { items: None }
}
}
@ -265,12 +271,13 @@ fn key_lock() -> KeyBinding {
}
}
fn show(config: Config, provider: PasswordProvider) -> Result<(), String> {
fn show(config: Arc<RwLock<Config>>, provider: Arc<Mutex<PasswordProvider>>) -> 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)
}

View file

@ -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<bool>,
/// Allow submitting selected entry with expand key if there is only 1 item left.
#[clap(long = "submit-with-expand")]
submit_with_expand: Option<bool>,
}
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()

View file

@ -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<T> = Arc<RwLock<HashMap<FlowBoxChild, MenuItem<T>>>>;
type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T> + Send>>;
pub type ArcMenuMap<T> = Arc<RwLock<HashMap<FlowBoxChild, MenuItem<T>>>>;
pub type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T> + Send>>;
pub type ArcFactory<T> = Arc<Mutex<dyn ItemFactory<T> + Send>>;
pub struct Selection<T: Clone + Send> {
pub menu: MenuItem<T>,
@ -48,9 +50,42 @@ pub struct Selection<T: Clone + Send> {
}
type SelectionSender<T> = Sender<Result<Selection<T>, Error>>;
pub struct ProviderData<T: Clone> {
pub items: Option<Vec<MenuItem<T>>>,
}
pub trait ItemProvider<T: Clone> {
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>);
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>);
fn get_elements(&mut self, search: Option<&str>) -> ProviderData<T>;
/// 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<T>) -> ProviderData<T>;
}
pub trait ItemFactory<T: Clone> {
fn new_menu_item(&self, label: String) -> Option<MenuItem<T>>;
}
/// Default generic item factory that creates an almost empty menu item
/// Without data, no icon, and sort score of 0.
pub struct DefaultItemFactory<T: Clone> {
_marker: PhantomData<T>,
}
impl<T: Clone> DefaultItemFactory<T> {
pub fn new() -> DefaultItemFactory<T> {
DefaultItemFactory::<T> {
_marker: Default::default(),
}
}
}
impl<T: Clone> ItemFactory<T> for DefaultItemFactory<T> {
fn new_menu_item(&self, label: String) -> Option<MenuItem<T>> {
Some(MenuItem::new(label, None, None, vec![], None, 0.0, None))
}
}
impl From<&Anchor> for Edge {
@ -121,6 +156,10 @@ pub struct MenuItem<T: Clone> {
/// Allows to store arbitrary additional information
pub data: Option<T>,
// /// 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<Modifier> {
let mut modifiers = HashSet::new();
@ -421,6 +466,7 @@ impl<T: Clone> MenuItem<T> {
working_dir: Option<String>,
initial_sort_score: f64,
data: Option<T>,
//allow_submit: bool,
) -> Self {
MenuItem {
label,
@ -430,6 +476,7 @@ impl<T: Clone> MenuItem<T> {
working_dir,
initial_sort_score,
data,
//allow_submit,
search_sort_score: 0.0,
visible: true,
}
@ -444,10 +491,11 @@ impl<T: Clone> AsRef<MenuItem<T>> for MenuItem<T> {
struct MetaData<T: Clone + Send> {
item_provider: ArcProvider<T>,
item_factory: Option<ArcFactory<T>>,
selected_sender: SelectionSender<T>,
config: Rc<Config>,
new_on_empty: bool,
config: Arc<RwLock<Config>>,
search_ignored_words: Option<Vec<Regex>>,
expand_mode: ExpandMode,
}
struct UiElements<T: Clone> {
@ -468,20 +516,20 @@ struct UiElements<T: Clone> {
/// # Errors
///
/// Will return Err when the channel between the UI and this is broken
pub fn show<T, P>(
config: Config,
item_provider: P,
new_on_empty: bool,
pub fn show<T>(
config: Arc<RwLock<Config>>,
item_provider: ArcProvider<T>,
item_factory: Option<ArcFactory<T>>,
search_ignored_words: Option<Vec<Regex>>,
expand_mode: ExpandMode,
custom_keys: Option<CustomKeys>,
) -> Result<Selection<T>, Error>
where
T: Clone + 'static + Send,
P: ItemProvider<T> + '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::<T>(&connect_cfg, &meta, app.clone(), custom_keys.as_ref());
});
let gtk_args: [&str; 0] = [];
@ -524,28 +574,16 @@ where
receiver_result?
}
fn build_ui<T, P>(
config: &Config,
item_provider: P,
sender: Sender<Result<Selection<T>, Error>>,
fn build_ui<T>(
config: &Arc<RwLock<Config>>,
meta: &Rc<MetaData<T>>,
app: Application,
new_on_empty: bool,
search_ignored_words: Option<Vec<Regex>>,
custom_keys: Option<&CustomKeys>,
) where
T: Clone + 'static + Send,
P: ItemProvider<T> + '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<T, P>(
.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<T, P>(
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<T, P>(
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<T, P>(
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<T: Clone + Send + 'static>(
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<T: Clone + 'static + Send>(
meta: &Rc<MetaData<T>>,
mut items: Vec<MenuItem<T>>,
) {
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<T: Clone + 'static + Send>(
// 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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
}
// 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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
} 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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
if custom_key_match {
let search_lock = ui.search_text.lock().unwrap();
if let Err(e) = handle_selected_item(
ui,
Rc::<MetaData<T>>::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<T>(ui: &Rc<UiElements<T>>, meta: &Rc<MetaData<T>>,
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()),
};
set_search_text(ui, meta, &query);
if let Ok(new_pos) = i32::try_from(query.len() + 1) {
ui.search.set_position(new_pos);
}
let query = changed.1;
set_search_text(ui, meta, &query);
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::<MetaData<T>>::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<T>(
ui: &Rc<UiElements<T>>,
meta: Rc<MetaData<T>>,
meta: &Rc<MetaData<T>>,
query: Option<&str>,
item: Option<MenuItem<T>>,
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,
};
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);
Ok(())
} else {
Err("selected item cannot be resolved".to_owned())
return Ok(());
}
}
Err("selected item cannot be resolved".to_owned())
}
fn send_selected_item<T>(
ui: &Rc<UiElements<T>>,
meta: Rc<MetaData<T>>,
meta: &Rc<MetaData<T>>,
custom_key: Option<KeyBinding>,
selected_item: MenuItem<T>,
) 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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
}
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<T: Clone + 'static + Send>(
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<T: Clone + 'static + Send>(
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::<MetaData<T>>::clone(&click_meta),
&click_meta,
None,
Some(element_clone.clone()),
false,
None,
) {
log::error!("{e}");
@ -1703,30 +1748,24 @@ fn lookup_icon(icon_path: Option<&str>, config: &Config) -> Option<Image> {
fn set_menu_visibility_for_search<T: Clone>(
query: &str,
items: &mut HashMap<FlowBoxChild, MenuItem<T>>,
config: &Config,
config: &Arc<RwLock<Config>>,
search_ignored_words: Option<&Vec<Regex>>,
) {
{
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() {
let mut query = if config.read().unwrap().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();
});
}
query = filtered_query(search_ignored_words, &query);
for (fb, menu_item) in items.iter_mut() {
let menu_item_search = format!(
@ -1735,28 +1774,31 @@ fn set_menu_visibility_for_search<T: Clone>(
.action
.as_ref()
.map(|a| {
if config.insensitive() {
if config.read().unwrap().insensitive() {
a.to_lowercase()
} else {
a.clone()
}
})
.unwrap_or_default(),
if config.insensitive() {
if config.read().unwrap().insensitive() {
menu_item.label.to_lowercase()
} else {
menu_item.label.clone()
}
);
let (search_sort_score, visible) = match config.match_method() {
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.fuzzy_min_score() && score > 0.0)
(
score,
score > config.read().unwrap().fuzzy_min_score() && score > 0.0,
)
}
MatchMethod::Contains => {
if menu_item_search.contains(&query) {
@ -1778,7 +1820,17 @@ fn set_menu_visibility_for_search<T: Clone>(
menu_item.visible = visible;
fb.set_visible(menu_item.visible);
}
}
#[must_use]
pub fn filtered_query(search_ignored_words: Option<&Vec<Regex>>, 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<T: Clone>(

View file

@ -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<MenuItem<AutoRunType>>) {
fn default_auto_elements(&mut self) -> ProviderData<AutoRunType> {
// 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<AutoRunType> for AutoItemProvider {
fn get_elements(&mut self, search_opt: Option<&str>) -> (bool, Vec<MenuItem<AutoRunType>>) {
fn get_elements(&mut self, search_opt: Option<&str>) -> ProviderData<AutoRunType> {
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<AutoRunType> 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)
}
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<AutoRunType>,
) -> (bool, Option<Vec<MenuItem<AutoRunType>>>) {
let (changed, items) = self.get_elements(Some(item.label.as_ref()));
(changed, Some(items))
fn get_sub_elements(&mut self, item: &MenuItem<AutoRunType>) -> ProviderData<AutoRunType> {
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<AutoRunType> 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<RwLock<Config>>) -> Result<(), Error> {
let provider = Arc::new(Mutex::new(AutoItemProvider::new(&config.read().unwrap())));
let arc_provider = Arc::clone(&provider) as ArcProvider<AutoRunType>;
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");

View file

@ -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<String> for DMenuProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) {
(false, self.items.clone())
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<String> {
if query.is_some() {
ProviderData { items: None }
} else {
ProviderData {
items: Some(self.items.clone()),
}
}
}
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> ProviderData<String> {
ProviderData { items: None }
}
}
@ -43,10 +52,19 @@ impl ItemProvider<String> 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<RwLock<Config>>) -> 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);

View file

@ -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<T: Clone> {
}
impl<T: Clone + Send + Sync> ItemProvider<T> for DRunProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<T> {
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<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> ProviderData<T> {
ProviderData { items: None }
}
}
@ -211,15 +219,22 @@ pub(crate) fn update_drun_cache_and_run<T: Clone>(
/// # 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<RwLock<Config>>) -> 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");
}

View file

@ -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<String> for EmojiProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) {
(false, self.elements.clone())
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<String> {
if query.is_some() {
ProviderData { items: None }
} else {
ProviderData {
items: Some(self.elements.clone()),
}
}
}
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> ProviderData<String> {
ProviderData { items: None }
}
}
@ -54,9 +62,22 @@ impl ItemProvider<String> 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<RwLock<Config>>) -> 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),

View file

@ -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<T: Clone> FileItemProvider<T> {
}
impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
fn get_elements(&mut self, search: Option<&str>) -> ProviderData<T> {
let default_path = if let Some(home) = dirs::home_dir() {
home.display().to_string()
} else {
@ -117,11 +118,7 @@ impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
let mut items: Vec<MenuItem<T>> = 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<T: Clone> ItemProvider<T> for FileItemProvider<T> {
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<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, self.last_result.clone())
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> ProviderData<T> {
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<T: Clone> ItemProvider<T> for FileItemProvider<T> {
///
/// # 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<RwLock<Config>>) -> 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 {

View file

@ -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<T: Clone> MathProvider<T> {
impl<T: Clone> ItemProvider<T> for MathProvider<T> {
#[allow(clippy::cast_possible_truncation)]
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
fn get_elements(&mut self, search: Option<&str>) -> ProviderData<T> {
if let Some(search_text) = search {
let result = calc(search_text);
@ -41,14 +48,16 @@ impl<T: Clone> ItemProvider<T> for MathProvider<T> {
);
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<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> ProviderData<T> {
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<MenuItem<String>> = vec![];
pub fn show(config: Arc<RwLock<Config>>) {
let mut calc: Vec<MenuItem<()>> = 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 {

View file

@ -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<i32> for RunProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<i32>>) {
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<i32>) -> (bool, Option<Vec<MenuItem<i32>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<()>) -> ProviderData<()> {
ProviderData {
items: self.items.clone(),
}
}
}
#[derive(Clone)]
struct RunProvider {
items: Option<Vec<MenuItem<i32>>>,
items: Option<Vec<MenuItem<()>>>,
cache_path: PathBuf,
cache: HashMap<String, i64>,
sort_order: SortOrder,
@ -48,7 +58,7 @@ impl RunProvider {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
fn load(&self) -> Vec<MenuItem<i32>> {
fn load(&self) -> Vec<MenuItem<()>> {
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<MenuItem<i32>> = entries
let mut entries: Vec<MenuItem<()>> = entries
.into_iter()
.filter(|entry| {
entry
@ -124,13 +134,23 @@ fn update_run_cache_and_run<T: Clone>(
/// # 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<RwLock<Config>>) -> 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");
}

View file

@ -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<T: Clone> SearchProvider<T> {
}
impl<T: Clone> ItemProvider<T> for SearchProvider<T> {
fn get_elements(&mut self, query: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<T> {
if let Some(query) = query {
let url = format!("{}{}", self.search_query, encode(query));
let run_search = MenuItem::new(
@ -35,14 +36,17 @@ impl<T: Clone> ItemProvider<T> for SearchProvider<T> {
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<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> ProviderData<T> {
ProviderData { items: None }
}
}
@ -50,9 +54,20 @@ impl<T: Clone> ItemProvider<T> for SearchProvider<T> {
/// # 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<RwLock<Config>>) -> 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),

View file

@ -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<T: Clone> {
elements: Vec<MenuItem<T>>,
items: Vec<MenuItem<T>>,
}
impl<T: Clone> SshProvider<T> {
@ -46,17 +47,23 @@ impl<T: Clone> SshProvider<T> {
.collect();
gui::apply_sort(&mut items, order);
Self { elements: items }
Self { items }
}
}
impl<T: Clone> ItemProvider<T> for SshProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
(false, self.elements.clone())
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<T> {
if query.is_some() {
ProviderData { items: None }
} else {
ProviderData {
items: Some(self.items.clone()),
}
}
}
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> (bool, Option<Vec<MenuItem<T>>>) {
(false, None)
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> ProviderData<T> {
ProviderData { items: None }
}
}
@ -87,11 +94,21 @@ pub(crate) fn launch<T: Clone>(menu_item: &MenuItem<T>, 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<RwLock<Config>>) -> 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");
}

View file

@ -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() {
@ -27,19 +27,20 @@ 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 {