From d3f17ade6570f34a4915c6af03ce2aa4714204ed Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 8 Apr 2025 23:35:57 +0200 Subject: [PATCH 01/10] imrpove drun --- Cargo.lock | 195 ++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + src/args.rs | 2 +- src/desktop/mod.rs | 52 +++++++----- src/gui.rs | 4 +- src/main.rs | 151 +++++++++++++++++++++++++++++------ 6 files changed, 335 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cce3da8..1967632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -138,6 +149,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.0" @@ -176,7 +193,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cairo-sys-rs", "glib", "libc", @@ -199,7 +216,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags", + "bitflags 2.9.0", "log", "polling", "rustix", @@ -213,7 +230,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10929724661d1c43856fd87c7a127ae944ec55579134fb485e4136fb6a46fdcb" dependencies = [ - "bitflags", + "bitflags 2.9.0", "polling", "rustix", "slab", @@ -257,6 +274,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", + "yaml-rust", +] + [[package]] name = "clap" version = "4.5.35" @@ -275,8 +308,8 @@ checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", - "clap_lex", - "strsim", + "clap_lex 0.7.4", + "strsim 0.11.1", ] [[package]] @@ -291,6 +324,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.4" @@ -462,6 +504,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "freedesktop-file-parser" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6059d3997cc694ec3e9a378db855866233ef7edfeafd85afcb2239fd130e6e6b" +dependencies = [ + "thiserror 2.0.12", + "xdgkit", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -655,7 +707,7 @@ version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707b819af8059ee5395a2de9f2317d87a53dbad8846a2f089f0bb44703f37686" dependencies = [ - "bitflags", + "bitflags 2.9.0", "futures-channel", "futures-core", "futures-executor", @@ -785,7 +837,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aec4fd3226bb6aa8dda5370142e14a4d15f00bba99bfb355b6ef7bb49d100758" dependencies = [ - "bitflags", + "bitflags 2.9.0", "gdk4", "glib", "glib-sys", @@ -838,6 +890,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.2" @@ -850,6 +908,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.4.0" @@ -898,6 +965,16 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -905,7 +982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -965,6 +1042,12 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1076,7 +1159,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -1094,6 +1177,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "pango" version = "0.20.9" @@ -1192,7 +1281,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", @@ -1256,6 +1345,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0452695941410a58c8ce4391707ba9bad26a247173bd9886a05a5e8a8babec75" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.37.4" @@ -1339,7 +1438,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -1443,7 +1542,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags", + "bitflags 2.9.0", "bytemuck", "calloop 0.13.0", "calloop-wayland-source", @@ -1475,6 +1574,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -1535,6 +1640,21 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + [[package]] name = "thiserror" version = "1.0.69" @@ -1575,6 +1695,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "tini" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004df4c5f0805eb5f55883204a514cfa43a6d924741be29e871753a53d5565a" + [[package]] name = "tokio" version = "1.44.2" @@ -1629,7 +1755,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -1708,7 +1834,7 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ - "bitflags", + "bitflags 2.9.0", "rustix", "wayland-backend", "wayland-scanner", @@ -1720,7 +1846,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cursor-icon", "wayland-backend", ] @@ -1742,7 +1868,7 @@ version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ - "bitflags", + "bitflags 2.9.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1754,7 +1880,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ - "bitflags", + "bitflags 2.9.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1768,7 +1894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.4", "quote", ] @@ -1797,6 +1923,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1953,9 +2088,10 @@ version = "0.1.0" dependencies = [ "anyhow", "calloop 0.14.2", - "clap", + "clap 4.5.35", "crossbeam", "env_logger", + "freedesktop-file-parser", "gdk4", "gtk4", "gtk4-layer-shell", @@ -1982,6 +2118,18 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +[[package]] +name = "xdgkit" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeac9c0125f3c131c6a2898d2a9f25c11b7954c3ff644a018cb9e06fa92919b" +dependencies = [ + "clap 3.2.25", + "quick-xml 0.21.0", + "serde", + "tini", +] + [[package]] name = "xkbcommon" version = "0.7.0" @@ -2008,6 +2156,15 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 9f84b39..eec2443 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,4 @@ smithay-client-toolkit = { version = "0.19.2", features = ["calloop"]} calloop = "0.14.2" crossbeam = "0.8.4" libc = "0.2.171" +freedesktop-file-parser = "0.1.0" diff --git a/src/args.rs b/src/args.rs index 81391ae..1c19175 100644 --- a/src/args.rs +++ b/src/args.rs @@ -37,7 +37,7 @@ impl FromStr for Mode { } #[derive(Parser, Debug, Deserialize, Serialize)] -#[clap(about = "Ravi is a wofi clone written in rust, it aims to be a drop in replacement")] +#[clap(about = "Worf is a wofi clone written in rust, it aims to be a drop in replacement")] pub struct Args { /// Forks the menu so you can close the terminal #[clap(short = 'f', long = "fork")] diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index c3120c2..eb539c0 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -1,8 +1,9 @@ +use freedesktop_file_parser::DesktopFile; use gtk4::prelude::*; use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use home::home_dir; use ini::configparser::ini::Ini; -use log::{info, warn}; +use log::{debug, info, warn}; use regex::Regex; use std::collections::HashMap; use std::path::Path; @@ -46,23 +47,25 @@ pub fn default_icon() -> String { } fn fetch_icon_from_desktop_file(icon_name: &str) -> Option { - find_desktop_files().into_iter().find_map(|desktop_file| { - desktop_file - .get("Desktop Entry") - .filter(|desktop_entry| { - desktop_entry - .get("Exec") - .and_then(|opt| opt.as_ref()) - .is_some_and(|exec| exec.to_lowercase().contains(icon_name)) - }) - .map(|desktop_entry| { - desktop_entry - .get("Icon") - .and_then(|opt| opt.as_ref()) - .map(ToOwned::to_owned) - .unwrap_or_default() - }) - }) + // find_desktop_files().into_iter().find_map(|desktop_file| { + // desktop_file + // .get("Desktop Entry") + // .filter(|desktop_entry| { + // desktop_entry + // .get("Exec") + // .and_then(|opt| opt.as_ref()) + // .is_some_and(|exec| exec.to_lowercase().contains(icon_name)) + // }) + // .map(|desktop_entry| { + // desktop_entry + // .get("Icon") + // .and_then(|opt| opt.as_ref()) + // .map(ToOwned::to_owned) + // .unwrap_or_default() + // }) + // }) + //todo + None } fn fetch_icon_from_theme(icon_name: &str) -> Option { @@ -135,8 +138,11 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option Vec>>> { - let mut paths = vec![PathBuf::from("/usr/share/applications")]; +pub(crate) fn find_desktop_files() -> Vec { + let mut paths = vec![ + PathBuf::from("/usr/share/applications"), + PathBuf::from("/usr/local/share/applications"), + ]; if let Some(home) = home_dir() { paths.push(home.join(".local/share/applications")); @@ -150,8 +156,10 @@ pub(crate) fn find_desktop_files() -> Vec) -> anyhow::Result<(i32) ); // No need for application_id unless you want portal support - let app = Application::builder().application_id("ravi").build(); + let app = Application::builder().application_id("worf").build(); let (sender, receiver) = channel::bounded(1); app.connect_activate(move |app| { @@ -185,7 +185,7 @@ fn setup_key_event_handler( let key_controller = EventControllerKey::new(); key_controller.connect_key_pressed(move |_, key_value, _, _| { match key_value { - Key::Escape => exit(1), + Key::Escape => exit(1), // todo better way to do this? Key::Return => { for s in &inner_box.selected_children() { // let element : &Option<&EntryElement> = &elements.get(s.index() as usize); diff --git a/src/main.rs b/src/main.rs index 3d71c81..4df93ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,14 +12,16 @@ use gtk4::prelude::{ FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt, }; use gtk4_layer_shell::LayerShell; +use log::{debug, warn}; use merge::Merge; +use std::collections::HashMap; use std::ops::Deref; use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::Arc; use std::thread::sleep; -use std::{fs, time}; +use std::{env, fs, time}; mod args; mod config; @@ -80,43 +82,139 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn drun(mut config: Config) -> anyhow::Result<()> { - let mut entries: Vec = Vec::new(); - for file in &find_desktop_files() { - if let Some(desktop_entry) = file.get("desktop entry") { - let icon = desktop_entry - .get("icon") - .and_then(|x| x.as_ref().map(|x| x.to_owned())); - let Some(exec) = desktop_entry.get("exec").and_then(|x| x.as_ref().cloned()) else { - continue; - }; +fn get_locale_variants() -> Vec { + let locale = env::var("LC_ALL") + .or_else(|_| env::var("LC_MESSAGES")) + .or_else(|_| env::var("LANG")) + .unwrap_or_else(|_| "c".to_string()); - if let Some((cmd, _)) = exec.split_once(' ') { - if !PathBuf::from(cmd).exists() { - continue; - } + let lang = locale.split('.').next().unwrap_or(&locale).to_lowercase(); + let mut variants = vec![]; + + if let Some((lang_part, region)) = lang.split_once('_') { + variants.push(format!("{}_{region}", lang_part)); // en_us + variants.push(lang_part.to_string()); // en + } else { + variants.push(lang.clone()); // e.g. "fr" + } + + variants +} + +fn extract_desktop_fields( + category: &str, + //keys: Vec, + desktop_map: &HashMap>>, +) -> HashMap { + let mut result: HashMap = HashMap::new(); + let category_map = desktop_map.get(category); + if category_map.is_none() { + debug!("No desktop map for category {category}, map data: {desktop_map:?}"); + return result; + } + + let keys_needed = ["name", "exec", "icon"]; + let locale_variants = get_locale_variants(); + + for (map_key, map_value) in category_map.unwrap() { + for key in keys_needed { + if result.contains_key(key) || map_value.is_none() { + continue; } - let name = desktop_entry - .get("name") - .and_then(|x| x.as_ref().map(|x| x.to_owned())); - if let Some(name) = name { - entries.push({ - EntryElement { - label: name, - icon_path: icon, - action: Some(exec), - sub_elements: None, - } + let (k, v) = locale_variants + .iter() + .find(|locale| { + let localized_key = format!("{}[{}]", key, locale); + key == localized_key }) + .map(|_| (Some(key), map_value)) + .unwrap_or_else(|| { + if key == map_key { + (Some(key), map_value) + } else { + (None, &None) + } + }); + if let Some(k) = k { + if let Some(v) = v { + result.insert(k.to_owned(), v.clone()); + } } } + + if result.len() == keys_needed.len() { + break; + } + } + + result +} +fn drun(mut config: Config) -> anyhow::Result<()> { + let mut entries: Vec = Vec::new(); + for file in &find_desktop_files() { + let n = get_locale_variants() + .iter() + .filter_map(|local| file.entry.name.variants.get(local)) + .next() + .map(|name| name.deref().clone()) + .or_else(|| Some(&file.entry.name.default)); + + debug!("{n:?}") + + // let desktop = Some("desktop entry"); + // let locale = + // env::var("LC_ALL") + // .or_else(|_| env::var("LC_MESSAGES")) + // .or_else(|_| env::var("LANG")) + // .unwrap_or_else(|_| "en_US.UTF-8".to_string()).split_once(".").map(|(k,_)| k.to_owned().to_lowercase()); + // + // + // + // + // if let Some(desktop_entry) = file.get("desktop entry") { + // let icon = desktop_entry + // .get("icon") + // .and_then(|x| x.as_ref().map(|x| x.to_owned())); + // + // + // let Some(exec) = desktop_entry.get("exec") + // + // + // + // .and_then(|x| x.as_ref()) else { + // warn!("Skipping desktop file {file:#?}"); + // continue; + // }; + // + // if let Some((cmd, _)) = exec.split_once(' ') { + // if !PathBuf::from(cmd).exists() { + // continue; + // } + // } + // + // let name = desktop_entry + // .get("name") + // .and_then(|x| x.as_ref().map(|x| x.to_owned())); + // + // if let Some(name) = name { + // entries.push({ + // EntryElement { + // label: name, + // icon_path: icon, + // action: Some(exec.clone()), + // sub_elements: None, + // } + // }) + // } + // } } entries.sort_by(|l, r| l.label.cmp(&r.label)); if config.prompt.is_none() { config.prompt = Some("drun".to_owned()); } + // todo ues a arc instead of cloning the config let selected_index = gui::show(config.clone(), entries.clone())?; entries.get(selected_index as usize).map(|e| { @@ -130,6 +228,7 @@ fn drun(mut config: Config) -> anyhow::Result<()> { fn spawn_fork(cmd: &str) { // todo fork this for real + // todo probably remove arguments? // Unix-like systems (Linux, macOS) let _ = Command::new(cmd) .stdin(Stdio::null()) // Disconnect stdin From e17f74b0963b1f4906f83fa5163b72e67b9c611c Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Wed, 9 Apr 2025 22:32:06 +0200 Subject: [PATCH 02/10] support sub elements, improve desktop file parsing --- src/desktop/mod.rs | 72 +++++++++++++++++++++++++- src/gui.rs | 15 +++--- src/main.rs | 125 ++++++++++++++++++--------------------------- 3 files changed, 130 insertions(+), 82 deletions(-) diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index eb539c0..b712799 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -8,7 +8,7 @@ use regex::Regex; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -use std::{fs, string}; +use std::{env, fs, string}; pub struct IconResolver { cache: HashMap, @@ -165,3 +165,73 @@ pub(crate) fn find_desktop_files() -> Vec { .collect(); p } + + +pub fn get_locale_variants() -> Vec { + let locale = env::var("LC_ALL") + .or_else(|_| env::var("LC_MESSAGES")) + .or_else(|_| env::var("LANG")) + .unwrap_or_else(|_| "c".to_string()); + + let lang = locale.split('.').next().unwrap_or(&locale).to_lowercase(); + let mut variants = vec![]; + + if let Some((lang_part, region)) = lang.split_once('_') { + variants.push(format!("{}_{region}", lang_part)); // en_us + variants.push(lang_part.to_string()); // en + } else { + variants.push(lang.clone()); // e.g. "fr" + } + + variants +} + +pub fn extract_desktop_fields( + category: &str, + //keys: Vec, + desktop_map: &HashMap>>, +) -> HashMap { + let mut result: HashMap = HashMap::new(); + let category_map = desktop_map.get(category); + if category_map.is_none() { + debug!("No desktop map for category {category}, map data: {desktop_map:?}"); + return result; + } + + let keys_needed = ["name", "exec", "icon"]; + let locale_variants = get_locale_variants(); + + for (map_key, map_value) in category_map.unwrap() { + for key in keys_needed { + if result.contains_key(key) || map_value.is_none() { + continue; + } + + let (k, v) = locale_variants + .iter() + .find(|locale| { + let localized_key = format!("{}[{}]", key, locale); + key == localized_key + }) + .map(|_| (Some(key), map_value)) + .unwrap_or_else(|| { + if key == map_key { + (Some(key), map_value) + } else { + (None, &None) + } + }); + if let Some(k) = k { + if let Some(v) = v { + result.insert(k.to_owned(), v.clone()); + } + } + } + + if result.len() == keys_needed.len() { + break; + } + } + + result +} diff --git a/src/gui.rs b/src/gui.rs index 4243a39..1266a94 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -20,14 +20,14 @@ use log::{debug, error, info}; use std::process::exit; #[derive(Clone)] -pub struct EntryElement { +pub struct MenuItem { pub label: String, // todo support empty label? pub icon_path: Option, pub action: Option, - pub sub_elements: Option>, + pub sub_elements: Vec, } -pub fn show(config: Config, elements: Vec) -> anyhow::Result<(i32)> { +pub fn show(config: Config, elements: Vec) -> anyhow::Result<(i32)> { // Load CSS let provider = CssProvider::new(); let css_file_path = File::for_path("/home/me/.config/wofi/style.css"); @@ -118,7 +118,7 @@ pub fn show(config: Config, elements: Vec) -> anyhow::Result<(i32) inner_box.set_activate_on_single_click(true); for entry in &elements { - add_entry_element(&inner_box, &entry); + add_menu_item(&inner_box, &entry); } // Set focus after everything is realized @@ -216,8 +216,8 @@ fn setup_key_event_handler( window.add_controller(key_controller); } -fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) { - let parent: Widget = if entry_element.sub_elements.is_some() { +fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) { + let parent: Widget = if !entry_element.sub_elements.is_empty() { let expander = Expander::new(None); // Inline label as expander label @@ -228,11 +228,12 @@ fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) { // todo subelements do not fill full space yet. // todo multi nesting is not supported yet. - for x in entry_element.sub_elements.iter().flatten() { + for x in entry_element.sub_elements.iter(){ let row = ListBoxRow::new(); row.set_widget_name("entry"); let label = Label::new(Some(&x.label)); + label.set_halign(Align::Start); row.set_child(Some(&label)); list_box.append(&row); } diff --git a/src/main.rs b/src/main.rs index 4df93ef..6ac9aff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,8 @@ use crate::args::{Args, Mode}; use crate::config::{Config, merge_config_with_args}; -use crate::desktop::find_desktop_files; -use crate::gui::EntryElement; +use crate::desktop::{default_icon, find_desktop_files, get_locale_variants}; +use crate::gui::MenuItem; use clap::Parser; use gdk4::prelude::Cast; use gtk4::prelude::{ @@ -12,7 +12,7 @@ use gtk4::prelude::{ FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt, }; use gtk4_layer_shell::LayerShell; -use log::{debug, warn}; +use log::{debug, info, warn}; use merge::Merge; use std::collections::HashMap; use std::ops::Deref; @@ -22,6 +22,7 @@ use std::process::{Command, Stdio}; use std::sync::Arc; use std::thread::sleep; use std::{env, fs, time}; +use freedesktop_file_parser::{DesktopAction, EntryType}; mod args; mod config; @@ -82,85 +83,61 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn get_locale_variants() -> Vec { - let locale = env::var("LC_ALL") - .or_else(|_| env::var("LC_MESSAGES")) - .or_else(|_| env::var("LANG")) - .unwrap_or_else(|_| "c".to_string()); - - let lang = locale.split('.').next().unwrap_or(&locale).to_lowercase(); - let mut variants = vec![]; - - if let Some((lang_part, region)) = lang.split_once('_') { - variants.push(format!("{}_{region}", lang_part)); // en_us - variants.push(lang_part.to_string()); // en - } else { - variants.push(lang.clone()); // e.g. "fr" - } - - variants +fn lookup_name_with_locale( + locale_variants: &Vec, + variants: &HashMap, + fallback: &str, +) -> Option { + locale_variants + .iter() + .filter_map(|local| variants.get(local)) + .next() + .map(|name| name.to_owned()) + .or_else(|| Some(fallback.to_owned())) } -fn extract_desktop_fields( - category: &str, - //keys: Vec, - desktop_map: &HashMap>>, -) -> HashMap { - let mut result: HashMap = HashMap::new(); - let category_map = desktop_map.get(category); - if category_map.is_none() { - debug!("No desktop map for category {category}, map data: {desktop_map:?}"); - return result; - } - - let keys_needed = ["name", "exec", "icon"]; +fn drun(mut config: Config) -> anyhow::Result<()> { + let mut entries: Vec = Vec::new(); let locale_variants = get_locale_variants(); + let default_icon = default_icon(); - for (map_key, map_value) in category_map.unwrap() { - for key in keys_needed { - if result.contains_key(key) || map_value.is_none() { - continue; - } - - let (k, v) = locale_variants - .iter() - .find(|locale| { - let localized_key = format!("{}[{}]", key, locale); - key == localized_key - }) - .map(|_| (Some(key), map_value)) - .unwrap_or_else(|| { - if key == map_key { - (Some(key), map_value) - } else { - (None, &None) - } - }); - if let Some(k) = k { - if let Some(v) = v { - result.insert(k.to_owned(), v.clone()); - } - } + for file in find_desktop_files().iter().filter(|f| { + f.entry.hidden.map_or(true, |hidden| !hidden) + && f.entry.no_display.map_or(true, |no_display| !no_display) + // todo handle not shown in? + }) { + let name = lookup_name_with_locale( + &locale_variants, + &file.entry.name.variants, + &file.entry.name.default, + ); + if name.is_none() { + debug!("Skipping desktop entry without name {file:?}") } - if result.len() == keys_needed.len() { - break; - } - } + let mut entry = MenuItem { + label: name.unwrap(), + icon_path: None, + action: None, + sub_elements: Vec::default(), + }; - result -} -fn drun(mut config: Config) -> anyhow::Result<()> { - let mut entries: Vec = Vec::new(); - for file in &find_desktop_files() { - let n = get_locale_variants() - .iter() - .filter_map(|local| file.entry.name.variants.get(local)) - .next() - .map(|name| name.deref().clone()) - .or_else(|| Some(&file.entry.name.default)); + file.actions.iter().for_each(|(_, action)| { + let action_name = lookup_name_with_locale( + &locale_variants, + &action.name.variants, + &action.name.default, + ); + let sub_entry = MenuItem { + label: action_name.unwrap().trim().to_owned(), + icon_path: None, + action: None, + sub_elements: Vec::default(), + }; + entry.sub_elements.push(sub_entry); + }); - debug!("{n:?}") + entries.push(entry); // let desktop = Some("desktop entry"); // let locale = From ce61db2bcf9211ff5ae47805c1faa8e52ac5835e Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 12 Apr 2025 00:48:34 +0200 Subject: [PATCH 03/10] show icons --- README.md | 1 + src/gui.rs | 56 ++++++++++++++++++++++++++++++++++++----------------- src/main.rs | 11 +++++++++-- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f928a7a..af23136 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ layerrule = blur, worf ## Breaking changes to Wofi * Error messages differ * Configuration files are not 100% compatible, Worf is using toml files instead, for most part this only means strings have to be quoted +* Themes are not 100% compatible ## Not supported diff --git a/src/gui.rs b/src/gui.rs index 1266a94..b65e12a 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -10,14 +10,13 @@ use gtk4::prelude::{ ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt, }; -use gtk4::{ - Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, Label, ListBox, ListBoxRow, - PolicyType, ScrolledWindow, SearchEntry, Widget, -}; +use gtk4::{Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, Image, Label, ListBox, ListBoxRow, PolicyType, ScrolledWindow, SearchEntry, Widget}; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4_layer_shell::{KeyboardMode, LayerShell}; use log::{debug, error, info}; use std::process::exit; +use hyprland::ctl::output::create; +use hyprland::ctl::plugin::list; #[derive(Clone)] pub struct MenuItem { @@ -219,29 +218,24 @@ fn setup_key_event_handler( fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) { let parent: Widget = if !entry_element.sub_elements.is_empty() { let expander = Expander::new(None); + expander.set_widget_name("expander-box"); + expander.set_halign(Align::Fill); - // Inline label as expander label - let label = Label::new(Some(&entry_element.label)); - expander.set_label_widget(Some(&label)); + let menu_row = create_menu_row(entry_element); + expander.set_label_widget(Some(&menu_row)); let list_box = ListBox::new(); - // todo subelements do not fill full space yet. - // todo multi nesting is not supported yet. - - for x in entry_element.sub_elements.iter(){ - let row = ListBoxRow::new(); - row.set_widget_name("entry"); + list_box.set_widget_name("entry"); - let label = Label::new(Some(&x.label)); - label.set_halign(Align::Start); - row.set_child(Some(&label)); - list_box.append(&row); + // todo multi nesting is not supported yet. + for sub_item in entry_element.sub_elements.iter(){ + list_box.append(&create_menu_row(sub_item)); } expander.set_child(Some(&list_box)); expander.upcast() } else { - Label::new(Some(&entry_element.label)).upcast() + create_menu_row(entry_element).upcast() }; parent.set_halign(Align::Start); @@ -253,6 +247,32 @@ fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) { inner_box.append(&child); } +fn create_menu_row(menu_item: &MenuItem) -> Widget { + let row = ListBoxRow::new(); + row.set_widget_name("entry"); + row.set_hexpand(true); + row.set_halign(Align::Start); + + let row_box = gtk4::Box::new(Orientation::Horizontal, 0); + row.set_child(Some(&row_box)); + + if let Some(image_path) = &menu_item.icon_path { + // todo check config too + let image = Image::from_icon_name(image_path); + image.set_pixel_size(24); + image.set_widget_name("img"); + row_box.append(&image); + } + + let label = Label::new(Some(&menu_item.label)); + + label.set_widget_name("unselected"); + row_box.append(&label); + + + row.upcast() +} + fn percent_or_absolute(value: &String, base_value: i32) -> Option { if value.contains("%") { let value = value.replace("%", ""); diff --git a/src/main.rs b/src/main.rs index 6ac9aff..745fbd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,9 +115,12 @@ fn drun(mut config: Config) -> anyhow::Result<()> { debug!("Skipping desktop entry without name {file:?}") } + let icon = file.entry.icon.as_ref().map(|s| s.content.clone()); + debug!("file, name={name:?}, icon={icon:?}"); + let mut entry = MenuItem { label: name.unwrap(), - icon_path: None, + icon_path: icon.clone(), action: None, sub_elements: Vec::default(), }; @@ -128,9 +131,13 @@ fn drun(mut config: Config) -> anyhow::Result<()> { &action.name.variants, &action.name.default, ); + let action_icon = action.icon.as_ref().map(|s| s.content.clone()).or(icon.as_ref().map(|s| s.clone())); + + debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); + let sub_entry = MenuItem { label: action_name.unwrap().trim().to_owned(), - icon_path: None, + icon_path: action_icon, action: None, sub_elements: Vec::default(), }; From 2d502233eabab7823b047322e926db38b4d0b1db Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sun, 13 Apr 2025 11:38:48 +0200 Subject: [PATCH 04/10] improve theme and add examples, support clicks --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 23 +- src/args.rs | 38 +- src/gui.rs | 290 ------------- src/{ => lib}/config.rs | 150 +++++-- src/{desktop/mod.rs => lib/desktop.rs} | 15 +- src/lib/gui.rs | 544 +++++++++++++++++++++++++ src/lib/mod.rs | 4 + src/lib/system.rs | 31 ++ src/main.rs | 186 +++++---- styles/compact.css | 69 ++++ styles/launcher.css | 69 ++++ 13 files changed, 991 insertions(+), 430 deletions(-) delete mode 100644 src/gui.rs rename src/{ => lib}/config.rs (70%) rename src/{desktop/mod.rs => lib/desktop.rs} (95%) create mode 100644 src/lib/gui.rs create mode 100644 src/lib/mod.rs create mode 100644 src/lib/system.rs create mode 100644 styles/compact.css create mode 100644 styles/launcher.css diff --git a/Cargo.lock b/Cargo.lock index 1967632..8c4684d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2105,6 +2105,7 @@ dependencies = [ "serde", "serde_json", "smithay-client-toolkit", + "strsim 0.11.1", "sysinfo", "thiserror 2.0.12", "toml", diff --git a/Cargo.toml b/Cargo.toml index eec2443..8af58ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ calloop = "0.14.2" crossbeam = "0.8.4" libc = "0.2.171" freedesktop-file-parser = "0.1.0" +strsim = "0.11.1" diff --git a/README.md b/README.md index af23136..fd79220 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # Worf -Worf is a clone of [wofi](https://github.com/SimplyCEO/wofi) written in rust. -Although no code was taken over, the original project is great and to honor their license this tool is licensed under the same GPLV3 terms. - -* Wofis css files are supported -* Wofis command line flags are supported +Worf is yet another dmenu style launcher, heavily inspired by wofi but written in Rust on top of GTK4. +It supports a lot of things the same way wofi does, so migrating to worf is easy, but things I did not +deemed necessary where dropped from worf. See breaking changes section for details. ## Setup @@ -20,10 +18,19 @@ layerrule = blur, worf * Window switcher for hyprland ## Breaking changes to Wofi -* Error messages differ +* Runtime behaviour is not guaranteed to be the same and won't ever be, this includes error messages and themes. +* Themes in general are mostly compatible. Worf is using the same entity ids, + because worf is build on GTK4 instead of GTK3 there will be differences in the look and feel. * Configuration files are not 100% compatible, Worf is using toml files instead, for most part this only means strings have to be quoted -* Themes are not 100% compatible +* Color files are not supported + +## Dropped configuration options +* stylesheet -> use style instead +* color / colors -> GTK4 does not support color files + +## New options +* --fuzzy-length: Defines how long a string must be be ## Not supported -* Wofi has a C-API, that is not and won't be supported. As of now there are no plans to provide a Rust API either. +* Wofi has a C-API, that is not and won't be supported. diff --git a/src/args.rs b/src/args.rs index 1c19175..94002a4 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,4 @@ +use crate::lib::config::{Align, MatchMethod, Orientation}; use clap::Parser; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -51,10 +52,6 @@ pub struct Args { #[clap(short = 's', long = "style")] style: Option, - /// Selects a colors file to use - #[clap(short = 'C', long = "color")] - color: Option, - /// Runs in dmenu mode #[clap(short = 'd', long = "dmenu")] dmenu: bool, @@ -117,7 +114,7 @@ pub struct Args { /// Sets the matching method, default is contains #[clap(short = 'M', long = "matching")] - matching: Option, + matching: Option, /// Allows case insensitive searching #[clap(short = 'i', long = "insensitive")] @@ -170,4 +167,35 @@ pub struct Args { /// Runs command for the displayed entries, without changing the output. %s for the real string #[clap(short = 'r', long = "pre-display-cmd")] pre_display_cmd: Option, + + /// Defines how good a fuzzy match must be, to be shown. + #[clap(long = "fuzzy-min-score")] + fuzzy_min_score: Option, + + /// Size of displayed images + #[clap(long = "image-size")] + image_size: Option, + + /// Orientation of main window + #[clap(long = "orientation")] + orientation: Option, + + /// Orientation of the row box, defining if label is below or at the side. + #[clap(long = "row-box-orientation")] + row_bow_orientation: Option, + + /// Specifies the horizontal align for the entire scrolled area, + /// it can be any of fill, start, end, or center, default is fill. + #[clap(long = "halign")] + pub halign: Option, + //// Specifies the horizontal align for the individual entries, + // it can be any of fill, start, end, or center, default is fill. + #[clap(long = "content-halign")] + pub content_halign: Option, + + /// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e + /// nd, or center, the default is orientation dependent. If vertical then it defaults to + /// start, if horizontal it defaults to center. + #[clap(long = "valign")] + pub valign: Option, } diff --git a/src/gui.rs b/src/gui.rs deleted file mode 100644 index b65e12a..0000000 --- a/src/gui.rs +++ /dev/null @@ -1,290 +0,0 @@ -use crate::config::Config; -use anyhow::{Context, anyhow}; -use crossbeam::channel; -use crossbeam::channel::Sender; -use gdk4::gio::File; -use gdk4::glib::Propagation; -use gdk4::prelude::{Cast, DisplayExt, MonitorExt}; -use gdk4::{Display, Key}; -use gtk4::prelude::{ - ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, - FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt, -}; -use gtk4::{Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, Image, Label, ListBox, ListBoxRow, PolicyType, ScrolledWindow, SearchEntry, Widget}; -use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; -use gtk4_layer_shell::{KeyboardMode, LayerShell}; -use log::{debug, error, info}; -use std::process::exit; -use hyprland::ctl::output::create; -use hyprland::ctl::plugin::list; - -#[derive(Clone)] -pub struct MenuItem { - pub label: String, // todo support empty label? - pub icon_path: Option, - pub action: Option, - pub sub_elements: Vec, -} - -pub fn show(config: Config, elements: Vec) -> anyhow::Result<(i32)> { - // Load CSS - let provider = CssProvider::new(); - let css_file_path = File::for_path("/home/me/.config/wofi/style.css"); - - provider.load_from_file(&css_file_path); - // Apply CSS to the display - let display = Display::default().expect("Could not connect to a display"); - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - - let display = Display::default().expect("Could not connect to a display"); - // Apply CSS to the display - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - - // No need for application_id unless you want portal support - let app = Application::builder().application_id("worf").build(); - let (sender, receiver) = channel::bounded(1); - - app.connect_activate(move |app| { - // Create a toplevel undecorated window - let window = ApplicationWindow::builder() - .application(app) - .decorated(false) - .resizable(false) - .default_width(20) - .default_height(20) - .build(); - - window.set_widget_name("window"); - - config.normal_window.map(|normal| { - if !normal { - window.set_layer(gtk4_layer_shell::Layer::Overlay); - window.init_layer_shell(); - window.set_keyboard_mode(KeyboardMode::Exclusive); - window.set_namespace(Some("worf")); - } - }); - - let outer_box = gtk4::Box::new(Orientation::Vertical, 0); - outer_box.set_widget_name("outer-box"); - window.set_child(Some(&outer_box)); - - let entry = SearchEntry::new(); - entry.set_widget_name("input"); - entry.set_css_classes(&["input"]); - entry.set_placeholder_text(config.prompt.as_deref()); - - // Example `search` and `password_char` usage - // let password_char = Some('*'); - // todo\ - // if let Some(c) = password_char { - // let entry_casted: Entry = entry.clone().upcast(); - // entry_casted.set_visibility(false); - // entry_casted.set_invisible_char(c); - // } - - outer_box.append(&entry); - - let scroll = ScrolledWindow::new(); - scroll.set_widget_name("scroll"); - scroll.set_hexpand(true); - scroll.set_vexpand(true); - - let hide_scroll = false; // todo - if hide_scroll { - scroll.set_policy(PolicyType::External, PolicyType::External); - } - - outer_box.append(&scroll); - - let inner_box = FlowBox::new(); - inner_box.set_widget_name("inner-box"); - inner_box.set_css_classes(&["inner-box"]); - - inner_box.set_selection_mode(gtk4::SelectionMode::Browse); - inner_box.set_max_children_per_line(1); // todo change to `columns` variable - //inner_box.set_orientation(Orientation::Horizontal); // or Vertical - inner_box.set_halign(Align::Fill); - inner_box.set_valign(Align::Start); - inner_box.set_activate_on_single_click(true); - - for entry in &elements { - add_menu_item(&inner_box, &entry); - } - - // Set focus after everything is realized - inner_box.connect_map(|fb| { - fb.grab_focus(); - }); - - let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0); - wrapper_box.set_homogeneous(true); - wrapper_box.append(&inner_box); - scroll.set_child(Some(&wrapper_box)); - - // todo implement search function - // // Dummy filter and sort funcs – replace with actual logic - // inner_box.set_filter_func(Some(Box::new(|_child| { - // true // filter logic here - // }))); - // inner_box.set_sort_func(Some(Box::new(|child1, child2| { - // child1.widget_name().cmp(&child2.widget_name()) - // }))); - - // Create key event controller - let entry_clone = entry.clone(); - setup_key_event_handler(&window, entry_clone, inner_box, app.clone(), sender.clone()); - - window.show(); - - // Get the display where the window resides - let display = window.display(); - - // Get the monitor that the window is on (use window's coordinates to find this) - window.surface().map(|surface| { - let monitor = display.monitor_at_surface(&surface); - if let Some(monitor) = monitor { - let geometry = monitor.geometry(); - config.width.as_ref().map(|width| { - percent_or_absolute(&width, geometry.width()) - .map(|w| window.set_width_request(w)) - }); - config.height.as_ref().map(|height| { - percent_or_absolute(&height, geometry.height()) - .map(|h| window.set_height_request(h)) - }); - } else { - error!("failed to get monitor to init window size"); - } - }); - }); - - let empty_array: [&str; 0] = []; - - app.run_with_args(&empty_array); - let selected_index = receiver.recv()?; - Ok(selected_index) -} - -fn setup_key_event_handler( - window: &ApplicationWindow, - entry_clone: SearchEntry, - inner_box: FlowBox, - app: Application, - sender: Sender, -) { - let key_controller = EventControllerKey::new(); - key_controller.connect_key_pressed(move |_, key_value, _, _| { - match key_value { - Key::Escape => exit(1), // todo better way to do this? - Key::Return => { - for s in &inner_box.selected_children() { - // let element : &Option<&EntryElement> = &elements.get(s.index() as usize); - // if let Some(element) = *element { - // debug!("Running action on element with name {}", element.label); - // (element.action)(); - // } - if let Err(e) = sender.send(s.index()) { - error!("failed to send selected child {e:?}") - } - app.quit(); - } - } - _ => { - if let Some(c) = key_value.name() { - // Only proceed if it's a single alphanumeric character - if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) { - let current = entry_clone.text().to_string(); - entry_clone.set_text(&format!("{current}{c}")); - } - } - } - } - - Propagation::Proceed - }); - // Add the controller to the window - window.add_controller(key_controller); -} - -fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) { - let parent: Widget = if !entry_element.sub_elements.is_empty() { - let expander = Expander::new(None); - expander.set_widget_name("expander-box"); - expander.set_halign(Align::Fill); - - let menu_row = create_menu_row(entry_element); - expander.set_label_widget(Some(&menu_row)); - - let list_box = ListBox::new(); - list_box.set_widget_name("entry"); - - // todo multi nesting is not supported yet. - for sub_item in entry_element.sub_elements.iter(){ - list_box.append(&create_menu_row(sub_item)); - } - - expander.set_child(Some(&list_box)); - expander.upcast() - } else { - create_menu_row(entry_element).upcast() - }; - - parent.set_halign(Align::Start); - - let child = FlowBoxChild::new(); - child.set_widget_name("entry"); - child.set_child(Some(&parent)); - - inner_box.append(&child); -} - -fn create_menu_row(menu_item: &MenuItem) -> Widget { - let row = ListBoxRow::new(); - row.set_widget_name("entry"); - row.set_hexpand(true); - row.set_halign(Align::Start); - - let row_box = gtk4::Box::new(Orientation::Horizontal, 0); - row.set_child(Some(&row_box)); - - if let Some(image_path) = &menu_item.icon_path { - // todo check config too - let image = Image::from_icon_name(image_path); - image.set_pixel_size(24); - image.set_widget_name("img"); - row_box.append(&image); - } - - let label = Label::new(Some(&menu_item.label)); - - label.set_widget_name("unselected"); - row_box.append(&label); - - - row.upcast() -} - -fn percent_or_absolute(value: &String, base_value: i32) -> Option { - if value.contains("%") { - let value = value.replace("%", ""); - let value = value.trim(); - match value.parse::() { - Ok(n) => { - let result = ((n as f32 / 100.0) * base_value as f32) as i32; - Some(result) - } - Err(_) => None, - } - } else { - value.parse::().ok() - } -} diff --git a/src/config.rs b/src/lib/config.rs similarity index 70% rename from src/config.rs rename to src/lib/config.rs index b5c20f7..c98db63 100644 --- a/src/config.rs +++ b/src/lib/config.rs @@ -1,16 +1,41 @@ use crate::args::Args; +use crate::lib::system; use anyhow::anyhow; +use clap::ValueEnum; use gtk4::prelude::ToValue; use merge::Merge; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::env; +use std::path::PathBuf; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] +pub enum MatchMethod { + Fuzzy, + Contains, + MultiContains, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] +pub enum Orientation { + Vertical, + Horizontal, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] +pub enum Align { + Fill, + Start, + Center, +} #[derive(Debug, Deserialize, Serialize, Merge, Clone)] pub struct Config { + /// Defines the path to the stylesheet being used. + /// Defaults to XDG_CONFIG_DIR/worf/style.css + /// If XDG_CONFIG_DIR is not defined $HOME/.config will be used instead + #[serde(default = "default_style")] pub style: Option, - pub stylesheet: Option, - pub color: Option, - pub colors: Option, pub show: Option, pub mode: Option, #[serde(default = "default_width")] @@ -32,24 +57,46 @@ pub struct Config { pub password: Option, pub exec_search: Option, pub hide_scroll: Option, - pub matching: Option, + + /// Defines how matching is done + #[serde(default = "default_match_method")] + pub matching: Option, pub insensitive: Option, pub parse_search: Option, pub location: Option, pub no_actions: Option, pub lines: Option, + /// Defines how many columns are shown per row + #[serde(default = "default_columns")] pub columns: Option, pub sort_order: Option, pub gtk_dark: Option, pub search: Option, pub monitor: Option, pub pre_display_cmd: Option, - pub orientation: Option, - pub halign: Option, - pub content_halign: Option, - pub valign: Option, + /// Defines how the entries root container are ordered + /// Default is vertical + #[serde(default = "default_orientation")] + pub orientation: Option, + /// Specifies the horizontal align for the entire scrolled area, + /// it can be any of fill, start, end, or center, default is fill. + #[serde(default = "default_halign")] + pub halign: Option, + //// Specifies the horizontal align for the individual entries, + // it can be any of fill, start, end, or center, default is fill. + #[serde(default = "default_content_halign")] + pub content_halign: Option, + + /// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e + /// nd, or center, the default is orientation dependent. If vertical then it defaults to + /// start, if horizontal it defaults to center. + pub valign: Option, + pub filter_rate: Option, - pub image_size: Option, + /// Specifies the image size when enabled. + /// Defaults to 32. + #[serde(default = "default_image_size")] + pub image_size: Option, pub key_up: Option, pub key_down: Option, pub key_left: Option, @@ -73,19 +120,28 @@ pub struct Config { pub copy_exec: Option, pub single_click: Option, pub pre_display_exec: Option, + + // Exclusive options + /// Minimum score for the fuzzy finder to accept a match. + /// Must be a value between 0 and 1 + /// Defaults to 0.1. + #[serde(default = "default_fuzzy_min_score")] + pub fuzzy_min_score: Option, + + /// Defines how the content in the row box is aligned + /// Defaults to vertical + #[serde(default = "default_row_box_orientation")] + pub row_bow_orientation: Option, } impl Default for Config { fn default() -> Self { Config { - style: None, - stylesheet: None, - color: None, - colors: None, + style: default_style(), show: None, mode: None, - width: None, - height: None, + width: default_width(), + height: default_height(), prompt: None, xoffset: None, x: None, @@ -105,18 +161,18 @@ impl Default for Config { location: None, no_actions: None, lines: None, - columns: None, + columns: default_columns(), sort_order: None, gtk_dark: None, search: None, monitor: None, pre_display_cmd: None, - orientation: None, - halign: None, - content_halign: None, + orientation: default_row_box_orientation(), + halign: default_halign(), + content_halign: default_content_halign(), valign: None, filter_rate: None, - image_size: None, + image_size: default_image_size(), key_up: None, key_down: None, key_left: None, @@ -139,10 +195,32 @@ impl Default for Config { copy_exec: None, single_click: None, pre_display_exec: None, + fuzzy_min_score: default_fuzzy_min_score(), + row_bow_orientation: default_row_box_orientation(), } } } +fn default_row_box_orientation() -> Option { + Some(Orientation::Horizontal) +} + +fn default_orientation() -> Option { + Some(Orientation::Vertical) +} + +fn default_halign() -> Option { + Some(Align::Fill) +} + +fn default_content_halign() -> Option { + Some(Align::Fill) +} + +fn default_columns() -> Option { + Some(1) +} + fn default_normal_window() -> Option { Some(false) } @@ -224,18 +302,44 @@ fn default_normal_window() -> Option { // key_default = "Ctrl-c"; // char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default); -fn default_height() -> Option { +fn default_style() -> Option { + system::config_path(None) + .ok() + .and_then(|pb| Some(pb.display().to_string())) + .or_else(|| { + log::error!("no stylesheet found, using system styles"); + None + }) +} + +pub fn default_height() -> Option { Some("40%".to_owned()) } -fn default_width() -> Option { +pub fn default_width() -> Option { Some("50%".to_owned()) } -fn default_password_char() -> Option { +pub fn default_password_char() -> Option { Some("*".to_owned()) } +pub fn default_fuzzy_min_length() -> Option { + Some(10) +} + +pub fn default_fuzzy_min_score() -> Option { + Some(0.1) +} + +pub fn default_match_method() -> Option { + Some(MatchMethod::Contains) +} + +pub fn default_image_size() -> Option { + Some(32) +} + pub fn merge_config_with_args(config: &mut Config, args: &Args) -> anyhow::Result { let args_json = serde_json::to_value(args)?; let mut config_json = serde_json::to_value(config)?; diff --git a/src/desktop/mod.rs b/src/lib/desktop.rs similarity index 95% rename from src/desktop/mod.rs rename to src/lib/desktop.rs index b712799..dae2a79 100644 --- a/src/desktop/mod.rs +++ b/src/lib/desktop.rs @@ -121,19 +121,14 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option Vec { let mut paths = vec![ PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/local/share/applications"), + PathBuf::from("/var/lib/flatpak/exports/share/applications"), ]; if let Some(home) = home_dir() { @@ -166,7 +162,6 @@ pub(crate) fn find_desktop_files() -> Vec { p } - pub fn get_locale_variants() -> Vec { let locale = env::var("LC_ALL") .or_else(|_| env::var("LC_MESSAGES")) diff --git a/src/lib/gui.rs b/src/lib/gui.rs new file mode 100644 index 0000000..13789e0 --- /dev/null +++ b/src/lib/gui.rs @@ -0,0 +1,544 @@ +use crate::lib::config; +use crate::lib::config::{Config, MatchMethod}; +use anyhow::{Context, anyhow}; +use crossbeam::channel; +use crossbeam::channel::Sender; +use gdk4::gio::{File, Menu}; +use gdk4::glib::{GString, Propagation, Unichar}; +use gdk4::prelude::{Cast, DisplayExt, ListModelExtManual, MonitorExt}; +use gdk4::{Display, Key}; +use gtk4::prelude::{ + ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, + FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, + WidgetExt, +}; +use gtk4::{ + Align, Entry, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, + ListBox, ListBoxRow, Ordering, PolicyType, Revealer, ScrolledWindow, SearchEntry, Widget, gdk, +}; +use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; +use gtk4_layer_shell::{KeyboardMode, LayerShell}; +use hyprland::ctl::output::create; +use hyprland::ctl::plugin::list; +use std::collections::HashMap; + +use log::{debug, error, info}; +use std::process::exit; +use std::sync::{Arc, Mutex, MutexGuard}; + +type ArcMenuMap = Arc>>; +type MenuItemSender = Sender>; + +impl Into for config::Orientation { + fn into(self) -> Orientation { + match self { + config::Orientation::Vertical => Orientation::Vertical, + config::Orientation::Horizontal => Orientation::Horizontal, + } + } +} + +impl Into for config::Align { + fn into(self) -> Align { + match self { + config::Align::Fill => Align::Fill, + config::Align::Start => Align::Start, + config::Align::Center => Align::Center, + } + } +} + +#[derive(Clone)] +pub struct MenuItem { + pub label: String, // todo support empty label? + pub icon_path: Option, + pub action: Option, + pub sub_elements: Vec, + pub working_dir: Option, + pub initial_sort_score: i64, + pub search_sort_score: f64, +} + +pub fn show(config: Config, elements: Vec) -> Result { + if let Some(ref css) = config.style { + let provider = CssProvider::new(); + let css_file_path = File::for_path(css); + provider.load_from_file(&css_file_path); + let display = Display::default().expect("Could not connect to a display"); + gtk4::style_context_add_provider_for_display( + &display, + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + + // No need for application_id unless you want portal support + let app = Application::builder().application_id("worf").build(); + let (sender, receiver) = channel::bounded(1); + + app.connect_activate(move |app| { + build_ui(&config, &elements, sender.clone(), app); + }); + + let empty_array: [&str; 0] = []; + app.run_with_args(&empty_array); + let selection = receiver.recv()?; + selection +} + +fn build_ui( + config: &Config, + elements: &Vec, + sender: Sender>, + app: &Application, +) { + // Create a toplevel undecorated window + let window = ApplicationWindow::builder() + .application(app) + .decorated(false) + .resizable(false) + .default_width(20) + .default_height(20) + .build(); + + window.set_widget_name("window"); + + config.normal_window.map(|normal| { + if !normal { + // Initialize the window as a layer + window.init_layer_shell(); + window.set_layer(gtk4_layer_shell::Layer::Overlay); + window.set_keyboard_mode(KeyboardMode::Exclusive); + window.set_namespace(Some("worf")); + } + }); + + let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); + outer_box.set_widget_name("outer-box"); + + window.set_child(Some(&outer_box)); + + let entry = SearchEntry::new(); + entry.set_widget_name("input"); + entry.set_css_classes(&["input"]); + entry.set_placeholder_text(config.prompt.as_deref()); + entry.set_sensitive(false); + outer_box.append(&entry); + + let scroll = ScrolledWindow::new(); + scroll.set_widget_name("scroll"); + scroll.set_hexpand(true); + scroll.set_vexpand(true); + // if let Some(valign) = config.valign { + // scroll.set_valign(valign.into()); + // } else { + // if config.orientation.unwrap() == config::Orientation::Horizontal { + // scroll.set_valign(Align::Center); + // } else { + // scroll.set_valign(Align::Start); + // } + // } + + let hide_scroll = false; // todo + if hide_scroll { + scroll.set_policy(PolicyType::External, PolicyType::External); + } + + outer_box.append(&scroll); + + let inner_box = FlowBox::new(); + inner_box.set_widget_name("inner-box"); + inner_box.set_css_classes(&["inner-box"]); + inner_box.set_hexpand(true); + inner_box.set_vexpand(false); + + inner_box.set_selection_mode(gtk4::SelectionMode::Browse); + inner_box.set_max_children_per_line(config.columns.unwrap()); + inner_box.set_activate_on_single_click(true); + + let mut list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); + for entry in elements { + list_items + .lock() + .unwrap() // panic here ok? deadlock? + .insert( + add_menu_item( + &inner_box, + &entry, + &config, + sender.clone(), + list_items.clone(), + app.clone(), + ), + entry.clone(), + ); + } + + let items_clone = list_items.clone(); + inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_clone)); + + // Set focus after everything is realized + inner_box.connect_map(|fb| { + fb.grab_focus(); + fb.invalidate_sort(); + }); + + let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0); + wrapper_box.append(&inner_box); + scroll.set_child(Some(&wrapper_box)); + + setup_key_event_handler( + &window, + entry.clone(), + inner_box, + app.clone(), + sender.clone(), + list_items.clone(), + config.clone(), + ); + + window.show(); + + let display = window.display(); + window.surface().map(|surface| { + // todo this does not work for multi monitor systems + let monitor = display.monitor_at_surface(&surface); + if let Some(monitor) = monitor { + let geometry = monitor.geometry(); + config.width.as_ref().map(|width| { + percent_or_absolute(&width, geometry.width()).map(|w| window.set_width_request(w)) + }); + config.height.as_ref().map(|height| { + percent_or_absolute(&height, geometry.height()) + .map(|h| window.set_height_request(h)) + }); + } else { + log::error!("failed to get monitor to init window size"); + } + }); +} + +fn setup_key_event_handler( + window: &ApplicationWindow, + entry_clone: SearchEntry, + inner_box: FlowBox, + app: Application, + sender: MenuItemSender, + list_items: Arc>>, + config: Config, +) { + let key_controller = EventControllerKey::new(); + + key_controller.connect_key_pressed(move |_, key_value, _, _| { + match key_value { + Key::Escape => { + if let Err(e) = sender.send(Err(anyhow!("No item selected"))) { + log::error!("failed to send message {e}"); + } + app.quit(); + } + Key::Return => { + if let Err(e) = handle_selected_item(&sender, &app, &inner_box, &list_items) { + log::error!("{e}"); + } + } + Key::BackSpace => { + let mut items = list_items.lock().unwrap(); + let mut query = entry_clone.text().to_string(); + query.pop(); + + entry_clone.set_text(&query); + filter_widgets(&query, &mut items, &config, &inner_box); + } + _ => { + let mut items = list_items.lock().unwrap(); + if let Some(c) = key_value.to_unicode() { + let current = entry_clone.text().to_string(); + let query = format!("{current}{c}"); + entry_clone.set_text(&query); + filter_widgets(&query, &mut items, &config, &inner_box); + } + } + } + + Propagation::Proceed + }); + window.add_controller(key_controller); +} + +fn sort_menu_items( + child1: &FlowBoxChild, + child2: &FlowBoxChild, + items_lock: &Mutex>, +) -> Ordering { + let lock = items_lock.lock().unwrap(); + let m1 = lock.get(child1); + let m2 = lock.get(child2); + + match (m1, m2) { + (Some(menu1), Some(menu2)) => { + if menu1.search_sort_score != 0.0 || menu2.search_sort_score != 0.0 { + if menu1.search_sort_score > menu2.search_sort_score { + Ordering::Smaller + } else { + Ordering::Larger + } + } else { + if menu1.initial_sort_score > menu2.initial_sort_score { + Ordering::Smaller + } else { + Ordering::Larger + } + } + } + (Some(_), None) => Ordering::Larger, + (None, Some(_)) => Ordering::Smaller, + (None, None) => Ordering::Equal, + } +} + +fn handle_selected_item( + sender: &MenuItemSender, + app: &Application, + inner_box: &FlowBox, + lock_arc: &ArcMenuMap, +) -> Result<(), String> { + for s in inner_box.selected_children() { + let list_items = lock_arc.lock().unwrap(); + let item = list_items.get(&s); + if let Some(item) = item { + if let Err(e) = sender.send(Ok(item.clone())) { + log::error!("failed to send message {e}"); + } + } + app.quit(); + return Ok(()); + } + Err("selected item cannot be resolved".to_owned()) +} + +fn add_menu_item( + inner_box: &FlowBox, + entry_element: &MenuItem, + config: &Config, + sender: MenuItemSender, + lock_arc: ArcMenuMap, + app: Application, +) -> FlowBoxChild { + let parent: Widget = if !entry_element.sub_elements.is_empty() { + let expander = Expander::new(None); + expander.set_widget_name("expander-box"); + expander.set_hexpand(true); + + let menu_row = create_menu_row( + entry_element, + config, + lock_arc.clone(), + sender.clone(), + app.clone(), + inner_box.clone(), + ); + expander.set_label_widget(Some(&menu_row)); + + let list_box = ListBox::new(); + list_box.set_hexpand(true); + list_box.set_halign(Align::Fill); + + for sub_item in &entry_element.sub_elements { + let sub_row = create_menu_row( + sub_item, + config, + lock_arc.clone(), + sender.clone(), + app.clone(), + inner_box.clone(), + ); + sub_row.set_hexpand(true); + sub_row.set_halign(Align::Fill); + sub_row.set_widget_name("entry"); + list_box.append(&sub_row); + } + + expander.set_child(Some(&list_box)); + expander.upcast() + } else { + create_menu_row( + entry_element, + config, + lock_arc.clone(), + sender.clone(), + app.clone(), + inner_box.clone(), + ) + .upcast() + }; + + parent.set_halign(Align::Fill); + parent.set_valign(Align::Start); + parent.set_hexpand(true); + + let child = FlowBoxChild::new(); + child.set_widget_name("entry"); + child.set_child(Some(&parent)); + child.set_hexpand(true); + child.set_vexpand(false); + + inner_box.append(&child); + child +} + +fn create_menu_row( + menu_item: &MenuItem, + config: &Config, + lock_arc: ArcMenuMap, + sender: MenuItemSender, + app: Application, + inner_box: FlowBox, +) -> Widget { + let row = ListBoxRow::new(); + row.set_hexpand(true); + row.set_halign(Align::Fill); + row.set_widget_name("row"); + + let click = GestureClick::new(); + click.set_button(gdk::BUTTON_PRIMARY); + click.connect_pressed(move |_gesture, n_press, _x, _y| { + if n_press == 2 { + if let Err(e) = handle_selected_item(&sender, &app, &inner_box, &lock_arc) { + log::error!("{e}"); + } + } + }); + + row.add_controller(click); + + let row_box = gtk4::Box::new(config.row_bow_orientation.unwrap().into(), 0); + row_box.set_hexpand(true); + row_box.set_vexpand(false); + row_box.set_halign(Align::Fill); + + row.set_child(Some(&row_box)); + + if let Some(image_path) = &menu_item.icon_path { + let image = Image::from_icon_name(image_path); + image.set_pixel_size( + config + .image_size + .unwrap_or(config::default_image_size().unwrap()), + ); + image.set_widget_name("img"); + row_box.append(&image); + } + + let label = Label::new(Some(&menu_item.label)); + label.set_hexpand(true); + row_box.append(&label); + + if config.content_halign.unwrap() == config::Align::Start + || config.content_halign.unwrap() == config::Align::Fill + { + label.set_xalign(0.0); + } + row.upcast() +} + +fn filter_widgets( + query: &str, + items: &mut HashMap, + config: &Config, + inner_box: &FlowBox, +) { + if items.is_empty() { + items.iter().for_each(|(child, _)| { + child.set_visible(true); + }); + if let Some(child) = inner_box.first_child() { + child.grab_focus(); + let fb = child.downcast::(); + if let Ok(fb) = fb { + inner_box.select_child(&fb); + } + } + return; + } + + let query = query.to_owned().to_lowercase(); + let mut highest_score = -1.0; + let mut fb: Option<&FlowBoxChild> = None; + items.iter_mut().for_each(|(flowbox_child, mut menu_item)| { + let menu_item_search = format!( + "{} {}", + menu_item + .action + .as_ref() + .map(|a| a.to_lowercase()) + .unwrap_or_default(), + &menu_item.label.to_lowercase() + ); + + let matching = if let Some(matching) = &config.matching { + matching + } else { + &config::default_match_method().unwrap() + }; + + let (search_sort_score, visible) = match matching { + MatchMethod::Fuzzy => { + let score = strsim::normalized_levenshtein(&query, &menu_item_search); + (score, score > config.fuzzy_min_score.unwrap()) + } + MatchMethod::Contains => { + if menu_item_search.contains(&query) { + (1.0, true) + } else { + (0.0, false) + } + } + MatchMethod::MultiContains => { + let score = query + .split(' ') + .filter(|i| menu_item_search.contains(i)) + .map(|_| 1.0) + .sum(); + (score, score > 0.0) + } + }; + + menu_item.search_sort_score = search_sort_score; + if visible { + highest_score = search_sort_score; + fb = Some(flowbox_child); + } + + flowbox_child.set_visible(visible); + }); + + if let Some(top_item) = fb { + inner_box.select_child(top_item); + top_item.grab_focus(); + } +} + +fn percent_or_absolute(value: &String, base_value: i32) -> Option { + if value.contains("%") { + let value = value.replace("%", "").trim().to_string(); + match value.parse::() { + Ok(n) => Some(((n as f32 / 100.0) * base_value as f32) as i32), + Err(_) => None, + } + } else { + value.parse::().ok() + } +} + +pub fn initialize_sort_scores(items: &mut Vec) { + let mut regular_score = items.len() as i64; + items.sort_by(|l, r| r.label.cmp(&l.label)); + + for item in items.iter_mut() { + if item.initial_sort_score == 0 { + item.initial_sort_score = regular_score; + regular_score += 1; + } + } +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs new file mode 100644 index 0000000..cc32f75 --- /dev/null +++ b/src/lib/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod desktop; +pub mod gui; +pub mod system; diff --git a/src/lib/system.rs b/src/lib/system.rs new file mode 100644 index 0000000..a1a4fcf --- /dev/null +++ b/src/lib/system.rs @@ -0,0 +1,31 @@ +use anyhow::anyhow; +use std::env; +use std::path::PathBuf; + +pub fn home_dir() -> Result { + env::var("HOME").map_err(|e| anyhow::anyhow!("$HOME not set: {e}")) +} + +pub fn conf_home() -> Result { + env::var("XDG_CONF_HOME").map_err(|e| anyhow::anyhow!("XDG_CONF_HOME not set: {e}")) +} + +pub fn config_path(config_path: Option) -> Result { + config_path + .map(PathBuf::from) + .and_then(|p| p.canonicalize().ok().filter(|c| c.exists())) + .or_else(|| { + [ + conf_home().ok().map(PathBuf::from), + home_dir() + .ok() + .map(PathBuf::from) + .map(|c| c.join(".config")), + ] + .into_iter() + .flatten() + .map(|base| base.join("worf").join("style.css")) + .find_map(|p| p.canonicalize().ok()) + }) + .ok_or_else(|| anyhow!("Could not find a valid config file.")) +} diff --git a/src/main.rs b/src/main.rs index 745fbd6..fee407c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,16 @@ #![warn(clippy::pedantic)] #![allow(clippy::implicit_return)] +// todo resolve paths like ~/ + use crate::args::{Args, Mode}; -use crate::config::{Config, merge_config_with_args}; -use crate::desktop::{default_icon, find_desktop_files, get_locale_variants}; -use crate::gui::MenuItem; +use crate::lib::config::{Config, merge_config_with_args}; +use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; +use crate::lib::gui; +use crate::lib::gui::MenuItem; +use anyhow::{Error, anyhow}; use clap::Parser; +use freedesktop_file_parser::{DesktopAction, EntryType}; use gdk4::prelude::Cast; use gtk4::prelude::{ ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, @@ -22,51 +27,40 @@ use std::process::{Command, Stdio}; use std::sync::Arc; use std::thread::sleep; use std::{env, fs, time}; -use freedesktop_file_parser::{DesktopAction, EntryType}; mod args; -mod config; -mod desktop; -mod gui; +mod lib; fn main() -> anyhow::Result<()> { gtk4::init()?; env_logger::Builder::new() - // todo change to info as default - .parse_filters(&std::env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_owned())) + // todo change to error as default + .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_owned())) .init(); let args = Args::parse(); - let home_dir = std::env::var("HOME")?; + let home_dir = env::var("HOME")?; let config_path = args .config .as_ref() .map(|c| PathBuf::from(c)) .unwrap_or_else(|| { - std::env::var("XDG_CONF_HOME") + env::var("XDG_CONF_HOME") .map_or( PathBuf::from(home_dir.clone()).join(".config"), |xdg_conf_home| PathBuf::from(&xdg_conf_home), ) - .join("wofi") // todo change to worf + .join("worf") .join("config") }); - // todo use this? - let colors_dir = std::env::var("XDG_CACHE_HOME") - .map_or( - PathBuf::from(home_dir.clone()).join(".cache"), - |xdg_conf_home| PathBuf::from(&xdg_conf_home), - ) - .join("wal") - .join("colors"); - let drun_cache = std::env::var("XDG_CACHE_HOME") + let drun_cache = env::var("XDG_CACHE_HOME") .map_or( PathBuf::from(home_dir.clone()).join(".cache"), |xdg_conf_home| PathBuf::from(&xdg_conf_home), ) - .join("worf-drun"); // todo change to worf + .join("worf-drun"); let toml_content = fs::read_to_string(config_path)?; let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly @@ -102,10 +96,14 @@ fn drun(mut config: Config) -> anyhow::Result<()> { let default_icon = default_icon(); for file in find_desktop_files().iter().filter(|f| { - f.entry.hidden.map_or(true, |hidden| !hidden) + f.entry.hidden.map_or(true, |hidden| !hidden) && f.entry.no_display.map_or(true, |no_display| !no_display) - // todo handle not shown in? }) { + let (action, working_dir) = match &file.entry.entry_type { + EntryType::Application(app) => (app.exec.clone(), app.path.clone()), + _ => (None, None), + }; + let name = lookup_name_with_locale( &locale_variants, &file.entry.name.variants, @@ -115,14 +113,26 @@ fn drun(mut config: Config) -> anyhow::Result<()> { debug!("Skipping desktop entry without name {file:?}") } - let icon = file.entry.icon.as_ref().map(|s| s.content.clone()); - debug!("file, name={name:?}, icon={icon:?}"); + let icon = file + .entry + .icon + .as_ref() + .map(|s| s.content.clone()) + .or(Some(default_icon.clone())); + debug!("file, name={name:?}, icon={icon:?}, action={action:?}"); + let mut sort_score = 0.0; + if name.as_ref().unwrap().contains("ox") { + sort_score = 999.0; + } let mut entry = MenuItem { label: name.unwrap(), icon_path: icon.clone(), - action: None, + action, sub_elements: Vec::default(), + working_dir: working_dir.clone(), + initial_sort_score: 0, + search_sort_score: sort_score, }; file.actions.iter().for_each(|(_, action)| { @@ -131,95 +141,83 @@ fn drun(mut config: Config) -> anyhow::Result<()> { &action.name.variants, &action.name.default, ); - let action_icon = action.icon.as_ref().map(|s| s.content.clone()).or(icon.as_ref().map(|s| s.clone())); + let action_icon = action + .icon + .as_ref() + .map(|s| s.content.clone()) + .or(icon.as_ref().map(|s| s.clone())); debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); let sub_entry = MenuItem { label: action_name.unwrap().trim().to_owned(), icon_path: action_icon, - action: None, + action: action.exec.clone(), sub_elements: Vec::default(), + working_dir: working_dir.clone(), + initial_sort_score: 0, + search_sort_score: 0.0, }; entry.sub_elements.push(sub_entry); }); entries.push(entry); - - // let desktop = Some("desktop entry"); - // let locale = - // env::var("LC_ALL") - // .or_else(|_| env::var("LC_MESSAGES")) - // .or_else(|_| env::var("LANG")) - // .unwrap_or_else(|_| "en_US.UTF-8".to_string()).split_once(".").map(|(k,_)| k.to_owned().to_lowercase()); - // - // - // - // - // if let Some(desktop_entry) = file.get("desktop entry") { - // let icon = desktop_entry - // .get("icon") - // .and_then(|x| x.as_ref().map(|x| x.to_owned())); - // - // - // let Some(exec) = desktop_entry.get("exec") - // - // - // - // .and_then(|x| x.as_ref()) else { - // warn!("Skipping desktop file {file:#?}"); - // continue; - // }; - // - // if let Some((cmd, _)) = exec.split_once(' ') { - // if !PathBuf::from(cmd).exists() { - // continue; - // } - // } - // - // let name = desktop_entry - // .get("name") - // .and_then(|x| x.as_ref().map(|x| x.to_owned())); - // - // if let Some(name) = name { - // entries.push({ - // EntryElement { - // label: name, - // icon_path: icon, - // action: Some(exec.clone()), - // sub_elements: None, - // } - // }) - // } - // } } - entries.sort_by(|l, r| l.label.cmp(&r.label)); - if config.prompt.is_none() { - config.prompt = Some("drun".to_owned()); - } + gui::initialize_sort_scores(&mut entries); // todo ues a arc instead of cloning the config - let selected_index = gui::show(config.clone(), entries.clone())?; - entries.get(selected_index as usize).map(|e| { - e.action.as_ref().map(|a| { - spawn_fork(&a); - }) - }); + let selection_result = gui::show(config.clone(), entries.clone()); + match selection_result { + Ok(selected_item) => { + if let Some(action) = selected_item.action { + spawn_fork(&action, &selected_item.working_dir)? + } + } + Err(e) => { + log::error!("{e}"); + } + } Ok(()) } -fn spawn_fork(cmd: &str) { - // todo fork this for real +fn spawn_fork(cmd: &str, working_dir: &Option) -> anyhow::Result<()> { // todo probably remove arguments? + // todo support working dir + // todo fix actions + // todo graphical disk map icon not working // Unix-like systems (Linux, macOS) - let _ = Command::new(cmd) - .stdin(Stdio::null()) // Disconnect stdin - .stdout(Stdio::null()) // Disconnect stdout - .stderr(Stdio::null()) // Disconnect stderr - .spawn(); - sleep(time::Duration::from_secs(30)); + + let parts = cmd.split(' ').collect::>(); + if parts.is_empty() { + return Err(anyhow!("empty command passed")); + } + + if let Some(dir) = working_dir { + env::set_current_dir(dir)?; + } + + let exec = parts[0]; + let args: Vec<_> = parts + .iter() + .skip(1) + .filter(|arg| !arg.starts_with("%")) + .collect(); + + unsafe { + let _ = Command::new(exec) + .args(args) + .stdin(Stdio::null()) // Disconnect stdin + .stdout(Stdio::null()) // Disconnect stdout + .stderr(Stdio::null()) // Disconnect stderr + .pre_exec(|| { + libc::setsid(); + Ok(()) + }) + .spawn(); + } + Ok(()) } // // fn main() -> anyhow::Result<()> { diff --git a/styles/compact.css b/styles/compact.css new file mode 100644 index 0000000..a87c642 --- /dev/null +++ b/styles/compact.css @@ -0,0 +1,69 @@ +* { +font-family: DejaVu; +} + +#window { + all: unset; + background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */ + border-radius: 0px; +} + +#window #outer-box { + /* The name of the search bar */ + /* The name of the scrolled window containing all of the entries */ + border: 2px solid rgba(63, 81, 181, 1); + border-radius: 6px; +} + +#window #outer-box #input { + background-color: rgba(32, 32, 32, 0.6); + color: #f2f2f2; + border-bottom: 2px solid rgba(214, 174, 0, 1); + padding: 0.8rem 1rem; + font-size: 1rem; +} + +#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active { + all: unset; + background-color: rgba(32, 32, 32, 0.6); + color: #f2f2f2; + border-bottom: 2px solid rgba(214, 174, 2, 1); + padding: 1.2rem 1.2rem 1.2rem 1rem; + font-size: 1rem; +} + +#window #outer-box #scroll { + /* The name of the box containing all of the entries */ +} +#window #outer-box #scroll #inner-box { + /* The name of all entries */ + /* The name of all boxes shown when expanding */ + /* entries with multiple actions */ +} +#window #outer-box #scroll #inner-box #entry { + color: #fff; + background-color: rgba(32, 32, 32, 0.1); + padding: 0.6rem 1rem; + /* The name of all images in entries displayed in image mode */ + /* The name of all the text in entries */ +} +#window #outer-box #scroll #inner-box #entry #img { + width: 1rem; + margin-right: 0.5rem; +} +#window #outer-box #scroll #inner-box #entry:selected { + color: #fff; + background-color: rgba(255, 255, 255, 0.1); + outline: none; +} + +#row:hover { + background-color: rgba(255, 255, 255, 0);; + outline: inherit; + outline-color: inherit; +} +#window #outer-box #scroll #inner-box #entry:hover { + background-color: rgba(255, 255, 255, 0.1); + outline: inherit; + outline-color: inherit; +} diff --git a/styles/launcher.css b/styles/launcher.css new file mode 100644 index 0000000..d4c075e --- /dev/null +++ b/styles/launcher.css @@ -0,0 +1,69 @@ +* { + font-family: DejaVu; +} + +#window { + all: unset; + background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */ + border-radius: 0px; +} + +#window #outer-box { + /* The name of the search bar */ + /* The name of the scrolled window containing all of the entries */ + border: 2px solid rgba(63, 81, 181, 1); + border-radius: 6px; +} + +#window #outer-box #input { + background-color: rgba(32, 32, 32, 0.6); + color: #f2f2f2; + border-bottom: 2px solid rgba(214, 174, 0, 1); + padding: 0.8rem 1rem; + font-size: 1rem; +} + +#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active { + all: unset; + background-color: rgba(32, 32, 32, 0.6); + color: #f2f2f2; + border-bottom: 2px solid rgba(214, 174, 2, 1); + font-size: 1rem; +} + +#window #outer-box #scroll { + /* The name of the box containing all of the entries */ +} +#window #outer-box #scroll #inner-box { + /* The name of all entries */ + /* The name of all boxes shown when expanding */ + /* entries with multiple actions */ +} +#window #outer-box #scroll #inner-box #entry { + color: #fff; + background-color: rgba(32, 32, 32, 0.1); + padding: 1rem; + margin: 1rem; + border-radius: 0.5rem; +} +#window #outer-box #scroll #inner-box #entry #img { + width: 1rem; + margin-right: 0.5rem; +} +#window #outer-box #scroll #inner-box #entry:selected { + color: #fff; + background-color: rgba(255, 255, 255, 0.1); + outline: none; +} + +#row:hover { + background-color: rgba(255, 255, 255, 0);; + outline: inherit; + outline-color: inherit; + +} +#window #outer-box #scroll #inner-box #entry:hover { + background-color: rgba(255, 255, 255, 0.1); + outline: inherit; + outline-color: inherit; +} From 9a53ad834a20610c53cfb23cdb18ab8b9c20ca41 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 15 Apr 2025 22:07:28 +0200 Subject: [PATCH 05/10] add support for halign --- src/lib/gui.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 13789e0..3dc3c60 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -72,7 +72,6 @@ pub fn show(config: Config, elements: Vec) -> Result) -> Result Date: Tue, 15 Apr 2025 22:11:31 +0200 Subject: [PATCH 06/10] support valign --- src/lib/gui.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 3dc3c60..f59d490 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -147,6 +147,16 @@ fn build_ui( inner_box.set_halign(Align::Fill); } + if let Some(valign) = config.valign { + inner_box.set_valign(valign.into()); + } else { + if config.orientation.unwrap() == config::Orientation::Horizontal { + inner_box.set_valign(Align::Center); + } else { + inner_box.set_valign(Align::Start); + } + } + inner_box.set_selection_mode(gtk4::SelectionMode::Browse); inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_activate_on_single_click(true); From 8a7bff1ad6b5b3a62c4c1882e65a6b1094640c1c Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 15 Apr 2025 22:47:26 +0200 Subject: [PATCH 07/10] add support for wrapping labels --- README.md | 11 +++++-- src/lib/gui.rs | 32 ++++++++++++++++++--- styles/{compact.css => compact/style.css} | 0 styles/launcher/config.toml | 8 ++++++ styles/{launcher.css => launcher/style.css} | 11 +++++++ 5 files changed, 56 insertions(+), 6 deletions(-) rename styles/{compact.css => compact/style.css} (100%) create mode 100644 styles/launcher/config.toml rename styles/{launcher.css => launcher/style.css} (88%) diff --git a/README.md b/README.md index fd79220..a221feb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ layerrule = blur, worf ## Additional functionality compared to Wofi (planed) * Support passing 'hidden' parameters that are not visible in the launcher but will be returned to the application * Window switcher for hyprland +* All arguments expect show are supported by config and args + +### New config / command line options +* fuzzy-length: Defines how long a string must be to be considered for fuzzy match +* row-box-orientation: Allows aligning values vertically to place the label below the icon + +### New Styling options +* `label`: Allows styling the label +* `row`: Allows styling to row, mainly used to disable hover effects ## Breaking changes to Wofi * Runtime behaviour is not guaranteed to be the same and won't ever be, this includes error messages and themes. @@ -28,8 +37,6 @@ layerrule = blur, worf * stylesheet -> use style instead * color / colors -> GTK4 does not support color files -## New options -* --fuzzy-length: Defines how long a string must be be ## Not supported diff --git a/src/lib/gui.rs b/src/lib/gui.rs index f59d490..1ef39e7 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -6,7 +6,7 @@ use crossbeam::channel::Sender; use gdk4::gio::{File, Menu}; use gdk4::glib::{GString, Propagation, Unichar}; use gdk4::prelude::{Cast, DisplayExt, ListModelExtManual, MonitorExt}; -use gdk4::{Display, Key}; +use gdk4::{pango, Display, Key}; use gtk4::prelude::{ ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, @@ -143,8 +143,6 @@ fn build_ui( inner_box.set_vexpand(false); if let Some(halign) = config.halign { inner_box.set_halign(halign.into()); - } else { - inner_box.set_halign(Align::Fill); } if let Some(valign) = config.valign { @@ -435,8 +433,11 @@ fn create_menu_row( row_box.append(&image); } - let label = Label::new(Some(&menu_item.label)); + // todo make max length configurable + let label = Label::new(Some(&wrap_text(&menu_item.label, 15))); label.set_hexpand(true); + label.set_widget_name("label"); + label.set_wrap(true); row_box.append(&label); if config.content_halign.unwrap() == config::Align::Start @@ -547,3 +548,26 @@ pub fn initialize_sort_scores(items: &mut Vec) { } } } + +fn wrap_text(text: &str, line_length: usize) -> String { + let mut result = String::new(); + let mut line = String::new(); + + for word in text.split_whitespace() { + if line.len() + word.len() + 1 > line_length { + if !line.is_empty() { + result.push_str(&line.trim_end()); + result.push('\n'); + line.clear(); + } + } + line.push_str(word); + line.push(' '); + } + + if !line.is_empty() { + result.push_str(&line.trim_end()); + } + + result +} diff --git a/styles/compact.css b/styles/compact/style.css similarity index 100% rename from styles/compact.css rename to styles/compact/style.css diff --git a/styles/launcher/config.toml b/styles/launcher/config.toml new file mode 100644 index 0000000..bae4b84 --- /dev/null +++ b/styles/launcher/config.toml @@ -0,0 +1,8 @@ +image_size=64 +columns=6 +orientation="Vertical" +row_bow_orientation="Vertical" +content_halign="Center" +height="70%" +width="60%" +valign="Start" diff --git a/styles/launcher.css b/styles/launcher/style.css similarity index 88% rename from styles/launcher.css rename to styles/launcher/style.css index d4c075e..141b141 100644 --- a/styles/launcher.css +++ b/styles/launcher/style.css @@ -45,15 +45,21 @@ padding: 1rem; margin: 1rem; border-radius: 0.5rem; + border-bottom: 5px solid rgba(32, 32, 32, 0.1); + } #window #outer-box #scroll #inner-box #entry #img { width: 1rem; margin-right: 0.5rem; } + #window #outer-box #scroll #inner-box #entry:selected { color: #fff; background-color: rgba(255, 255, 255, 0.1); outline: none; + border-bottom: 5px solid rgba(214, 174, 0, 1); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } #row:hover { @@ -67,3 +73,8 @@ outline: inherit; outline-color: inherit; } + +#label { + margin-top: 1rem; + margin-bottom: 0; +} From 9be4510e5391ed688518f2bb7529a92200abe195 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Wed, 16 Apr 2025 23:14:45 +0200 Subject: [PATCH 08/10] drun: * move drun into modes * add support for wrapping labels * make MenuItem a generic struct * add optional field to struct to pass data along * merge config and args --- Cargo.lock | 594 ++++++---------------------------------------- Cargo.toml | 7 +- README.md | 2 + src/args.rs | 201 ---------------- src/lib/config.rs | 291 +++++++++++++++++++---- src/lib/gui.rs | 98 ++++---- src/lib/mod.rs | 1 + src/lib/mode.rs | 211 ++++++++++++++++ src/lib/system.rs | 28 --- src/main.rs | 182 ++------------ 10 files changed, 610 insertions(+), 1005 deletions(-) delete mode 100644 src/args.rs create mode 100644 src/lib/mode.rs diff --git a/Cargo.lock b/Cargo.lock index 8c4684d..b1c3533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -123,7 +123,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", "winapi", ] @@ -161,26 +161,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "bytemuck" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "bytes" version = "1.10.1" @@ -210,54 +190,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.9.0", - "log", - "polling", - "rustix", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10929724661d1c43856fd87c7a127ae944ec55579134fb485e4136fb6a46fdcb" -dependencies = [ - "bitflags 2.9.0", - "polling", - "rustix", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "cc" -version = "1.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" -dependencies = [ - "shlex", -] - [[package]] name = "cfg-expr" version = "0.17.2" @@ -321,7 +253,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -345,15 +277,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "configparser" version = "1.0.0" @@ -416,12 +339,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "cursor-icon" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" - [[package]] name = "derive_more" version = "1.0.0" @@ -439,15 +356,30 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", "unicode-xid", ] [[package]] -name = "downcast-rs" -version = "1.2.1" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] [[package]] name = "either" @@ -484,16 +416,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -510,7 +432,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6059d3997cc694ec3e9a378db855866233ef7edfeafd85afcb2239fd130e6e6b" dependencies = [ - "thiserror 2.0.12", + "thiserror", "xdgkit", ] @@ -564,7 +486,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -645,6 +567,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.1" @@ -732,7 +665,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -868,7 +801,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -917,12 +850,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "home" version = "0.5.11" @@ -962,7 +889,7 @@ checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1027,7 +954,7 @@ checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1043,16 +970,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "log" @@ -1066,24 +997,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -1093,28 +1006,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "merge" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" -dependencies = [ - "merge_derive", - "num-traits", -] - -[[package]] -name = "merge_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1135,15 +1026,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1153,15 +1035,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" -dependencies = [ - "bitflags 2.9.0", -] - [[package]] name = "object" version = "0.36.7" @@ -1177,6 +1050,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -1243,7 +1122,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1273,21 +1152,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "portable-atomic" version = "1.11.0" @@ -1312,30 +1176,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.94" @@ -1355,15 +1195,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.40" @@ -1388,6 +1219,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.1" @@ -1432,19 +1274,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - [[package]] name = "ryu" version = "1.0.20" @@ -1474,7 +1303,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1497,7 +1326,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1509,12 +1338,6 @@ dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "siphasher" version = "1.0.1" @@ -1536,34 +1359,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.9.0", - "bytemuck", - "calloop 0.13.0", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.5", - "pkg-config", - "rustix", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkbcommon", - "xkeysym", -] - [[package]] name = "socket2" version = "0.5.9" @@ -1586,17 +1381,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.100" @@ -1608,19 +1392,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sysinfo" -version = "0.34.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "windows", -] - [[package]] name = "system-deps" version = "7.0.3" @@ -1655,33 +1426,13 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "thiserror-impl", ] [[package]] @@ -1692,7 +1443,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1725,7 +1476,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1762,23 +1513,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" - [[package]] name = "unicode-ident" version = "1.0.18" @@ -1815,98 +1549,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wayland-backend" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" -dependencies = [ - "cc", - "downcast-rs", - "rustix", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" -dependencies = [ - "bitflags 2.9.0", - "rustix", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.9.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" -dependencies = [ - "rustix", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" -dependencies = [ - "bitflags 2.9.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" -dependencies = [ - "bitflags 2.9.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" -dependencies = [ - "proc-macro2", - "quick-xml 0.37.4", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" -dependencies = [ - "pkg-config", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1938,59 +1580,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", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-targets", -] - -[[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.100", -] - -[[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.100", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2087,9 +1676,9 @@ name = "worf" version = "0.1.0" dependencies = [ "anyhow", - "calloop 0.14.2", "clap 4.5.35", "crossbeam", + "dirs", "env_logger", "freedesktop-file-parser", "gdk4", @@ -2100,25 +1689,14 @@ dependencies = [ "ini", "libc", "log", - "merge", "regex", "serde", "serde_json", - "smithay-client-toolkit", "strsim 0.11.1", - "sysinfo", - "thiserror 2.0.12", + "thiserror", "toml", - "wayland-client", - "wayland-protocols", ] -[[package]] -name = "xcursor" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" - [[package]] name = "xdgkit" version = "3.2.5" @@ -2126,31 +1704,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeac9c0125f3c131c6a2898d2a9f25c11b7954c3ff644a018cb9e06fa92919b" dependencies = [ "clap 3.2.25", - "quick-xml 0.21.0", + "quick-xml", "serde", "tini", ] -[[package]] -name = "xkbcommon" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" -dependencies = [ - "libc", - "memmap2 0.8.0", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -dependencies = [ - "bytemuck", -] - [[package]] name = "xml-rs" version = "0.8.25" @@ -2183,5 +1741,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 8af58ad..b3486b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,19 +13,14 @@ home = "0.5.11" log = "0.4.27" regex = "1.11.1" hyprland = "0.4.0-beta.2" -sysinfo = "0.34.2" ini = "1.3.0" clap = { version = "4.5.35", features = ["derive"] } thiserror = "2.0.12" serde = { version = "1.0.219", features = ["derive"] } toml = "0.8.20" -merge = "0.1.0" serde_json = "1.0.140" -wayland-client = "0.31.8" -wayland-protocols = "0.32.6" -smithay-client-toolkit = { version = "0.19.2", features = ["calloop"]} -calloop = "0.14.2" crossbeam = "0.8.4" libc = "0.2.171" freedesktop-file-parser = "0.1.0" strsim = "0.11.1" +dirs = "6.0.0" diff --git a/README.md b/README.md index a221feb..df84cbf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ layerrule = blur, worf because worf is build on GTK4 instead of GTK3 there will be differences in the look and feel. * Configuration files are not 100% compatible, Worf is using toml files instead, for most part this only means strings have to be quoted * Color files are not supported +* `mode` dropped, use show +* `D` argument dropped. Arguments are the same as config in worf, no need to have have this flag. ## Dropped configuration options * stylesheet -> use style instead diff --git a/src/args.rs b/src/args.rs deleted file mode 100644 index 94002a4..0000000 --- a/src/args.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::lib::config::{Align, MatchMethod, Orientation}; -use clap::Parser; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use thiserror::Error; - -// Define a custom error type using the `thiserror` crate -#[derive(Debug, Error)] -pub enum ArgsError { - #[error("input is not valid {0}")] - InvalidParameter(String), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum Mode { - /// searches $PATH for executables and allows them to be run by selecting them. - Run, - /// searches $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/applications for desktop files and allows them to be run by selecting them. - Drun, - - /// reads from stdin and displays options which when selected will be output to stdout. - Dmenu, -} - -impl FromStr for Mode { - type Err = ArgsError; - - fn from_str(s: &str) -> Result { - match s { - "run" => Ok(Mode::Run), - "drun" => Ok(Mode::Drun), - "dmenu" => Ok(Mode::Dmenu), - _ => Err(ArgsError::InvalidParameter( - format!("{s} is not a valid argument show this, see help for details").to_owned(), - )), - } - } -} - -#[derive(Parser, Debug, Deserialize, Serialize)] -#[clap(about = "Worf is a wofi clone written in rust, it aims to be a drop in replacement")] -pub struct Args { - /// Forks the menu so you can close the terminal - #[clap(short = 'f', long = "fork")] - fork: bool, - - /// Selects a config file to use - #[clap(short = 'c', long = "conf")] - pub config: Option, - - /// Selects a stylesheet to use - #[clap(short = 's', long = "style")] - style: Option, - - /// Runs in dmenu mode - #[clap(short = 'd', long = "dmenu")] - dmenu: bool, - - /// Specifies the mode to run in. A list can be found in wofi(7) - #[clap(long = "show")] - pub mode: Mode, - - /// Specifies the surface width - #[clap(short = 'W', long = "width")] - width: Option, - - /// Specifies the surface height - #[clap(short = 'H', long = "height")] - height: Option, - - /// Prompt to display - #[clap(short = 'p', long = "prompt")] - pub prompt: Option, - - /// The x offset - #[clap(short = 'x', long = "xoffset")] - x: Option, - - /// The y offset - #[clap(short = 'y', long = "yoffset")] - y: Option, - - /// Render to a normal window - #[clap(short = 'n', long = "normal-window")] - normal_window: bool, - - /// Allows images to be rendered - #[clap(short = 'I', long = "allow-images")] - allow_images: bool, - - /// Allows pango markup - #[clap(short = 'm', long = "allow-markup")] - allow_markup: bool, - - /// Sets the cache file to use - #[clap(short = 'k', long = "cache-file")] - cache_file: Option, - - /// Specifies the terminal to use when running in a term - #[clap(short = 't', long = "term")] - terminal: Option, - - /// Runs in password mode - #[clap(short = 'P', long = "password")] - password_char: Option, - - /// Makes enter always use the search contents, not the first result - #[clap(short = 'e', long = "exec-search")] - exec_search: bool, - - /// Hides the scroll bars - #[clap(short = 'b', long = "hide-scroll")] - hide_scroll: bool, - - /// Sets the matching method, default is contains - #[clap(short = 'M', long = "matching")] - matching: Option, - - /// Allows case insensitive searching - #[clap(short = 'i', long = "insensitive")] - insensitive: bool, - - /// Parses the search text removing image escapes and pango - #[clap(short = 'q', long = "parse-search")] - parse_search: bool, - - /// Prints the version and then exits - #[clap(short = 'v', long = "version")] - version: bool, - - /// Sets the location - #[clap(short = 'l', long = "location")] - location: Option, - - /// Disables multiple actions for modes that support it - #[clap(short = 'a', long = "no-actions")] - no_actions: bool, - - /// Sets a config option - #[clap(short = 'D', long = "define")] - define: Option, - - /// Sets the height in number of lines - #[clap(short = 'L', long = "lines")] - lines: Option, - - /// Sets the number of columns to display - #[clap(short = 'w', long = "columns")] - columns: Option, - - /// Sets the sort order - #[clap(short = 'O', long = "sort-order")] - sort_order: Option, - - /// Uses the dark variant of the current GTK theme - #[clap(short = 'G', long = "gtk-dark")] - gtk_dark: bool, - - /// Search for something immediately on open - #[clap(short = 'Q', long = "search")] - search: Option, - - /// Sets the monitor to open on - #[clap(short = 'o', long = "monitor")] - monitor: Option, - - /// Runs command for the displayed entries, without changing the output. %s for the real string - #[clap(short = 'r', long = "pre-display-cmd")] - pre_display_cmd: Option, - - /// Defines how good a fuzzy match must be, to be shown. - #[clap(long = "fuzzy-min-score")] - fuzzy_min_score: Option, - - /// Size of displayed images - #[clap(long = "image-size")] - image_size: Option, - - /// Orientation of main window - #[clap(long = "orientation")] - orientation: Option, - - /// Orientation of the row box, defining if label is below or at the side. - #[clap(long = "row-box-orientation")] - row_bow_orientation: Option, - - /// Specifies the horizontal align for the entire scrolled area, - /// it can be any of fill, start, end, or center, default is fill. - #[clap(long = "halign")] - pub halign: Option, - //// Specifies the horizontal align for the individual entries, - // it can be any of fill, start, end, or center, default is fill. - #[clap(long = "content-halign")] - pub content_halign: Option, - - /// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e - /// nd, or center, the default is orientation dependent. If vertical then it defaults to - /// start, if horizontal it defaults to center. - #[clap(long = "valign")] - pub valign: Option, -} diff --git a/src/lib/config.rs b/src/lib/config.rs index c98db63..805d414 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -1,13 +1,16 @@ -use crate::args::Args; use crate::lib::system; -use anyhow::anyhow; -use clap::ValueEnum; +use anyhow::{anyhow, Context}; +use clap::builder::TypedValueParser; +use clap::{Parser, ValueEnum}; use gtk4::prelude::ToValue; -use merge::Merge; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::env; +use std::collections::HashMap; +use std::env::Args; use std::path::PathBuf; +use std::str::FromStr; +use std::{env, fs}; +use thiserror::Error; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum MatchMethod { @@ -29,74 +32,180 @@ pub enum Align { Center, } -#[derive(Debug, Deserialize, Serialize, Merge, Clone)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Mode { + /// searches $PATH for executables and allows them to be run by selecting them. + Run, + /// searches $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/applications f + /// or desktop files and allows them to be run by selecting them. + Drun, + + /// reads from stdin and displays options which when selected will be output to stdout. + Dmenu, +} + +#[derive(Debug, Error)] +pub enum ArgsError { + #[error("input is not valid {0}")] + InvalidParameter(String), +} + +impl FromStr for Mode { + type Err = ArgsError; + + fn from_str(s: &str) -> Result { + match s { + "run" => Ok(Mode::Run), + "drun" => Ok(Mode::Drun), + "dmenu" => Ok(Mode::Dmenu), + _ => Err(ArgsError::InvalidParameter( + format!("{s} is not a valid argument show this, see help for details").to_owned(), + )), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Parser)] +#[clap(about = "Worf is a wofi clone written in rust, it aims to be a drop-in replacement")] pub struct Config { - /// Defines the path to the stylesheet being used. - /// Defaults to XDG_CONFIG_DIR/worf/style.css - /// If XDG_CONFIG_DIR is not defined $HOME/.config will be used instead + /// Forks the menu so you can close the terminal + #[clap(short = 'f', long = "fork")] + pub fork: Option, + + /// Selects a config file to use + #[clap(short = 'c', long = "conf")] + pub config: Option, + + /// Runs in dmenu mode + #[clap(short = 'd', long = "dmenu")] + pub dmenu: Option, + + /// Prints the version and then exits + #[clap(short = 'v', long = "version")] + pub version: Option, + + /// Defines the style sheet to be loaded. + /// Defaults to $XDG_CONF_DIR/worf/style.css + /// or $HOME/.config/worf/style.css if XDG_CONF_DIR is not set. #[serde(default = "default_style")] + #[clap(long = "style")] pub style: Option, - pub show: Option, - pub mode: Option, + + /// Defines the mode worf is running in + #[clap(long = "show")] + pub show: Option, + + /// Default width of the window, defaults to 50% of the screen #[serde(default = "default_width")] + #[clap(long = "width")] pub width: Option, + + /// Default height of the window, defaults to 40% of the screen #[serde(default = "default_height")] + #[clap(long = "height")] pub height: Option, + + #[clap(short = 'p', long = "prompt")] pub prompt: Option, + + #[clap(short = 'x', long = "xoffset")] pub xoffset: Option, + + #[clap(long = "x")] pub x: Option, + + #[clap(short = 'y', long = "yoffset")] pub yoffset: Option, + + #[clap(long = "y")] pub y: Option, + + /// If true a normal window instead of a layer shell will be used #[serde(default = "default_normal_window")] - pub normal_window: Option, + #[clap(short = 'n', long = "normal-window")] + pub normal_window: bool, + + #[clap(short = 'I', long = "allow-images")] pub allow_images: Option, + + #[clap(short = 'm', long = "allow-markup")] pub allow_markup: Option, + + #[clap(short = 'k', long = "cache-file")] pub cache_file: Option, + + #[clap(short = 't', long = "term")] pub term: Option, + #[serde(default = "default_password_char")] + #[clap(short = 'P', long = "password")] pub password: Option, + + #[clap(short = 'e', long = "exec-search")] pub exec_search: Option, + + #[clap(short = 'b', long = "hide-scroll")] pub hide_scroll: Option, - /// Defines how matching is done #[serde(default = "default_match_method")] + #[clap(short = 'M', long = "matching")] pub matching: Option, + + #[clap(short = 'i', long = "insensitive")] pub insensitive: Option, + + #[clap(short = 'q', long = "parse-search")] pub parse_search: Option, + + #[clap(short = 'l', long = "location")] pub location: Option, + + #[clap(short = 'a', long = "no-actions")] pub no_actions: Option, + + #[clap(short = 'L', long = "lines")] pub lines: Option, - /// Defines how many columns are shown per row + #[serde(default = "default_columns")] + #[clap(short = 'w', long = "columns")] pub columns: Option, + + #[clap(short = 'O', long = "sort-order")] pub sort_order: Option, + + #[clap(short = 'G', long = "gtk-dark")] pub gtk_dark: Option, + + #[clap(short = 'Q', long = "search")] pub search: Option, + + #[clap(short = 'o', long = "monitor")] pub monitor: Option, + + #[clap(short = 'r', long = "pre-display-cmd")] pub pre_display_cmd: Option, - /// Defines how the entries root container are ordered - /// Default is vertical + #[serde(default = "default_orientation")] + #[clap(long = "orientation")] pub orientation: Option, - /// Specifies the horizontal align for the entire scrolled area, - /// it can be any of fill, start, end, or center, default is fill. + #[serde(default = "default_halign")] + #[clap(long = "halign")] pub halign: Option, - //// Specifies the horizontal align for the individual entries, - // it can be any of fill, start, end, or center, default is fill. + #[serde(default = "default_content_halign")] + #[clap(long = "content-halign")] pub content_halign: Option, - /// Specifies the vertical align for the entire scrolled area, it can be any of fill, start, e - /// nd, or center, the default is orientation dependent. If vertical then it defaults to - /// start, if horizontal it defaults to center. + #[clap(long = "valign")] pub valign: Option, pub filter_rate: Option, - /// Specifies the image size when enabled. - /// Defaults to 32. + #[serde(default = "default_image_size")] + #[clap(long = "image-size")] pub image_size: Option, + pub key_up: Option, pub key_down: Option, pub key_left: Option, @@ -110,8 +219,10 @@ pub struct Config { pub key_expand: Option, pub key_hide_search: Option, pub key_copy: Option, - #[serde(flatten)] - pub custom_keys: Option>, + + // todo re-add this + // #[serde(flatten)] + // pub key_custom: Option>, pub line_wrap: Option, pub global_coords: Option, pub hide_search: Option, @@ -121,25 +232,39 @@ pub struct Config { pub single_click: Option, pub pre_display_exec: Option, - // Exclusive options - /// Minimum score for the fuzzy finder to accept a match. - /// Must be a value between 0 and 1 - /// Defaults to 0.1. + /// Minimum score for a fuzzy search to be shown #[serde(default = "default_fuzzy_min_score")] + #[clap(long = "fuzzy-min-score")] pub fuzzy_min_score: Option, - /// Defines how the content in the row box is aligned - /// Defaults to vertical + /// Orientation of items in the row box where items are displayed #[serde(default = "default_row_box_orientation")] + #[clap(long = "row-box-orientation")] pub row_bow_orientation: Option, + + /// Set to to true to wrap text after a given amount of chars + #[serde(default = "default_text_wrap")] + #[clap(long = "text-wrap")] + pub text_wrap: Option, + + /// Defines after how many chars a line is broken over. + /// Only cuts at spaces. + #[serde(default = "default_text_wrap_length")] + #[clap(long = "text-wrap-length")] + pub text_wrap_length: Option, + + } impl Default for Config { fn default() -> Self { Config { + fork: None, + config: None, + dmenu: None, + version: None, style: default_style(), show: None, - mode: None, width: default_width(), height: default_height(), prompt: None, @@ -147,7 +272,7 @@ impl Default for Config { x: None, yoffset: None, y: None, - normal_window: None, + normal_window: default_normal_window(), allow_images: None, allow_markup: None, cache_file: None, @@ -186,7 +311,7 @@ impl Default for Config { key_expand: None, key_hide_search: None, key_copy: None, - custom_keys: None, + //key_custom: None, line_wrap: None, global_coords: None, hide_search: None, @@ -197,6 +322,8 @@ impl Default for Config { pre_display_exec: None, fuzzy_min_score: default_fuzzy_min_score(), row_bow_orientation: default_row_box_orientation(), + text_wrap: default_text_wrap(), + text_wrap_length: default_text_wrap_length(), } } } @@ -205,7 +332,7 @@ fn default_row_box_orientation() -> Option { Some(Orientation::Horizontal) } -fn default_orientation() -> Option { +pub(crate) fn default_orientation() -> Option { Some(Orientation::Vertical) } @@ -221,8 +348,8 @@ fn default_columns() -> Option { Some(1) } -fn default_normal_window() -> Option { - Some(false) +fn default_normal_window() -> bool { + false } // TODO @@ -303,7 +430,7 @@ fn default_normal_window() -> Option { // char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default); fn default_style() -> Option { - system::config_path(None) + style_path(None) .ok() .and_then(|pb| Some(pb.display().to_string())) .or_else(|| { @@ -340,7 +467,89 @@ pub fn default_image_size() -> Option { Some(32) } -pub fn merge_config_with_args(config: &mut Config, args: &Args) -> anyhow::Result { +pub fn default_text_wrap_length() -> Option { + Some(15) +} + +pub fn default_text_wrap() -> Option { + Some(false) +} + +pub fn parse_args() -> Config { + Config::parse() +} + + +pub fn style_path(full_path: Option) -> Result { + let alternative_paths = path_alternatives(vec![dirs::config_dir()], PathBuf::from("worf").join("style.css")); + resolve_path( + full_path, + alternative_paths + .into_iter() + .collect(), + ) +} + +pub fn path_alternatives(base_paths: Vec>, sub_path: PathBuf) -> Vec { + base_paths + .into_iter() + .filter_map(|s| s) + .map(|pb| pb.join(&sub_path)) + .filter_map(|pb| pb.canonicalize().ok()) + .filter(|c| c.exists()) + .collect() +} + +pub fn resolve_path( + full_path: Option, + alternatives: Vec, +) -> Result { + full_path + .map(PathBuf::from) + .and_then(|p| p.canonicalize().ok().filter(|c| c.exists())) + .or_else(|| { + alternatives + .into_iter() + .filter(|p| p.exists()) + .find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists())) + }) + .ok_or_else(|| anyhow!("Could not find a valid config file.")) +} + +pub fn load_config(args_opt: Option) -> Result { + let home_dir = env::var("HOME")?; + let config_path = args_opt.as_ref().map(|c| { + c.config + .as_ref() + .and_then(|p| Some(PathBuf::from(p))) + .unwrap_or_else(|| { + env::var("XDG_CONF_HOME") + .map_or( + PathBuf::from(home_dir.clone()).join(".config"), + |xdg_conf_home| PathBuf::from(&xdg_conf_home), + ) + .join("worf") + .join("config") + }) + }); + + match config_path { + Some(path) => { + let toml_content = fs::read_to_string(path)?; + let mut config: Config = toml::from_str(&toml_content)?; + + if let Some(args) = args_opt { + let merge_result = merge_config_with_args(&mut config, &args)?; + Ok(merge_result) + } else { + Ok(config) + } + } + None => Err(anyhow!("No config file found")), + } +} + +pub fn merge_config_with_args(config: &mut Config, args: &Config) -> anyhow::Result { let args_json = serde_json::to_value(args)?; let mut config_json = serde_json::to_value(config)?; diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 1ef39e7..d9d6bcc 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -26,8 +26,8 @@ use log::{debug, error, info}; use std::process::exit; use std::sync::{Arc, Mutex, MutexGuard}; -type ArcMenuMap = Arc>>; -type MenuItemSender = Sender>; +type ArcMenuMap = Arc>>>; +type MenuItemSender = Sender, anyhow::Error>>; impl Into for config::Orientation { fn into(self) -> Orientation { @@ -49,17 +49,20 @@ impl Into for config::Align { } #[derive(Clone)] -pub struct MenuItem { +pub struct MenuItem { pub label: String, // todo support empty label? pub icon_path: Option, pub action: Option, - pub sub_elements: Vec, + pub sub_elements: Vec>, pub working_dir: Option, pub initial_sort_score: i64, pub search_sort_score: f64, + + /// Allows to store arbitrary additional information + pub data: Option, } -pub fn show(config: Config, elements: Vec) -> Result { +pub fn show(config: Config, elements: Vec>) -> Result, anyhow::Error> where T: Clone + 'static { if let Some(ref css) = config.style { let provider = CssProvider::new(); let css_file_path = File::for_path(css); @@ -85,12 +88,12 @@ pub fn show(config: Config, elements: Vec) -> Result( config: &Config, - elements: &Vec, - sender: Sender>, + elements: &Vec>, + sender: Sender, anyhow::Error>>, app: &Application, -) { +) where T: Clone + 'static { // Create a toplevel undecorated window let window = ApplicationWindow::builder() .application(app) @@ -102,15 +105,13 @@ fn build_ui( window.set_widget_name("window"); - config.normal_window.map(|normal| { - if !normal { - // Initialize the window as a layer - window.init_layer_shell(); - window.set_layer(gtk4_layer_shell::Layer::Overlay); - window.set_keyboard_mode(KeyboardMode::Exclusive); - window.set_namespace(Some("worf")); - } - }); + if !config.normal_window { + // Initialize the window as a layer + window.init_layer_shell(); + window.set_layer(gtk4_layer_shell::Layer::Overlay); + window.set_keyboard_mode(KeyboardMode::Exclusive); + window.set_namespace(Some("worf")); + } let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); outer_box.set_widget_name("outer-box"); @@ -159,7 +160,7 @@ fn build_ui( inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_activate_on_single_click(true); - let mut list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); + let mut list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); for entry in elements { list_items .lock() @@ -221,13 +222,13 @@ fn build_ui( }); } -fn setup_key_event_handler( +fn setup_key_event_handler( window: &ApplicationWindow, entry_clone: SearchEntry, inner_box: FlowBox, app: Application, - sender: MenuItemSender, - list_items: Arc>>, + sender: MenuItemSender, + list_items: Arc>>>, config: Config, ) { let key_controller = EventControllerKey::new(); @@ -269,10 +270,10 @@ fn setup_key_event_handler( window.add_controller(key_controller); } -fn sort_menu_items( +fn sort_menu_items( child1: &FlowBoxChild, child2: &FlowBoxChild, - items_lock: &Mutex>, + items_lock: &Mutex>>, ) -> Ordering { let lock = items_lock.lock().unwrap(); let m1 = lock.get(child1); @@ -281,13 +282,13 @@ fn sort_menu_items( match (m1, m2) { (Some(menu1), Some(menu2)) => { if menu1.search_sort_score != 0.0 || menu2.search_sort_score != 0.0 { - if menu1.search_sort_score > menu2.search_sort_score { + if menu1.search_sort_score < menu2.search_sort_score { Ordering::Smaller } else { Ordering::Larger } } else { - if menu1.initial_sort_score > menu2.initial_sort_score { + if menu1.initial_sort_score < menu2.initial_sort_score { Ordering::Smaller } else { Ordering::Larger @@ -300,12 +301,12 @@ fn sort_menu_items( } } -fn handle_selected_item( - sender: &MenuItemSender, +fn handle_selected_item( + sender: &MenuItemSender, app: &Application, inner_box: &FlowBox, - lock_arc: &ArcMenuMap, -) -> Result<(), String> { + lock_arc: &ArcMenuMap, +) -> Result<(), String> where T: Clone { for s in inner_box.selected_children() { let list_items = lock_arc.lock().unwrap(); let item = list_items.get(&s); @@ -320,12 +321,12 @@ fn handle_selected_item( Err("selected item cannot be resolved".to_owned()) } -fn add_menu_item( +fn add_menu_item( inner_box: &FlowBox, - entry_element: &MenuItem, + entry_element: &MenuItem, config: &Config, - sender: MenuItemSender, - lock_arc: ArcMenuMap, + sender: MenuItemSender, + lock_arc: ArcMenuMap, app: Application, ) -> FlowBoxChild { let parent: Widget = if !entry_element.sub_elements.is_empty() { @@ -390,11 +391,11 @@ fn add_menu_item( child } -fn create_menu_row( - menu_item: &MenuItem, +fn create_menu_row( + menu_item: &MenuItem, config: &Config, - lock_arc: ArcMenuMap, - sender: MenuItemSender, + lock_arc: ArcMenuMap, + sender: MenuItemSender, app: Application, inner_box: FlowBox, ) -> Widget { @@ -434,7 +435,13 @@ fn create_menu_row( } // todo make max length configurable - let label = Label::new(Some(&wrap_text(&menu_item.label, 15))); + let text = if config.text_wrap.is_some_and(|x| x == true) { + &wrap_text(&menu_item.label, config.text_wrap_length) + } else { + menu_item.label.as_str() + }; + + let label = Label::new(Some(text)); label.set_hexpand(true); label.set_widget_name("label"); label.set_wrap(true); @@ -448,9 +455,9 @@ fn create_menu_row( row.upcast() } -fn filter_widgets( +fn filter_widgets( query: &str, - items: &mut HashMap, + items: &mut HashMap>, config: &Config, inner_box: &FlowBox, ) { @@ -537,9 +544,9 @@ fn percent_or_absolute(value: &String, base_value: i32) -> Option { } } -pub fn initialize_sort_scores(items: &mut Vec) { +pub fn initialize_sort_scores(items: &mut Vec>) { let mut regular_score = items.len() as i64; - items.sort_by(|l, r| r.label.cmp(&l.label)); + items.sort_by(|l, r| l.label.cmp(&r.label)); for item in items.iter_mut() { if item.initial_sort_score == 0 { @@ -549,12 +556,13 @@ pub fn initialize_sort_scores(items: &mut Vec) { } } -fn wrap_text(text: &str, line_length: usize) -> String { +fn wrap_text(text: &str, line_length: Option) -> String { let mut result = String::new(); let mut line = String::new(); + let len = line_length.unwrap_or(text.len()); for word in text.split_whitespace() { - if line.len() + word.len() + 1 > line_length { + if line.len() + word.len() + 1 > len { if !line.is_empty() { result.push_str(&line.trim_end()); result.push('\n'); diff --git a/src/lib/mod.rs b/src/lib/mod.rs index cc32f75..e02e7e3 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -2,3 +2,4 @@ pub mod config; pub mod desktop; pub mod gui; pub mod system; +pub mod mode; diff --git a/src/lib/mode.rs b/src/lib/mode.rs new file mode 100644 index 0000000..c57b9ba --- /dev/null +++ b/src/lib/mode.rs @@ -0,0 +1,211 @@ +use crate::lib::config::Config; +use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; +use crate::lib::gui; +use crate::lib::gui::MenuItem; +use crate::lookup_name_with_locale; +use anyhow::{Context, anyhow}; +use freedesktop_file_parser::EntryType; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::os::unix::prelude::CommandExt; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::{env, fs, io}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct DRunCache { + desktop_entry: String, + run_count: usize, +} + +pub fn d_run(mut config: Config) -> anyhow::Result<()> { + let locale_variants = get_locale_variants(); + let default_icon = default_icon(); + + let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); + let mut d_run_cache = { + if let Some(ref cache_path) = cache_path { + if let Err(e) = create_file_if_not_exists(cache_path) { + log::warn!("No drun cache file and cannot create: {e:?}"); + } + } + + load_cache_file(&cache_path).unwrap_or_default() + }; + + let mut entries: Vec> = Vec::new(); + for file in find_desktop_files().iter().filter(|f| { + f.entry.hidden.map_or(true, |hidden| !hidden) + && f.entry.no_display.map_or(true, |no_display| !no_display) + }) { + let (action, working_dir) = match &file.entry.entry_type { + EntryType::Application(app) => (app.exec.clone(), app.path.clone()), + _ => (None, None), + }; + + let name = match lookup_name_with_locale( + &locale_variants, + &file.entry.name.variants, + &file.entry.name.default, + ) { + Some(name) => name, + None => { + log::debug!("Skipping desktop entry without name {file:?}"); + continue; + } + }; + + let icon = file + .entry + .icon + .as_ref() + .map(|s| s.content.clone()) + .or(Some(default_icon.clone())); + log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}"); + let sort_score = d_run_cache.get(&name).unwrap_or(&0); + + let mut entry: MenuItem = MenuItem { + label: name, + icon_path: icon.clone(), + action, + sub_elements: Vec::default(), + working_dir: working_dir.clone(), + initial_sort_score: -(*sort_score), + search_sort_score: 0.0, + data: None, + }; + + file.actions.iter().for_each(|(_, action)| { + let action_name = lookup_name_with_locale( + &locale_variants, + &action.name.variants, + &action.name.default, + ); + let action_icon = action + .icon + .as_ref() + .map(|s| s.content.clone()) + .or(icon.as_ref().map(|s| s.clone())); + + log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); + + let sub_entry = MenuItem { + label: action_name.unwrap().trim().to_owned(), + icon_path: action_icon, + action: action.exec.clone(), + sub_elements: Vec::default(), + working_dir: working_dir.clone(), + initial_sort_score: 0, // subitems are never sorted right now. + search_sort_score: 0.0, + data: None, + }; + entry.sub_elements.push(sub_entry); + }); + + entries.push(entry); + } + + gui::initialize_sort_scores(&mut entries); + + // todo ues a arc instead of cloning the config + let selection_result = gui::show(config.clone(), entries.clone()); + match selection_result { + Ok(selected_item) => { + if let Some(cache) = cache_path { + *d_run_cache.entry(selected_item.label).or_insert(0) += 1; + if let Err(e) = save_cache_file(&cache, d_run_cache) { + log::warn!("cannot save drun cache {e:?}"); + } + } + + if let Some(action) = selected_item.action { + spawn_fork(&action, &selected_item.working_dir)? + } + } + Err(e) => { + log::error!("{e}"); + } + } + + Ok(()) +} + +fn save_cache_file(path: &PathBuf, data: HashMap) -> anyhow::Result<()> { + // Convert the HashMap to TOML string + let toml_string = toml::ser::to_string(&data).map_err(|e| anyhow::anyhow!(e))?; + fs::write(path, toml_string).map_err(|e| anyhow::anyhow!(e)) +} + +fn load_cache_file(cache_path: &Option) -> anyhow::Result> { + let path = match cache_path { + Some(p) => p, + None => return Err(anyhow!("Cache is missing")), + }; + + let toml_content = fs::read_to_string(path)?; + let parsed: toml::Value = toml_content.parse().expect("Failed to parse TOML"); + + let mut result: HashMap = HashMap::new(); + if let toml::Value::Table(table) = parsed { + for (key, val) in table { + if let toml::Value::Integer(i) = val { + result.insert(key, i); + } else { + log::warn!("Skipping key '{}' because it's not an integer", key); + } + } + } + Ok(result) +} + +fn create_file_if_not_exists(path: &PathBuf) -> anyhow::Result<()> { + let file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path); + + match file { + Ok(_) => Ok(()), + + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e).context(format!("Failed to create file {}", path.display()))?, + } +} + +fn spawn_fork(cmd: &str, working_dir: &Option) -> anyhow::Result<()> { + // todo probably remove arguments? + // todo support working dir + // todo fix actions + // todo graphical disk map icon not working + // Unix-like systems (Linux, macOS) + + let parts = cmd.split(' ').collect::>(); + if parts.is_empty() { + return Err(anyhow!("empty command passed")); + } + + if let Some(dir) = working_dir { + env::set_current_dir(dir)?; + } + + let exec = parts[0]; + let args: Vec<_> = parts + .iter() + .skip(1) + .filter(|arg| !arg.starts_with("%")) + .collect(); + + unsafe { + let _ = Command::new(exec) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { + libc::setsid(); + Ok(()) + }) + .spawn(); + } + Ok(()) +} diff --git a/src/lib/system.rs b/src/lib/system.rs index a1a4fcf..0d5e58e 100644 --- a/src/lib/system.rs +++ b/src/lib/system.rs @@ -1,31 +1,3 @@ use anyhow::anyhow; use std::env; use std::path::PathBuf; - -pub fn home_dir() -> Result { - env::var("HOME").map_err(|e| anyhow::anyhow!("$HOME not set: {e}")) -} - -pub fn conf_home() -> Result { - env::var("XDG_CONF_HOME").map_err(|e| anyhow::anyhow!("XDG_CONF_HOME not set: {e}")) -} - -pub fn config_path(config_path: Option) -> Result { - config_path - .map(PathBuf::from) - .and_then(|p| p.canonicalize().ok().filter(|c| c.exists())) - .or_else(|| { - [ - conf_home().ok().map(PathBuf::from), - home_dir() - .ok() - .map(PathBuf::from) - .map(|c| c.join(".config")), - ] - .into_iter() - .flatten() - .map(|base| base.join("worf").join("style.css")) - .find_map(|p| p.canonicalize().ok()) - }) - .ok_or_else(|| anyhow!("Could not find a valid config file.")) -} diff --git a/src/main.rs b/src/main.rs index fee407c..fad1824 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ // todo resolve paths like ~/ -use crate::args::{Args, Mode}; -use crate::lib::config::{Config, merge_config_with_args}; +use crate::lib::config::Config; use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; -use crate::lib::gui; +use crate::lib::{config, gui, mode}; use crate::lib::gui::MenuItem; use anyhow::{Error, anyhow}; use clap::Parser; @@ -18,7 +17,6 @@ use gtk4::prelude::{ }; use gtk4_layer_shell::LayerShell; use log::{debug, info, warn}; -use merge::Merge; use std::collections::HashMap; use std::ops::Deref; use std::os::unix::process::CommandExt; @@ -28,9 +26,9 @@ use std::sync::Arc; use std::thread::sleep; use std::{env, fs, time}; -mod args; mod lib; + fn main() -> anyhow::Result<()> { gtk4::init()?; @@ -38,43 +36,23 @@ fn main() -> anyhow::Result<()> { // todo change to error as default .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_owned())) .init(); - let args = Args::parse(); - - let home_dir = env::var("HOME")?; - let config_path = args - .config - .as_ref() - .map(|c| PathBuf::from(c)) - .unwrap_or_else(|| { - env::var("XDG_CONF_HOME") - .map_or( - PathBuf::from(home_dir.clone()).join(".config"), - |xdg_conf_home| PathBuf::from(&xdg_conf_home), - ) - .join("worf") - .join("config") - }); - - let drun_cache = env::var("XDG_CACHE_HOME") - .map_or( - PathBuf::from(home_dir.clone()).join(".cache"), - |xdg_conf_home| PathBuf::from(&xdg_conf_home), - ) - .join("worf-drun"); - let toml_content = fs::read_to_string(config_path)?; - let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly - let config = merge_config_with_args(&mut config, &args)?; + let args = config::parse_args(); + let config = config::load_config(Some(args))?; - match args.mode { - Mode::Run => {} - Mode::Drun => { - drun(config)?; + if let Some(show) = &config.show { + match show { + config::Mode::Run => {} + config::Mode::Drun => { + mode::d_run(config)?; + } + config::Mode::Dmenu => {} } - Mode::Dmenu => {} - } - Ok(()) + Ok(()) + } else { + Err(anyhow!("No mode provided")) + } } fn lookup_name_with_locale( @@ -90,135 +68,7 @@ fn lookup_name_with_locale( .or_else(|| Some(fallback.to_owned())) } -fn drun(mut config: Config) -> anyhow::Result<()> { - let mut entries: Vec = Vec::new(); - let locale_variants = get_locale_variants(); - let default_icon = default_icon(); - - for file in find_desktop_files().iter().filter(|f| { - f.entry.hidden.map_or(true, |hidden| !hidden) - && f.entry.no_display.map_or(true, |no_display| !no_display) - }) { - let (action, working_dir) = match &file.entry.entry_type { - EntryType::Application(app) => (app.exec.clone(), app.path.clone()), - _ => (None, None), - }; - - let name = lookup_name_with_locale( - &locale_variants, - &file.entry.name.variants, - &file.entry.name.default, - ); - if name.is_none() { - debug!("Skipping desktop entry without name {file:?}") - } - - let icon = file - .entry - .icon - .as_ref() - .map(|s| s.content.clone()) - .or(Some(default_icon.clone())); - debug!("file, name={name:?}, icon={icon:?}, action={action:?}"); - let mut sort_score = 0.0; - if name.as_ref().unwrap().contains("ox") { - sort_score = 999.0; - } - - let mut entry = MenuItem { - label: name.unwrap(), - icon_path: icon.clone(), - action, - sub_elements: Vec::default(), - working_dir: working_dir.clone(), - initial_sort_score: 0, - search_sort_score: sort_score, - }; - - file.actions.iter().for_each(|(_, action)| { - let action_name = lookup_name_with_locale( - &locale_variants, - &action.name.variants, - &action.name.default, - ); - let action_icon = action - .icon - .as_ref() - .map(|s| s.content.clone()) - .or(icon.as_ref().map(|s| s.clone())); - - debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); - - let sub_entry = MenuItem { - label: action_name.unwrap().trim().to_owned(), - icon_path: action_icon, - action: action.exec.clone(), - sub_elements: Vec::default(), - working_dir: working_dir.clone(), - initial_sort_score: 0, - search_sort_score: 0.0, - }; - entry.sub_elements.push(sub_entry); - }); - - entries.push(entry); - } - - gui::initialize_sort_scores(&mut entries); - - // todo ues a arc instead of cloning the config - let selection_result = gui::show(config.clone(), entries.clone()); - match selection_result { - Ok(selected_item) => { - if let Some(action) = selected_item.action { - spawn_fork(&action, &selected_item.working_dir)? - } - } - Err(e) => { - log::error!("{e}"); - } - } - - Ok(()) -} - -fn spawn_fork(cmd: &str, working_dir: &Option) -> anyhow::Result<()> { - // todo probably remove arguments? - // todo support working dir - // todo fix actions - // todo graphical disk map icon not working - // Unix-like systems (Linux, macOS) - let parts = cmd.split(' ').collect::>(); - if parts.is_empty() { - return Err(anyhow!("empty command passed")); - } - - if let Some(dir) = working_dir { - env::set_current_dir(dir)?; - } - - let exec = parts[0]; - let args: Vec<_> = parts - .iter() - .skip(1) - .filter(|arg| !arg.starts_with("%")) - .collect(); - - unsafe { - let _ = Command::new(exec) - .args(args) - .stdin(Stdio::null()) // Disconnect stdin - .stdout(Stdio::null()) // Disconnect stdout - .stderr(Stdio::null()) // Disconnect stderr - .pre_exec(|| { - libc::setsid(); - Ok(()) - }) - .spawn(); - } - Ok(()) -} // // fn main() -> anyhow::Result<()> { // env_logger::Builder::new() From c232dc371f0a522f7b2b4fe708c042a6a0089de6 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 19 Apr 2025 00:00:43 +0200 Subject: [PATCH 09/10] fix warnings --- .github/workflows/rust.yml | 20 ++-- Cargo.lock | 16 --- Cargo.toml | 21 +++- src/lib/config.rs | 140 ++++++++++++++++++-------- src/lib/desktop.rs | 183 +++++++++++++++++----------------- src/lib/gui.rs | 199 ++++++++++++++++++------------------- src/lib/mod.rs | 1 - src/lib/mode.rs | 101 +++++++++---------- src/lib/system.rs | 3 - src/main.rs | 56 ++--------- src/mod.rs | 1 - 11 files changed, 377 insertions(+), 364 deletions(-) delete mode 100644 src/lib/system.rs delete mode 100644 src/mod.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6eb63d0..1932245 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,12 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Format - run: cargo fmt --check - - name: Clippy - run: cargo clippy - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Formatting + run: cargo fmt --all -- --check + - name: Clippy warnings + run: cargo clippy -- -D warnings + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test -- --show-output diff --git a/Cargo.lock b/Cargo.lock index b1c3533..8cef9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,12 +277,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "configparser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7" - [[package]] name = "crossbeam" version = "0.8.4" @@ -912,15 +906,6 @@ dependencies = [ "hashbrown 0.15.2", ] -[[package]] -name = "ini" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9271a5dfd4228fa56a78d7508a35c321639cc71f783bb7a5723552add87bce" -dependencies = [ - "configparser", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1686,7 +1671,6 @@ dependencies = [ "gtk4-layer-shell", "home", "hyprland", - "ini", "libc", "log", "regex", diff --git a/Cargo.toml b/Cargo.toml index b3486b4..87f2c10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,24 @@ name = "worf" version = "0.1.0" edition = "2024" +[lints.clippy] +# enable pedantic +pedantic = { level = "warn", priority = -1 } +## exclude some too pedantic lints for now +similar_names = "allow" + +# additional lints +clone_on_ref_ptr = "warn" + +[lib] +name = "worf_lib" +path = "src/lib/mod.rs" + +[[bin]] +name = "worf" +path = "src/main.rs" + + [dependencies] gtk4 = { version = "0.9.5", default-features = true, features = ["v4_6"] } gtk4-layer-shell = "0.5.0" @@ -13,7 +31,6 @@ home = "0.5.11" log = "0.4.27" regex = "1.11.1" hyprland = "0.4.0-beta.2" -ini = "1.3.0" clap = { version = "4.5.35", features = ["derive"] } thiserror = "2.0.12" serde = { version = "1.0.219", features = ["derive"] } @@ -21,6 +38,6 @@ toml = "0.8.20" serde_json = "1.0.140" crossbeam = "0.8.4" libc = "0.2.171" -freedesktop-file-parser = "0.1.0" +freedesktop-file-parser = "0.1.3" strsim = "0.11.1" dirs = "6.0.0" diff --git a/src/lib/config.rs b/src/lib/config.rs index 805d414..7c903a2 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -1,15 +1,11 @@ -use crate::lib::system; -use anyhow::{anyhow, Context}; -use clap::builder::TypedValueParser; -use clap::{Parser, ValueEnum}; -use gtk4::prelude::ToValue; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::env::Args; use std::path::PathBuf; use std::str::FromStr; use std::{env, fs}; + +use anyhow::anyhow; +use clap::{Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; use thiserror::Error; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] @@ -34,10 +30,10 @@ pub enum Align { #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Mode { - /// searches $PATH for executables and allows them to be run by selecting them. + /// searches `$PATH` for executables and allows them to be run by selecting them. Run, - /// searches $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/applications f - /// or desktop files and allows them to be run by selecting them. + /// searches `$XDG_DATA_HOME/applications` and `$XDG_DATA_DIRS/applications` + /// for desktop files and allows them to be run by selecting them. Drun, /// reads from stdin and displays options which when selected will be output to stdout. @@ -85,8 +81,8 @@ pub struct Config { pub version: Option, /// Defines the style sheet to be loaded. - /// Defaults to $XDG_CONF_DIR/worf/style.css - /// or $HOME/.config/worf/style.css if XDG_CONF_DIR is not set. + /// Defaults to `$XDG_CONF_DIR/worf/style.css` + /// or `$HOME/.config/worf/style.css` if `$XDG_CONF_DIR` is not set. #[serde(default = "default_style")] #[clap(long = "style")] pub style: Option, @@ -252,8 +248,6 @@ pub struct Config { #[serde(default = "default_text_wrap_length")] #[clap(long = "text-wrap-length")] pub text_wrap_length: Option, - - } impl Default for Config { @@ -327,28 +321,45 @@ impl Default for Config { } } } - -fn default_row_box_orientation() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_row_box_orientation() -> Option { Some(Orientation::Horizontal) } -pub(crate) fn default_orientation() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_orientation() -> Option { Some(Orientation::Vertical) } -fn default_halign() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_halign() -> Option { Some(Align::Fill) } -fn default_content_halign() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_content_halign() -> Option { Some(Align::Fill) } -fn default_columns() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_columns() -> Option { Some(1) } -fn default_normal_window() -> bool { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_normal_window() -> bool { false } @@ -429,77 +440,112 @@ fn default_normal_window() -> bool { // key_default = "Ctrl-c"; // char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default); -fn default_style() -> Option { +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_style() -> Option { style_path(None) .ok() - .and_then(|pb| Some(pb.display().to_string())) + .map(|pb| pb.display().to_string()) .or_else(|| { log::error!("no stylesheet found, using system styles"); None }) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_height() -> Option { Some("40%".to_owned()) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_width() -> Option { Some("50%".to_owned()) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_password_char() -> Option { Some("*".to_owned()) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_fuzzy_min_length() -> Option { Some(10) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_fuzzy_min_score() -> Option { Some(0.1) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_match_method() -> Option { Some(MatchMethod::Contains) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_image_size() -> Option { Some(32) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_text_wrap_length() -> Option { Some(15) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] pub fn default_text_wrap() -> Option { Some(false) } +#[must_use] pub fn parse_args() -> Config { Config::parse() } - +/// # Errors +/// +/// Will return Err when it cannot resolve any path or no style is found pub fn style_path(full_path: Option) -> Result { - let alternative_paths = path_alternatives(vec![dirs::config_dir()], PathBuf::from("worf").join("style.css")); - resolve_path( - full_path, - alternative_paths - .into_iter() - .collect(), - ) + let alternative_paths = path_alternatives( + vec![dirs::config_dir()], + &PathBuf::from("worf").join("style.css"), + ); + resolve_path(full_path, alternative_paths.into_iter().collect()) } -pub fn path_alternatives(base_paths: Vec>, sub_path: PathBuf) -> Vec { +#[must_use] +pub fn path_alternatives(base_paths: Vec>, sub_path: &PathBuf) -> Vec { base_paths .into_iter() - .filter_map(|s| s) - .map(|pb| pb.join(&sub_path)) + .flatten() + .map(|pb| pb.join(sub_path)) .filter_map(|pb| pb.canonicalize().ok()) .filter(|c| c.exists()) .collect() } +/// # Errors +/// +/// Will return `Err` if it is not able to find any valid path pub fn resolve_path( full_path: Option, alternatives: Vec, @@ -513,16 +559,21 @@ pub fn resolve_path( .filter(|p| p.exists()) .find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists())) }) - .ok_or_else(|| anyhow!("Could not find a valid config file.")) + .ok_or_else(|| anyhow!("Could not find a valid file.")) } +/// # Errors +/// +/// Will return Err when it +/// * cannot read the config file +/// * cannot parse the config file +/// * no config file exists +/// * config file and args cannot be merged pub fn load_config(args_opt: Option) -> Result { let home_dir = env::var("HOME")?; let config_path = args_opt.as_ref().map(|c| { - c.config - .as_ref() - .and_then(|p| Some(PathBuf::from(p))) - .unwrap_or_else(|| { + c.config.as_ref().map_or_else( + || { env::var("XDG_CONF_HOME") .map_or( PathBuf::from(home_dir.clone()).join(".config"), @@ -530,7 +581,9 @@ pub fn load_config(args_opt: Option) -> Result { ) .join("worf") .join("config") - }) + }, + PathBuf::from, + ) }); match config_path { @@ -549,6 +602,9 @@ pub fn load_config(args_opt: Option) -> Result { } } +/// # Errors +/// +/// Will return Err when it fails to merge the config with the arguments. pub fn merge_config_with_args(config: &mut Config, args: &Config) -> anyhow::Result { let args_json = serde_json::to_value(args)?; let mut config_json = serde_json::to_value(config)?; diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index dae2a79..62b947b 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -1,8 +1,8 @@ +use anyhow::anyhow; use freedesktop_file_parser::DesktopFile; use gtk4::prelude::*; use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use home::home_dir; -use ini::configparser::ini::Ini; use log::{debug, info, warn}; use regex::Regex; use std::collections::HashMap; @@ -14,13 +14,21 @@ pub struct IconResolver { cache: HashMap, } +impl Default for IconResolver { + #[must_use] + fn default() -> IconResolver { + Self::new() + } +} + impl IconResolver { - #![allow(clippy::single_call_fn)] + #[must_use] pub fn new() -> IconResolver { IconResolver { cache: HashMap::new(), } } + pub fn icon_path(&mut self, icon_name: &str) -> String { if let Some(icon_path) = self.cache.get(icon_name) { info!("Fetching {icon_name} from cache"); @@ -28,47 +36,55 @@ impl IconResolver { } info!("Loading icon for {icon_name}"); - let icon = fetch_icon_from_theme(icon_name) - .or_else(|| fetch_icon_from_common_dirs(icon_name)) - .or_else(|| fetch_icon_from_desktop_file(icon_name)) - .unwrap_or_else(|| { + .or_else(|_| { + fetch_icon_from_common_dirs(icon_name).map_or_else( + || Err(anyhow::anyhow!("Missing file")), // Return an error here + Ok, + ) + }) + .or_else(|_| { warn!("Missing icon for {icon_name}, using fallback"); default_icon() }); - self.cache.insert(icon_name.to_owned(), icon.clone()); - self.cache.get(icon_name).unwrap().to_owned() + self.cache + .entry(icon_name.to_owned()) + .or_insert_with(|| icon.unwrap_or_default()) + .to_owned() } } -pub fn default_icon() -> String { - fetch_icon_from_theme("image-missing").unwrap() +/// # Errors +/// +/// Will return `Err` if no icon can be found +pub fn default_icon() -> anyhow::Result { + fetch_icon_from_theme("image-missing") } -fn fetch_icon_from_desktop_file(icon_name: &str) -> Option { - // find_desktop_files().into_iter().find_map(|desktop_file| { - // desktop_file - // .get("Desktop Entry") - // .filter(|desktop_entry| { - // desktop_entry - // .get("Exec") - // .and_then(|opt| opt.as_ref()) - // .is_some_and(|exec| exec.to_lowercase().contains(icon_name)) - // }) - // .map(|desktop_entry| { - // desktop_entry - // .get("Icon") - // .and_then(|opt| opt.as_ref()) - // .map(ToOwned::to_owned) - // .unwrap_or_default() - // }) - // }) - //todo - None -} - -fn fetch_icon_from_theme(icon_name: &str) -> Option { +// fn fetch_icon_from_desktop_file(icon_name: &str) -> Option { +// // find_desktop_files().into_iter().find_map(|desktop_file| { +// // desktop_file +// // .get("Desktop Entry") +// // .filter(|desktop_entry| { +// // desktop_entry +// // .get("Exec") +// // .and_then(|opt| opt.as_ref()) +// // .is_some_and(|exec| exec.to_lowercase().contains(icon_name)) +// // }) +// // .map(|desktop_entry| { +// // desktop_entry +// // .get("Icon") +// // .and_then(|opt| opt.as_ref()) +// // .map(ToOwned::to_owned) +// // .unwrap_or_default() +// // }) +// // }) +// //todo +// None +// } + +fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result { let display = gtk4::gdk::Display::default(); if display.is_none() { log::error!("Failed to get display"); @@ -85,9 +101,14 @@ fn fetch_icon_from_theme(icon_name: &str) -> Option { IconLookupFlags::empty(), ); - icon.file() + match icon + .file() .and_then(|file| file.path()) .and_then(|path| path.to_str().map(string::ToString::to_string)) + { + None => Err(anyhow!("Cannot find file")), + Some(i) => Ok(i), + } } fn fetch_icon_from_common_dirs(icon_name: &str) -> Option { @@ -125,15 +146,17 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option Vec { +/// # Errors +/// +/// Will return Err when it cannot parse the internal regex +pub fn find_desktop_files() -> anyhow::Result> { let mut paths = vec![ PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/local/share/applications"), @@ -144,24 +167,33 @@ pub(crate) fn find_desktop_files() -> Vec { paths.push(home.join(".local/share/applications")); } + if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") { + paths.push(PathBuf::from(xdg_data_home).join(".applications")); + } + + if let Ok(xdg_data_dir) = env::var("XDG_DATA_DIRS") { + paths.push(PathBuf::from(xdg_data_dir).join(".applications")); + } + + let regex = &Regex::new("(?i).*\\.desktop$")?; + let p: Vec<_> = paths .into_iter() - .filter(|icon_dir| icon_dir.exists()) - .filter_map(|icon_dir| { - find_file_case_insensitive(&icon_dir, &Regex::new("(?i).*\\.desktop$").unwrap()) - }) + .filter(|desktop_dir| desktop_dir.exists()) + .filter_map(|icon_dir| find_file_case_insensitive(&icon_dir, regex)) .flat_map(|desktop_files| { desktop_files.into_iter().filter_map(|desktop_file| { - debug!("loading desktop file {:?}", desktop_file); + debug!("loading desktop file {desktop_file:?}"); fs::read_to_string(desktop_file) .ok() .and_then(|content| freedesktop_file_parser::parse(&content).ok()) }) }) .collect(); - p + Ok(p) } +#[must_use] pub fn get_locale_variants() -> Vec { let locale = env::var("LC_ALL") .or_else(|_| env::var("LC_MESSAGES")) @@ -172,7 +204,7 @@ pub fn get_locale_variants() -> Vec { let mut variants = vec![]; if let Some((lang_part, region)) = lang.split_once('_') { - variants.push(format!("{}_{region}", lang_part)); // en_us + variants.push(format!("{lang_part}_{region}")); // en_us variants.push(lang_part.to_string()); // en } else { variants.push(lang.clone()); // e.g. "fr" @@ -181,52 +213,17 @@ pub fn get_locale_variants() -> Vec { variants } -pub fn extract_desktop_fields( - category: &str, - //keys: Vec, - desktop_map: &HashMap>>, -) -> HashMap { - let mut result: HashMap = HashMap::new(); - let category_map = desktop_map.get(category); - if category_map.is_none() { - debug!("No desktop map for category {category}, map data: {desktop_map:?}"); - return result; - } - - let keys_needed = ["name", "exec", "icon"]; - let locale_variants = get_locale_variants(); - - for (map_key, map_value) in category_map.unwrap() { - for key in keys_needed { - if result.contains_key(key) || map_value.is_none() { - continue; - } - - let (k, v) = locale_variants - .iter() - .find(|locale| { - let localized_key = format!("{}[{}]", key, locale); - key == localized_key - }) - .map(|_| (Some(key), map_value)) - .unwrap_or_else(|| { - if key == map_key { - (Some(key), map_value) - } else { - (None, &None) - } - }); - if let Some(k) = k { - if let Some(v) = v { - result.insert(k.to_owned(), v.clone()); - } - } - } - - if result.len() == keys_needed.len() { - break; - } - } - - result +// implicit hasher does not make sense here, it is only for desktop files +#[allow(clippy::implicit_hasher)] +#[must_use] +pub fn lookup_name_with_locale( + locale_variants: &[String], + variants: &HashMap, + fallback: &str, +) -> Option { + locale_variants + .iter() + .find_map(|local| variants.get(local)) + .map(std::borrow::ToOwned::to_owned) + .or_else(|| Some(fallback.to_owned())) } diff --git a/src/lib/gui.rs b/src/lib/gui.rs index d9d6bcc..e48b119 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -1,46 +1,43 @@ -use crate::lib::config; -use crate::lib::config::{Config, MatchMethod}; -use anyhow::{Context, anyhow}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use anyhow::anyhow; use crossbeam::channel; use crossbeam::channel::Sender; -use gdk4::gio::{File, Menu}; -use gdk4::glib::{GString, Propagation, Unichar}; -use gdk4::prelude::{Cast, DisplayExt, ListModelExtManual, MonitorExt}; -use gdk4::{pango, Display, Key}; +use gdk4::gio::File; +use gdk4::glib::Propagation; +use gdk4::prelude::{Cast, DisplayExt, MonitorExt}; +use gdk4::{Display, Key}; use gtk4::prelude::{ - ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, - FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, - WidgetExt, + ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt, + GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt, }; use gtk4::{ - Align, Entry, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, - ListBox, ListBoxRow, Ordering, PolicyType, Revealer, ScrolledWindow, SearchEntry, Widget, gdk, + Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, + ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk, }; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4_layer_shell::{KeyboardMode, LayerShell}; -use hyprland::ctl::output::create; -use hyprland::ctl::plugin::list; -use std::collections::HashMap; +use log; -use log::{debug, error, info}; -use std::process::exit; -use std::sync::{Arc, Mutex, MutexGuard}; +use crate::config; +use crate::config::{Config, MatchMethod}; type ArcMenuMap = Arc>>>; type MenuItemSender = Sender, anyhow::Error>>; -impl Into for config::Orientation { - fn into(self) -> Orientation { - match self { +impl From for Orientation { + fn from(orientation: config::Orientation) -> Self { + match orientation { config::Orientation::Vertical => Orientation::Vertical, config::Orientation::Horizontal => Orientation::Horizontal, } } } -impl Into for config::Align { - fn into(self) -> Align { - match self { +impl From for Align { + fn from(align: config::Align) -> Self { + match align { config::Align::Fill => Align::Fill, config::Align::Start => Align::Start, config::Align::Center => Align::Center, @@ -62,39 +59,46 @@ pub struct MenuItem { pub data: Option, } -pub fn show(config: Config, elements: Vec>) -> Result, anyhow::Error> where T: Clone + 'static { +/// # Errors +/// +/// Will return Err when the channel between the UI and this is broken +pub fn show(config: Config, elements: Vec>) -> Result, anyhow::Error> +where + T: Clone + 'static, +{ if let Some(ref css) = config.style { let provider = CssProvider::new(); let css_file_path = File::for_path(css); provider.load_from_file(&css_file_path); - let display = Display::default().expect("Could not connect to a display"); - gtk4::style_context_add_provider_for_display( - &display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + if let Some(display) = Display::default() { + gtk4::style_context_add_provider_for_display( + &display, + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } } let app = Application::builder().application_id("worf").build(); let (sender, receiver) = channel::bounded(1); app.connect_activate(move |app| { - build_ui(&config, &elements, sender.clone(), app); + build_ui(&config, &elements, &sender, app); }); let gtk_args: [&str; 0] = []; app.run_with_args(>k_args); - let selection = receiver.recv()?; - selection + receiver.recv()? } fn build_ui( config: &Config, elements: &Vec>, - sender: Sender, anyhow::Error>>, + sender: &Sender, anyhow::Error>>, app: &Application, -) where T: Clone + 'static { - // Create a toplevel undecorated window +) where + T: Clone + 'static, +{ let window = ApplicationWindow::builder() .application(app) .decorated(false) @@ -130,8 +134,7 @@ fn build_ui( scroll.set_hexpand(true); scroll.set_vexpand(true); - let hide_scroll = false; // todo - if hide_scroll { + if config.hide_scroll.is_some_and(|hs| hs) { scroll.set_policy(PolicyType::External, PolicyType::External); } @@ -148,37 +151,28 @@ fn build_ui( if let Some(valign) = config.valign { inner_box.set_valign(valign.into()); + } else if config.orientation.unwrap() == config::Orientation::Horizontal { + inner_box.set_valign(Align::Center); } else { - if config.orientation.unwrap() == config::Orientation::Horizontal { - inner_box.set_valign(Align::Center); - } else { - inner_box.set_valign(Align::Start); - } + inner_box.set_valign(Align::Start); } inner_box.set_selection_mode(gtk4::SelectionMode::Browse); inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_activate_on_single_click(true); - let mut list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); + let list_items: ArcMenuMap = Arc::new(Mutex::new(HashMap::new())); for entry in elements { list_items .lock() .unwrap() // panic here ok? deadlock? .insert( - add_menu_item( - &inner_box, - &entry, - &config, - sender.clone(), - list_items.clone(), - app.clone(), - ), + add_menu_item(&inner_box, entry, config, sender, &list_items, app), entry.clone(), ); } - let items_clone = list_items.clone(); + let items_clone = Arc::>>>::clone(&list_items); inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_clone)); // Set focus after everything is realized @@ -197,29 +191,28 @@ fn build_ui( inner_box, app.clone(), sender.clone(), - list_items.clone(), + Arc::>>>::clone(&list_items), config.clone(), ); window.show(); let display = window.display(); - window.surface().map(|surface| { + if let Some(surface) = window.surface() { // todo this does not work for multi monitor systems let monitor = display.monitor_at_surface(&surface); if let Some(monitor) = monitor { let geometry = monitor.geometry(); config.width.as_ref().map(|width| { - percent_or_absolute(&width, geometry.width()).map(|w| window.set_width_request(w)) + percent_or_absolute(width, geometry.width()).map(|w| window.set_width_request(w)) }); config.height.as_ref().map(|height| { - percent_or_absolute(&height, geometry.height()) - .map(|h| window.set_height_request(h)) + percent_or_absolute(height, geometry.height()).map(|h| window.set_height_request(h)) }); } else { log::error!("failed to get monitor to init window size"); } - }); + } } fn setup_key_event_handler( @@ -287,12 +280,10 @@ fn sort_menu_items( } else { Ordering::Larger } + } else if menu1.initial_sort_score < menu2.initial_sort_score { + Ordering::Smaller } else { - if menu1.initial_sort_score < menu2.initial_sort_score { - Ordering::Smaller - } else { - Ordering::Larger - } + Ordering::Larger } } (Some(_), None) => Ordering::Larger, @@ -306,8 +297,11 @@ fn handle_selected_item( app: &Application, inner_box: &FlowBox, lock_arc: &ArcMenuMap, -) -> Result<(), String> where T: Clone { - for s in inner_box.selected_children() { +) -> Result<(), String> +where + T: Clone, +{ + if let Some(s) = inner_box.selected_children().into_iter().next() { let list_items = lock_arc.lock().unwrap(); let item = list_items.get(&s); if let Some(item) = item { @@ -325,19 +319,30 @@ fn add_menu_item( inner_box: &FlowBox, entry_element: &MenuItem, config: &Config, - sender: MenuItemSender, - lock_arc: ArcMenuMap, - app: Application, + sender: &MenuItemSender, + lock_arc: &ArcMenuMap, + app: &Application, ) -> FlowBoxChild { - let parent: Widget = if !entry_element.sub_elements.is_empty() { + let parent: Widget = if entry_element.sub_elements.is_empty() { + create_menu_row( + entry_element, + config, + Arc::>>>::clone(lock_arc), + sender.clone(), + app.clone(), + inner_box.clone(), + ) + .upcast() + } else { let expander = Expander::new(None); expander.set_widget_name("expander-box"); expander.set_hexpand(true); + // todo deduplicate this snippet let menu_row = create_menu_row( entry_element, config, - lock_arc.clone(), + Arc::>>>::clone(lock_arc), sender.clone(), app.clone(), inner_box.clone(), @@ -352,7 +357,7 @@ fn add_menu_item( let sub_row = create_menu_row( sub_item, config, - lock_arc.clone(), + Arc::>>>::clone(lock_arc), sender.clone(), app.clone(), inner_box.clone(), @@ -365,16 +370,6 @@ fn add_menu_item( expander.set_child(Some(&list_box)); expander.upcast() - } else { - create_menu_row( - entry_element, - config, - lock_arc.clone(), - sender.clone(), - app.clone(), - inner_box.clone(), - ) - .upcast() }; parent.set_halign(Align::Fill); @@ -435,7 +430,7 @@ fn create_menu_row( } // todo make max length configurable - let text = if config.text_wrap.is_some_and(|x| x == true) { + let text = if config.text_wrap.is_some_and(|x| x) { &wrap_text(&menu_item.label, config.text_wrap_length) } else { menu_item.label.as_str() @@ -462,9 +457,10 @@ fn filter_widgets( inner_box: &FlowBox, ) { if items.is_empty() { - items.iter().for_each(|(child, _)| { + for (child, _) in items.iter() { child.set_visible(true); - }); + } + if let Some(child) = inner_box.first_child() { child.grab_focus(); let fb = child.downcast::(); @@ -476,9 +472,8 @@ fn filter_widgets( } let query = query.to_owned().to_lowercase(); - let mut highest_score = -1.0; let mut fb: Option<&FlowBoxChild> = None; - items.iter_mut().for_each(|(flowbox_child, mut menu_item)| { + for (flowbox_child, menu_item) in items.iter_mut() { let menu_item_search = format!( "{} {}", menu_item @@ -519,12 +514,11 @@ fn filter_widgets( menu_item.search_sort_score = search_sort_score; if visible { - highest_score = search_sort_score; fb = Some(flowbox_child); } flowbox_child.set_visible(visible); - }); + } if let Some(top_item) = fb { inner_box.select_child(top_item); @@ -532,9 +526,12 @@ fn filter_widgets( } } -fn percent_or_absolute(value: &String, base_value: i32) -> Option { - if value.contains("%") { - let value = value.replace("%", "").trim().to_string(); +// allowed because truncating is fine, we do no need the precision +#[allow(clippy::cast_possible_truncation)] +#[allow(clippy::cast_precision_loss)] +fn percent_or_absolute(value: &str, base_value: i32) -> Option { + if value.contains('%') { + let value = value.replace('%', "").trim().to_string(); match value.parse::() { Ok(n) => Some(((n as f32 / 100.0) * base_value as f32) as i32), Err(_) => None, @@ -544,7 +541,9 @@ fn percent_or_absolute(value: &String, base_value: i32) -> Option { } } -pub fn initialize_sort_scores(items: &mut Vec>) { +// highly unlikely that we are dealing with > i64 items +#[allow(clippy::cast_possible_wrap)] +pub fn initialize_sort_scores(items: &mut [MenuItem]) { let mut regular_score = items.len() as i64; items.sort_by(|l, r| l.label.cmp(&r.label)); @@ -562,19 +561,17 @@ fn wrap_text(text: &str, line_length: Option) -> String { let len = line_length.unwrap_or(text.len()); for word in text.split_whitespace() { - if line.len() + word.len() + 1 > len { - if !line.is_empty() { - result.push_str(&line.trim_end()); - result.push('\n'); - line.clear(); - } + if line.len() + word.len() + 1 > len && !line.is_empty() { + result.push_str(line.trim_end()); + result.push('\n'); + line.clear(); } line.push_str(word); line.push(' '); } if !line.is_empty() { - result.push_str(&line.trim_end()); + result.push_str(line.trim_end()); } result diff --git a/src/lib/mod.rs b/src/lib/mod.rs index e02e7e3..6b1769e 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,5 +1,4 @@ pub mod config; pub mod desktop; pub mod gui; -pub mod system; pub mod mode; diff --git a/src/lib/mode.rs b/src/lib/mode.rs index c57b9ba..798f50e 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,8 +1,9 @@ -use crate::lib::config::Config; -use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; -use crate::lib::gui; -use crate::lib::gui::MenuItem; -use crate::lookup_name_with_locale; +use crate::config::Config; +use crate::desktop::{ + default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale, +}; +use crate::gui; +use crate::gui::MenuItem; use anyhow::{Context, anyhow}; use freedesktop_file_parser::EntryType; use serde::{Deserialize, Serialize}; @@ -18,9 +19,12 @@ struct DRunCache { run_count: usize, } -pub fn d_run(mut config: Config) -> anyhow::Result<()> { +/// # Errors +/// +/// Will return `Err` if it was not able to spawn the process +pub fn d_run(config: &Config) -> anyhow::Result<()> { let locale_variants = get_locale_variants(); - let default_icon = default_icon(); + let default_icon = default_icon().unwrap_or_default(); let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); let mut d_run_cache = { @@ -30,29 +34,26 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> { } } - load_cache_file(&cache_path).unwrap_or_default() + load_cache_file(cache_path.as_ref()).unwrap_or_default() }; let mut entries: Vec> = Vec::new(); - for file in find_desktop_files().iter().filter(|f| { - f.entry.hidden.map_or(true, |hidden| !hidden) - && f.entry.no_display.map_or(true, |no_display| !no_display) + for file in find_desktop_files().ok().iter().flatten().filter(|f| { + f.entry.hidden.is_none_or(|hidden| !hidden) + && f.entry.no_display.is_none_or(|no_display| !no_display) }) { let (action, working_dir) = match &file.entry.entry_type { EntryType::Application(app) => (app.exec.clone(), app.path.clone()), _ => (None, None), }; - let name = match lookup_name_with_locale( + let Some(name) = lookup_name_with_locale( &locale_variants, &file.entry.name.variants, &file.entry.name.default, - ) { - Some(name) => name, - None => { - log::debug!("Skipping desktop entry without name {file:?}"); - continue; - } + ) else { + log::debug!("Skipping desktop entry without name {file:?}"); + continue; }; let icon = file @@ -76,30 +77,31 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> { }; file.actions.iter().for_each(|(_, action)| { - let action_name = lookup_name_with_locale( + if let Some(action_name) = lookup_name_with_locale( &locale_variants, &action.name.variants, &action.name.default, - ); - let action_icon = action - .icon - .as_ref() - .map(|s| s.content.clone()) - .or(icon.as_ref().map(|s| s.clone())); - - log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); - - let sub_entry = MenuItem { - label: action_name.unwrap().trim().to_owned(), - icon_path: action_icon, - action: action.exec.clone(), - sub_elements: Vec::default(), - working_dir: working_dir.clone(), - initial_sort_score: 0, // subitems are never sorted right now. - search_sort_score: 0.0, - data: None, - }; - entry.sub_elements.push(sub_entry); + ) { + let action_icon = action + .icon + .as_ref() + .map(|s| s.content.clone()) + .or(icon.clone()); + + log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); + + let sub_entry = MenuItem { + label: action_name, + icon_path: action_icon, + action: action.exec.clone(), + sub_elements: Vec::default(), + working_dir: working_dir.clone(), + initial_sort_score: 0, // subitems are never sorted right now. + search_sort_score: 0.0, + data: None, + }; + entry.sub_elements.push(sub_entry); + } }); entries.push(entry); @@ -113,13 +115,13 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> { Ok(selected_item) => { if let Some(cache) = cache_path { *d_run_cache.entry(selected_item.label).or_insert(0) += 1; - if let Err(e) = save_cache_file(&cache, d_run_cache) { + if let Err(e) = save_cache_file(&cache, &d_run_cache) { log::warn!("cannot save drun cache {e:?}"); } } if let Some(action) = selected_item.action { - spawn_fork(&action, &selected_item.working_dir)? + spawn_fork(&action, selected_item.working_dir.as_ref())?; } } Err(e) => { @@ -130,16 +132,15 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> { Ok(()) } -fn save_cache_file(path: &PathBuf, data: HashMap) -> anyhow::Result<()> { +fn save_cache_file(path: &PathBuf, data: &HashMap) -> anyhow::Result<()> { // Convert the HashMap to TOML string let toml_string = toml::ser::to_string(&data).map_err(|e| anyhow::anyhow!(e))?; fs::write(path, toml_string).map_err(|e| anyhow::anyhow!(e)) } -fn load_cache_file(cache_path: &Option) -> anyhow::Result> { - let path = match cache_path { - Some(p) => p, - None => return Err(anyhow!("Cache is missing")), +fn load_cache_file(cache_path: Option<&PathBuf>) -> anyhow::Result> { + let Some(path) = cache_path else { + return Err(anyhow!("Cache is missing")); }; let toml_content = fs::read_to_string(path)?; @@ -151,7 +152,7 @@ fn load_cache_file(cache_path: &Option) -> anyhow::Result anyhow::Result<()> { let file = fs::OpenOptions::new() .write(true) .create_new(true) - .open(&path); + .open(path); match file { Ok(_) => Ok(()), @@ -172,7 +173,7 @@ fn create_file_if_not_exists(path: &PathBuf) -> anyhow::Result<()> { } } -fn spawn_fork(cmd: &str, working_dir: &Option) -> anyhow::Result<()> { +fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> { // todo probably remove arguments? // todo support working dir // todo fix actions @@ -192,7 +193,7 @@ fn spawn_fork(cmd: &str, working_dir: &Option) -> anyhow::Result<()> { let args: Vec<_> = parts .iter() .skip(1) - .filter(|arg| !arg.starts_with("%")) + .filter(|arg| !arg.starts_with('%')) .collect(); unsafe { diff --git a/src/lib/system.rs b/src/lib/system.rs deleted file mode 100644 index 0d5e58e..0000000 --- a/src/lib/system.rs +++ /dev/null @@ -1,3 +0,0 @@ -use anyhow::anyhow; -use std::env; -use std::path::PathBuf; diff --git a/src/main.rs b/src/main.rs index fad1824..418f3f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,7 @@ -#![warn(clippy::pedantic)] -#![allow(clippy::implicit_return)] - -// todo resolve paths like ~/ - -use crate::lib::config::Config; -use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; -use crate::lib::{config, gui, mode}; -use crate::lib::gui::MenuItem; -use anyhow::{Error, anyhow}; -use clap::Parser; -use freedesktop_file_parser::{DesktopAction, EntryType}; -use gdk4::prelude::Cast; -use gtk4::prelude::{ - ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, - FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt, -}; -use gtk4_layer_shell::LayerShell; -use log::{debug, info, warn}; -use std::collections::HashMap; -use std::ops::Deref; -use std::os::unix::process::CommandExt; -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use std::sync::Arc; -use std::thread::sleep; -use std::{env, fs, time}; - -mod lib; +use std::env; +use anyhow::anyhow; +use worf_lib::{config, mode}; fn main() -> anyhow::Result<()> { gtk4::init()?; @@ -42,11 +16,15 @@ fn main() -> anyhow::Result<()> { if let Some(show) = &config.show { match show { - config::Mode::Run => {} + config::Mode::Run => { + todo!("run not implemented") + } config::Mode::Drun => { - mode::d_run(config)?; + mode::d_run(&config)?; + } + config::Mode::Dmenu => { + todo!("dmenu not implemented") } - config::Mode::Dmenu => {} } Ok(()) @@ -55,20 +33,6 @@ fn main() -> anyhow::Result<()> { } } -fn lookup_name_with_locale( - locale_variants: &Vec, - variants: &HashMap, - fallback: &str, -) -> Option { - locale_variants - .iter() - .filter_map(|local| variants.get(local)) - .next() - .map(|name| name.to_owned()) - .or_else(|| Some(fallback.to_owned())) -} - - // // fn main() -> anyhow::Result<()> { // env_logger::Builder::new() diff --git a/src/mod.rs b/src/mod.rs deleted file mode 100644 index 6e10f4a..0000000 --- a/src/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod args; From 77a25f9512cb85964cdeb2e4f9bbd524db9abd8e Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 19 Apr 2025 01:19:54 +0200 Subject: [PATCH 10/10] fix ci --- .github/workflows/rust.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1932245..1bfa29b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,6 +18,24 @@ jobs: - uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Install ubuntu dependcies + run: | + sudo apt update + sudo apt install -y librust-gdk4-sys-dev \ + libglib2.0-dev libgtk-layer-shell-dev libgtk-layer-shell0 gir1.2-gtklayershell-0.1 \ + libgtk-4-dev gobject-introspection libgirepository1.0-dev gtk-doc-tools python3 valac \ + git cmake gcc meson ninja-build + + - name: Install gt4k layer shell + run: | + git clone https://github.com/wmww/gtk4-layer-shell + cd gtk4-layer-shell + meson setup -Dexamples=true -Ddocs=true -Dtests=true build + ninja -C build + sudo ninja -C build install + sudo ldconfig + - name: Formatting run: cargo fmt --all -- --check - name: Clippy warnings