From 2453bbaf72778bf889927e6a1f79a9d7413c27bf Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 21 Apr 2025 19:30:07 +0200 Subject: [PATCH] add support for location --- README.md | 14 ++++--- examples/dmenu.sh | 12 ++++++ src/lib/config.rs | 38 +++++++++++++---- src/lib/desktop.rs | 60 ++++----------------------- src/lib/gui.rs | 39 +++++++++++++----- src/lib/mode.rs | 100 ++++++++++++++++++++++++++------------------- src/main.rs | 8 ++-- 7 files changed, 151 insertions(+), 120 deletions(-) create mode 100755 examples/dmenu.sh diff --git a/README.md b/README.md index f3a58f7..d19c523 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Allow blur for Worf layerrule = blur, worf ``` -## Additional functionality compared to Wofi (planed) +## Additional functionality compared to Wofi * 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 @@ -32,6 +32,12 @@ layerrule = blur, worf * `label`: Allows styling the label * `row`: Allows styling to row, mainly used to disable hover effects +## Library + +The launcher and UI can be used to build any launcher, as the ui, config and run logic is available as a separate crate. +This library is not available publicly yet as the interface is not stable enough. + + ## Breaking changes to Wofi * 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, @@ -39,6 +45,7 @@ layerrule = blur, worf * 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 * `line_wrap` is now called `line-wrap` +* Wofi has a C-API, that is not and won't be supported. ## Dropped arguments * `mode`, use show @@ -48,8 +55,3 @@ layerrule = blur, worf ### Dropped configuration options * stylesheet -> use style instead * color / colors -> GTK4 does not support color files - - - -## Not supported -* Wofi has a C-API, that is not and won't be supported. diff --git a/examples/dmenu.sh b/examples/dmenu.sh new file mode 100755 index 0000000..9157161 --- /dev/null +++ b/examples/dmenu.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# A list of options, one per line +options="Option 1 +Option 2 +Option 3" + +# Pipe options to wofi and capture the selection +selection=$(echo "$options" | cargo run -- --show dmenu) + +# Do something with the selection +echo "You selected: $selection" diff --git a/src/lib/config.rs b/src/lib/config.rs index d9b287a..66896ec 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::{env, fmt, fs}; -use anyhow::{Error, anyhow}; +use anyhow::anyhow; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -23,6 +23,14 @@ impl fmt::Display for ConfigurationError { } } +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] +pub enum Anchor { + Top, + Left, + Bottom, + Right, +} + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum MatchMethod { Fuzzy, @@ -85,6 +93,20 @@ pub enum ArgsError { InvalidParameter(String), } +impl FromStr for Anchor { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.trim() { + "top" => Ok(Anchor::Top), + "left" => Ok(Anchor::Left), + "bottom" => Ok(Anchor::Bottom), + "right" => Ok(Anchor::Right), + other => Err(format!("Invalid anchor: {}", other)), + } + } +} + impl FromStr for Mode { type Err = ArgsError; @@ -201,8 +223,11 @@ pub struct Config { #[clap(short = 'q', long = "parse-search")] pub parse_search: Option, - #[clap(short = 'l', long = "location")] - pub location: Option, + /// set where the window is displayed. + /// can be used to anchor a window to an edge by + /// setting top,left for example + #[clap(short = 'l', long = "location", value_delimiter = ',', value_parser = clap::builder::ValueParser::new(Anchor::from_str))] + pub location: Option>, #[clap(short = 'a', long = "no-actions")] pub no_actions: Option, @@ -508,7 +533,6 @@ pub fn default_line_wrap() -> Option { // max_lines = lines; // columns = strtol(config_get(config, "columns", "1"), NULL, 10); // sort_order = config_get_mnemonic(config, "sort_order", "default", 2, "default", "alphabetical"); -// line_wrap = config_get_mnemonic(config, "line_wrap", "off", 4, "off", "word", "char", "word_char") - 1; // bool global_coords = strcmp(config_get(config, "global_coords", "false"), "true") == 0; // hide_search = strcmp(config_get(config, "hide_search", "false"), "true") == 0; // char* search = map_get(config, "search"); @@ -700,9 +724,9 @@ pub fn load_config(args_opt: Option) -> Result merge_result.prompt = Some("run".to_owned()), Mode::Drun => merge_result.prompt = Some("drun".to_owned()), Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()), - Mode::Math => merge_result.prompt = Some("math".to_owned()), - Mode::File => merge_result.prompt = Some("file".to_owned()), - Mode::Auto => merge_result.prompt = Some("auto".to_owned()), + Mode::Math => merge_result.prompt = Some("math".to_owned()), + Mode::File => merge_result.prompt = Some("file".to_owned()), + Mode::Auto => merge_result.prompt = Some("auto".to_owned()), }, } } diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 0681c72..72e88a3 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -1,70 +1,26 @@ -use anyhow::anyhow; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::{env, fs, string}; + use freedesktop_file_parser::DesktopFile; use gdk4::Display; use gtk4::prelude::*; use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use home::home_dir; -use log::{debug, info, warn}; +use log; use regex::Regex; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; -use std::{env, fs, string}; #[derive(Debug)] pub enum DesktopError { MissingIcon, } -// -// #[derive(Clone)] -// pub struct IconResolver { -// cache: HashMap, -// } -// -// impl Default for IconResolver { -// #[must_use] -// fn default() -> IconResolver { -// Self::new() -// } -// } -// -// impl IconResolver { -// #[must_use] -// pub fn new() -> IconResolver { -// IconResolver { -// cache: HashMap::new(), -// } -// } -// -// pub fn icon_path_no_cache(&self, icon_name: &str) -> Result { -// let icon = fetch_icon_from_theme(icon_name) -// .or_else(|_| -// fetch_icon_from_common_dirs(icon_name) -// .or_else(|_| default_icon())); -// -// icon -// } -// -// pub fn icon_path(&mut self, icon_name: &str) -> String { -// if let Some(icon_path) = self.cache.get(icon_name) { -// return icon_path.to_owned(); -// } -// -// let icon = self.icon_path_no_cache(icon_name); -// -// self.cache -// .entry(icon_name.to_owned()) -// .or_insert_with(|| icon.unwrap_or_default()) -// .to_owned() -// } -// } - /// # Errors /// /// Will return `Err` if no icon can be found pub fn default_icon() -> Result { - fetch_icon_from_theme("image-missing").map_err(|e| DesktopError::MissingIcon) + fetch_icon_from_theme("image-missing").map_err(|_| DesktopError::MissingIcon) } fn fetch_icon_from_theme(icon_name: &str) -> Result { @@ -177,7 +133,7 @@ pub fn find_desktop_files() -> Vec { .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:?}"); + log::debug!("loading desktop file {desktop_file:?}"); fs::read_to_string(desktop_file) .ok() .and_then(|content| freedesktop_file_parser::parse(&content).ok()) diff --git a/src/lib/gui.rs b/src/lib/gui.rs index f52ab74..d0d19a6 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::ops::DerefMut; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -15,13 +14,17 @@ use gtk4::prelude::{ ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt, }; -use gtk4::{Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk, NaturalWrapMode}; +use gtk4::{ + Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, + ListBox, ListBoxRow, NaturalWrapMode, Ordering, PolicyType, ScrolledWindow, SearchEntry, + Widget, gdk, +}; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell}; use log; use crate::config; -use crate::config::{Animation, Config, MatchMethod, WrapMode}; +use crate::config::{Anchor, Animation, Config, MatchMethod, WrapMode}; type ArcMenuMap = Arc>>>; type ArcProvider = Arc>>; @@ -32,6 +35,17 @@ pub trait ItemProvider { fn get_sub_elements(&mut self, item: &MenuItem) -> Option>>; } +impl From<&Anchor> for Edge { + fn from(value: &Anchor) -> Self { + match value { + Anchor::Top => Edge::Top, + Anchor::Left => Edge::Left, + Anchor::Bottom => Edge::Bottom, + Anchor::Right => Edge::Right, + } + } +} + impl From for Orientation { fn from(orientation: config::Orientation) -> Self { match orientation { @@ -44,9 +58,9 @@ impl From for Orientation { impl From<&WrapMode> for NaturalWrapMode { fn from(value: &WrapMode) -> Self { match value { - WrapMode::None => {NaturalWrapMode::None}, - WrapMode::Word => {NaturalWrapMode::Word}, - WrapMode::Inherit => {NaturalWrapMode::Inherit}, + WrapMode::None => NaturalWrapMode::None, + WrapMode::Word => NaturalWrapMode::Word, + WrapMode::Inherit => NaturalWrapMode::Inherit, } } } @@ -141,8 +155,11 @@ fn build_ui( window.set_namespace(Some("worf")); } - /// todo make this configurable - //window.set_anchor(Edge::Top, true); + config.location.as_ref().map(|location| { + for anchor in location { + window.set_anchor(anchor.into(), true); + } + }); let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); outer_box.set_widget_name("outer-box"); @@ -816,7 +833,7 @@ fn create_menu_row( } let label = Label::new(Some(menu_item.label.as_str())); - let wrap_mode : NaturalWrapMode = if let Some(config_wrap) = &config.line_wrap { + let wrap_mode: NaturalWrapMode = if let Some(config_wrap) = &config.line_wrap { config_wrap.into() } else { NaturalWrapMode::Word @@ -951,7 +968,9 @@ fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option { // highly unlikely that we are dealing with > i64 items #[allow(clippy::cast_possible_wrap)] -pub fn sort_menu_items_alphabetically_honor_initial_score(items: &mut [MenuItem]) { +pub fn sort_menu_items_alphabetically_honor_initial_score( + items: &mut [MenuItem], +) { let mut regular_score = items.len() as i64; items.sort_by(|l, r| l.label.cmp(&r.label)); diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 065e53a..9fc85f5 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,21 +1,39 @@ +use std::os::unix::prelude::CommandExt; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::{env, fmt, fs, io}; + +use anyhow::Context; +use freedesktop_file_parser::EntryType; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + use crate::config::{Config, expand_path}; use crate::desktop::{ default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale, }; +use crate::gui; use crate::gui::{ItemProvider, MenuItem}; -use crate::{config, desktop, gui}; -use anyhow::{Context, Error, anyhow}; -use freedesktop_file_parser::EntryType; -use gtk4::Image; -use libc::option; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::os::unix::fs::PermissionsExt; -use std::os::unix::prelude::CommandExt; -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use std::{env, fs, io}; + +#[derive(Debug)] +pub enum ModeError { + UpdateCacheError(String), + MissingAction, + RunError(String), + MissingCache, +} + +impl fmt::Display for ModeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ModeError::UpdateCacheError(s) => write!(f, "UpdateCacheError {s}"), + ModeError::MissingAction => write!(f, "MissingAction"), + ModeError::RunError(s) => write!(f, "RunError, {s}"), + ModeError::MissingCache => write!(f, "MissingCache"), + } + } +} #[derive(Debug, Deserialize, Serialize, Clone)] struct DRunCache { @@ -139,7 +157,7 @@ impl ItemProvider for DRunProvider { self.items.clone() } - fn get_sub_elements(&mut self, item: &MenuItem) -> Option>> { + fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { None } } @@ -283,9 +301,7 @@ struct MathProvider { impl MathProvider { fn new(menu_item_data: T) -> Self { - Self { - menu_item_data, - } + Self { menu_item_data } } fn contains_math_functions_or_starts_with_number(input: &str) -> bool { @@ -327,7 +343,7 @@ impl ItemProvider for MathProvider { } } - fn get_sub_elements(&mut self, item: &MenuItem) -> Option>> { + fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { None } } @@ -366,7 +382,9 @@ impl ItemProvider for AutoItemProvider { let trimmed_search = search.trim(); if trimmed_search.is_empty() { self.drun_provider.get_elements(search_opt) - } else if MathProvider::::contains_math_functions_or_starts_with_number(trimmed_search) { + } else if MathProvider::::contains_math_functions_or_starts_with_number( + trimmed_search, + ) { self.math_provider.get_elements(search_opt) } else if trimmed_search.starts_with("$") || trimmed_search.starts_with("/") @@ -392,7 +410,7 @@ impl ItemProvider for AutoItemProvider { /// # Errors /// /// Will return `Err` if it was not able to spawn the process -pub fn d_run(config: &Config) -> anyhow::Result<()> { +pub fn d_run(config: &Config) -> Result<(), ModeError> { let provider = DRunProvider::new("".to_owned()); let cache_path = provider.cache_path.clone(); let mut cache = provider.cache.clone(); @@ -400,9 +418,7 @@ pub fn d_run(config: &Config) -> anyhow::Result<()> { // todo ues a arc instead of cloning the config let selection_result = gui::show(config.clone(), provider); match selection_result { - Ok(s) => { - update_drun_cache_and_run(cache_path, &mut cache, s)?; - } + Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s)?, Err(_) => { log::error!("No item selected"); } @@ -411,7 +427,7 @@ pub fn d_run(config: &Config) -> anyhow::Result<()> { Ok(()) } -pub fn auto(config: &Config) -> anyhow::Result<()> { +pub fn auto(config: &Config) -> Result<(), ModeError> { let provider = AutoItemProvider::new(); let cache_path = provider.drun_provider.cache_path.clone(); let mut cache = provider.drun_provider.cache.clone(); @@ -429,12 +445,9 @@ pub fn auto(config: &Config) -> anyhow::Result<()> { } AutoRunType::File => { if let Some(action) = selection_result.action { - spawn_fork(&action, selection_result.working_dir.as_ref())? + spawn_fork(&action, selection_result.working_dir.as_ref())?; } } - _ => { - todo!("not supported yet"); - } } } } @@ -446,7 +459,7 @@ pub fn auto(config: &Config) -> anyhow::Result<()> { Ok(()) } -pub fn file(config: &Config) -> Result<(), String> { +pub fn file(config: &Config) -> Result<(), ModeError> { let provider = FileItemProvider::new("".to_owned()); // todo ues a arc instead of cloning the config @@ -454,7 +467,7 @@ pub fn file(config: &Config) -> Result<(), String> { match selection_result { Ok(s) => { if let Some(action) = s.action { - spawn_fork(&action, s.working_dir.as_ref()).map_err(|e| e.to_string())?; + spawn_fork(&action, s.working_dir.as_ref())?; } } Err(_) => { @@ -465,14 +478,13 @@ pub fn file(config: &Config) -> Result<(), String> { Ok(()) } -pub fn math(config: &Config) -> Result<(), String> { +pub fn math(config: &Config) -> Result<(), ModeError> { let provider = MathProvider::new("".to_owned()); // todo ues a arc instead of cloning the config let selection_result = gui::show(config.clone(), provider); match selection_result { - Ok(_) => { - } + Ok(_) => {} Err(_) => { log::error!("No item selected"); } @@ -481,11 +493,15 @@ pub fn math(config: &Config) -> Result<(), String> { Ok(()) } +pub fn dmenu(_: &Config) -> Result<(), ModeError> { + Ok(()) +} + fn update_drun_cache_and_run( cache_path: Option, cache: &mut HashMap, selection_result: MenuItem, -) -> Result<(), Error> { +) -> Result<(), ModeError> { if let Some(cache_path) = cache_path { *cache.entry(selection_result.label).or_insert(0) += 1; if let Err(e) = save_cache_file(&cache_path, &cache) { @@ -496,7 +512,7 @@ fn update_drun_cache_and_run( if let Some(action) = selection_result.action { spawn_fork(&action, selection_result.working_dir.as_ref()) } else { - Err(anyhow::anyhow!("cannot find drun action")) + Err(ModeError::MissingAction) } } @@ -520,12 +536,13 @@ fn save_cache_file(path: &PathBuf, data: &HashMap) -> anyhow::Resul fs::write(path, toml_string).map_err(|e| anyhow::anyhow!(e)) } -fn load_cache_file(cache_path: Option<&PathBuf>) -> anyhow::Result> { +fn load_cache_file(cache_path: Option<&PathBuf>) -> Result, ModeError> { let Some(path) = cache_path else { - return Err(anyhow!("Cache is missing")); + return Err(ModeError::MissingCache); }; - let toml_content = fs::read_to_string(path)?; + let toml_content = + fs::read_to_string(path).map_err(|e| ModeError::UpdateCacheError(format!("{e}")))?; let parsed: toml::Value = toml_content.parse().expect("Failed to parse TOML"); let mut result: HashMap = HashMap::new(); @@ -555,17 +572,18 @@ fn create_file_if_not_exists(path: &PathBuf) -> anyhow::Result<()> { } } -fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> { +fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> Result<(), ModeError> { // todo fix actions ?? // todo graphical disk map icon not working let parts = cmd.split(' ').collect::>(); if parts.is_empty() { - return Err(anyhow!("empty command passed")); + return Err(ModeError::MissingAction); } if let Some(dir) = working_dir { - env::set_current_dir(dir)?; + env::set_current_dir(dir) + .map_err(|e| ModeError::RunError(format!("cannot set workdir {e}")))? } let exec = parts[0].replace('"', ""); diff --git a/src/main.rs b/src/main.rs index e024387..65f3df5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ fn main() -> anyhow::Result<()> { .init(); let args = config::parse_args(); - let mut config = config::load_config(Some(args)).map_err(|e| anyhow!(e))?; + let config = config::load_config(Some(args)).map_err(|e| anyhow!(e))?; if let Some(show) = &config.show { match show { @@ -21,10 +21,10 @@ fn main() -> anyhow::Result<()> { todo!("run not implemented") } Mode::Drun => { - mode::d_run(&config)?; + mode::d_run(&config).map_err(|e| anyhow!(e))?; } Mode::Dmenu => { - todo!("dmenu not implemented") + mode::dmenu(&config).map_err(|e| anyhow!(e))?; } Mode::File => { mode::file(&config).map_err(|e| anyhow!(e))?; @@ -33,7 +33,7 @@ fn main() -> anyhow::Result<()> { mode::math(&config).map_err(|e| anyhow!(e))?; } Mode::Auto => { - mode::auto(&config)?; + mode::auto(&config).map_err(|e| anyhow!(e))?; } }