add auto mode

This commit is contained in:
Alexander Mohr 2025-04-21 02:08:38 +02:00
parent 1da13e94fa
commit f558131233
13 changed files with 1011 additions and 362 deletions

23
Cargo.lock generated
View file

@ -436,6 +436,12 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "freedesktop-file-parser" name = "freedesktop-file-parser"
version = "0.1.3" version = "0.1.3"
@ -1013,6 +1019,16 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "meval"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
dependencies = [
"fnv",
"nom",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.7" version = "0.8.7"
@ -1033,6 +1049,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "nom"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1726,6 +1748,7 @@ dependencies = [
"hyprland", "hyprland",
"libc", "libc",
"log", "log",
"meval",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",

View file

@ -42,3 +42,4 @@ freedesktop-file-parser = "0.1.3"
strsim = "0.11.1" strsim = "0.11.1"
dirs = "6.0.0" dirs = "6.0.0"
which = "7.0.3" which = "7.0.3"
meval = "0.2.0"

View file

@ -1,13 +1,28 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::{env, fs}; use std::{env, fmt, fs};
use anyhow::anyhow; use anyhow::{Error, anyhow};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use thiserror::Error; use thiserror::Error;
#[derive(Debug)]
pub enum ConfigurationError {
Open(String),
Parse(String),
}
impl fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigurationError::Open(e) => write!(f, "{e}"),
ConfigurationError::Parse(e) => write!(f, "{e}"),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
pub enum MatchMethod { pub enum MatchMethod {
Fuzzy, Fuzzy,
@ -46,6 +61,9 @@ pub enum Mode {
/// reads from stdin and displays options which when selected will be output to stdout. /// reads from stdin and displays options which when selected will be output to stdout.
Dmenu, Dmenu,
/// tries to determine automatically what to do
Auto,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -62,6 +80,7 @@ impl FromStr for Mode {
"run" => Ok(Mode::Run), "run" => Ok(Mode::Run),
"drun" => Ok(Mode::Drun), "drun" => Ok(Mode::Drun),
"dmenu" => Ok(Mode::Dmenu), "dmenu" => Ok(Mode::Dmenu),
"auto" => Ok(Mode::Auto),
_ => Err(ArgsError::InvalidParameter( _ => Err(ArgsError::InvalidParameter(
format!("{s} is not a valid argument show this, see help for details").to_owned(), format!("{s} is not a valid argument show this, see help for details").to_owned(),
)), )),
@ -598,6 +617,17 @@ pub fn style_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
resolve_path(full_path, alternative_paths.into_iter().collect()) resolve_path(full_path, alternative_paths.into_iter().collect())
} }
/// # Errors
///
/// Will return Err when it cannot resolve any path or no style is found
pub fn conf_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
let alternative_paths = path_alternatives(
vec![dirs::config_dir()],
&PathBuf::from("worf").join("config"),
);
resolve_path(full_path, alternative_paths.into_iter().collect())
}
#[must_use] #[must_use]
pub fn path_alternatives(base_paths: Vec<Option<PathBuf>>, sub_path: &PathBuf) -> Vec<PathBuf> { pub fn path_alternatives(base_paths: Vec<Option<PathBuf>>, sub_path: &PathBuf) -> Vec<PathBuf> {
base_paths base_paths
@ -635,41 +665,28 @@ pub fn resolve_path(
/// * cannot parse the config file /// * cannot parse the config file
/// * no config file exists /// * no config file exists
/// * config file and args cannot be merged /// * config file and args cannot be merged
pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> { pub fn load_config(args_opt: Option<Config>) -> Result<Config, ConfigurationError> {
let home_dir = env::var("HOME")?; let config_path = conf_path(args_opt.as_ref().map(|c| c.config.clone()).flatten());
let config_path = args_opt.as_ref().map(|c| {
c.config.as_ref().map_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")
},
PathBuf::from,
)
});
match config_path { match config_path {
Some(path) => { Ok(path) => {
let toml_content = fs::read_to_string(path)?; let toml_content =
let mut config: Config = toml::from_str(&toml_content)?; fs::read_to_string(path).map_err(|e| ConfigurationError::Open(format!("{e}")))?;
let mut config: Config = toml::from_str(&toml_content)
.map_err(|e| ConfigurationError::Parse(format!("{e}")))?;
if let Some(args) = args_opt { if let Some(args) = args_opt {
let mut merge_result = merge_config_with_args(&mut config, &args)?; let mut merge_result = merge_config_with_args(&mut config, &args)
.map_err(|e| ConfigurationError::Parse(format!("{e}")))?;
if merge_result.prompt.is_none() { if merge_result.prompt.is_none() {
match &merge_result.show { match &merge_result.show {
None => {} None => {}
Some(mode) => { Some(mode) => match mode {
match mode {
Mode::Run => merge_result.prompt = Some("run".to_owned()), Mode::Run => merge_result.prompt = Some("run".to_owned()),
Mode::Drun => merge_result.prompt = Some("drun".to_owned()), Mode::Drun => merge_result.prompt = Some("drun".to_owned()),
Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()), Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()),
} _ => {}
} },
} }
} }
@ -678,9 +695,32 @@ pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
Ok(config) Ok(config)
} }
} }
None => Err(anyhow!("No config file found")),
Err(e) => Err(ConfigurationError::Open(format!("{e}"))),
} }
} }
pub fn expand_path(input: &str) -> PathBuf {
let mut path = input.to_string();
// Expand ~ to home directory
if path.starts_with("~") {
if let Some(home_dir) = dirs::home_dir() {
path = path.replacen("~", home_dir.to_str().unwrap_or(""), 1);
}
}
// Expand $VAR style environment variables
if path.contains('$') {
for (key, value) in env::vars() {
let var_pattern = format!("${}", key);
if path.contains(&var_pattern) {
path = path.replace(&var_pattern, &value);
}
}
}
PathBuf::from(path)
}
/// # Errors /// # Errors
/// ///

View file

@ -1,5 +1,6 @@
use anyhow::anyhow; use anyhow::anyhow;
use freedesktop_file_parser::DesktopFile; use freedesktop_file_parser::DesktopFile;
use gdk4::Display;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use gtk4::{IconLookupFlags, IconTheme, TextDirection};
use home::home_dir; use home::home_dir;
@ -10,88 +11,71 @@ use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, fs, string}; use std::{env, fs, string};
pub struct IconResolver { #[derive(Debug)]
cache: HashMap<String, String>, pub enum DesktopError {
MissingIcon,
} }
impl Default for IconResolver { //
#[must_use] // #[derive(Clone)]
fn default() -> IconResolver { // pub struct IconResolver {
Self::new() // cache: HashMap<String, String>,
} // }
} //
// impl Default for IconResolver {
impl IconResolver { // #[must_use]
#[must_use] // fn default() -> IconResolver {
pub fn new() -> IconResolver { // Self::new()
IconResolver { // }
cache: HashMap::new(), // }
} //
} // impl IconResolver {
// #[must_use]
pub fn icon_path(&mut self, icon_name: &str) -> String { // pub fn new() -> IconResolver {
if let Some(icon_path) = self.cache.get(icon_name) { // IconResolver {
info!("Fetching {icon_name} from cache"); // cache: HashMap::new(),
return icon_path.to_owned(); // }
} // }
//
info!("Loading icon for {icon_name}"); // pub fn icon_path_no_cache(&self, icon_name: &str) -> Result<String, DesktopError> {
let icon = fetch_icon_from_theme(icon_name) // let icon = fetch_icon_from_theme(icon_name)
.or_else(|_| { // .or_else(|_|
fetch_icon_from_common_dirs(icon_name).map_or_else( // fetch_icon_from_common_dirs(icon_name)
|| Err(anyhow::anyhow!("Missing file")), // Return an error here // .or_else(|_| default_icon()));
Ok, //
) // icon
}) // }
.or_else(|_| { //
warn!("Missing icon for {icon_name}, using fallback"); // pub fn icon_path(&mut self, icon_name: &str) -> String {
default_icon() // if let Some(icon_path) = self.cache.get(icon_name) {
}); // return icon_path.to_owned();
// }
self.cache //
.entry(icon_name.to_owned()) // let icon = self.icon_path_no_cache(icon_name);
.or_insert_with(|| icon.unwrap_or_default()) //
.to_owned() // self.cache
} // .entry(icon_name.to_owned())
} // .or_insert_with(|| icon.unwrap_or_default())
// .to_owned()
// }
// }
/// # Errors /// # Errors
/// ///
/// Will return `Err` if no icon can be found /// Will return `Err` if no icon can be found
pub fn default_icon() -> anyhow::Result<String> { pub fn default_icon() -> Result<String, DesktopError> {
fetch_icon_from_theme("image-missing") fetch_icon_from_theme("image-missing").map_err(|e| DesktopError::MissingIcon)
} }
// fn fetch_icon_from_desktop_file(icon_name: &str) -> Option<String> { fn fetch_icon_from_theme(icon_name: &str) -> Result<String, DesktopError> {
// // 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<String> {
let display = gtk4::gdk::Display::default(); let display = gtk4::gdk::Display::default();
if display.is_none() { if display.is_none() {
log::error!("Failed to get display"); log::error!("Failed to get display");
} }
let display = display.unwrap(); let display = Display::default().expect("Failed to get default display");
let theme = IconTheme::for_display(&display); let theme = IconTheme::for_display(&display);
let icon = theme.lookup_icon( let icon = theme.lookup_icon(
icon_name, icon_name,
&[], &[],
@ -106,17 +90,25 @@ fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result<String> {
.and_then(|file| file.path()) .and_then(|file| file.path())
.and_then(|path| path.to_str().map(string::ToString::to_string)) .and_then(|path| path.to_str().map(string::ToString::to_string))
{ {
None => Err(anyhow!("Cannot find file")), None => {
let path = PathBuf::from("/usr/share/icons")
.join(theme.theme_name())
.join(format!("{icon_name}.svg"));
if path.exists() {
Ok(path.display().to_string())
} else {
Err(DesktopError::MissingIcon)
}
}
Some(i) => Ok(i), Some(i) => Ok(i),
} }
} }
fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> { pub fn fetch_icon_from_common_dirs(icon_name: &str) -> Result<String, DesktopError> {
let mut paths = vec![ let mut paths = vec![
PathBuf::from("/usr/local/share/icons"), PathBuf::from("/usr/local/share/icons"),
PathBuf::from("/usr/share/icons"), PathBuf::from("/usr/share/icons"),
PathBuf::from("/usr/share/pixmaps"), PathBuf::from("/usr/share/pixmaps"),
// /usr/share/icons contains the theme icons, handled via separate function
]; ];
if let Some(home) = home_dir() { if let Some(home) = home_dir() {
@ -133,6 +125,7 @@ fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> {
find_file_case_insensitive(dir.as_path(), &formatted_name) find_file_case_insensitive(dir.as_path(), &formatted_name)
.and_then(|files| files.first().map(|f| f.to_string_lossy().into_owned())) .and_then(|files| files.first().map(|f| f.to_string_lossy().into_owned()))
}) })
.ok_or_else(|| DesktopError::MissingIcon)
} }
fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<PathBuf>> { fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<PathBuf>> {
@ -156,7 +149,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
/// # Errors /// # Errors
/// ///
/// Will return Err when it cannot parse the internal regex /// Will return Err when it cannot parse the internal regex
pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> { pub fn find_desktop_files() -> Vec<DesktopFile> {
let mut paths = vec![ let mut paths = vec![
PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/share/applications"),
PathBuf::from("/usr/local/share/applications"), PathBuf::from("/usr/local/share/applications"),
@ -168,6 +161,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
} }
if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") { if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") {
// todo use dirs:: instead
paths.push(PathBuf::from(xdg_data_home).join(".applications")); paths.push(PathBuf::from(xdg_data_home).join(".applications"));
} }
@ -175,7 +169,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
paths.push(PathBuf::from(xdg_data_dir).join(".applications")); paths.push(PathBuf::from(xdg_data_dir).join(".applications"));
} }
let regex = &Regex::new("(?i).*\\.desktop$")?; let regex = &Regex::new("(?i).*\\.desktop$").unwrap();
let p: Vec<_> = paths let p: Vec<_> = paths
.into_iter() .into_iter()
@ -190,7 +184,7 @@ pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
}) })
}) })
.collect(); .collect();
Ok(p) p
} }
#[must_use] #[must_use]

View file

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::DerefMut;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
@ -19,15 +20,21 @@ use gtk4::{
ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk, ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk,
}; };
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
use gtk4_layer_shell::{KeyboardMode, LayerShell}; use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell};
use log; use log;
use crate::config; use crate::config;
use crate::config::{Animation, Config, MatchMethod}; use crate::config::{Animation, Config, MatchMethod};
type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>; type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>;
type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T>>>;
type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>; type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>;
pub trait ItemProvider<T: std::clone::Clone> {
fn get_elements(&mut self, search: Option<&str>) -> Vec<MenuItem<T>>;
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> Option<Vec<MenuItem<T>>>;
}
impl From<config::Orientation> for Orientation { impl From<config::Orientation> for Orientation {
fn from(orientation: config::Orientation) -> Self { fn from(orientation: config::Orientation) -> Self {
match orientation { match orientation {
@ -47,8 +54,8 @@ impl From<config::Align> for Align {
} }
} }
#[derive(Clone)] #[derive(Clone, PartialEq)]
pub struct MenuItem<T> { pub struct MenuItem<T: Clone> {
pub label: String, // todo support empty label? pub label: String, // todo support empty label?
pub icon_path: Option<String>, pub icon_path: Option<String>,
pub action: Option<String>, pub action: Option<String>,
@ -61,12 +68,19 @@ pub struct MenuItem<T> {
pub data: Option<T>, pub data: Option<T>,
} }
impl<T: Clone> AsRef<MenuItem<T>> for MenuItem<T> {
fn as_ref(&self) -> &MenuItem<T> {
self
}
}
/// # Errors /// # Errors
/// ///
/// Will return Err when the channel between the UI and this is broken /// Will return Err when the channel between the UI and this is broken
pub fn show<T>(config: Config, elements: Vec<MenuItem<T>>) -> Result<MenuItem<T>, anyhow::Error> pub fn show<T, P>(config: Config, item_provider: P) -> Result<MenuItem<T>, anyhow::Error>
where where
T: Clone + 'static, T: Clone + 'static,
P: ItemProvider<T> + 'static + Clone,
{ {
if let Some(ref css) = config.style { if let Some(ref css) = config.style {
let provider = CssProvider::new(); let provider = CssProvider::new();
@ -85,7 +99,7 @@ where
let (sender, receiver) = channel::bounded(1); let (sender, receiver) = channel::bounded(1);
app.connect_activate(move |app| { app.connect_activate(move |app| {
build_ui(&config, &elements, &sender, app); build_ui(&config, item_provider.clone(), &sender, app);
}); });
let gtk_args: [&str; 0] = []; let gtk_args: [&str; 0] = [];
@ -93,13 +107,14 @@ where
receiver.recv()? receiver.recv()?
} }
fn build_ui<T>( fn build_ui<T, P>(
config: &Config, config: &Config,
elements: &Vec<MenuItem<T>>, item_provider: P,
sender: &Sender<Result<MenuItem<T>, anyhow::Error>>, sender: &Sender<Result<MenuItem<T>, anyhow::Error>>,
app: &Application, app: &Application,
) where ) where
T: Clone + 'static, T: Clone + 'static,
P: ItemProvider<T> + 'static,
{ {
let window = ApplicationWindow::builder() let window = ApplicationWindow::builder()
.application(app) .application(app)
@ -119,6 +134,9 @@ fn build_ui<T>(
window.set_namespace(Some("worf")); window.set_namespace(Some("worf"));
} }
/// todo make this configurable
window.set_anchor(Edge::Top, true);
let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0);
outer_box.set_widget_name("outer-box"); outer_box.set_widget_name("outer-box");
@ -126,7 +144,7 @@ fn build_ui<T>(
entry.set_widget_name("input"); entry.set_widget_name("input");
entry.set_css_classes(&["input"]); entry.set_css_classes(&["input"]);
entry.set_placeholder_text(config.prompt.as_deref()); entry.set_placeholder_text(config.prompt.as_deref());
entry.set_sensitive(false); entry.set_can_focus(false);
outer_box.append(&entry); outer_box.append(&entry);
let scroll = ScrolledWindow::new(); let scroll = ScrolledWindow::new();
@ -145,6 +163,7 @@ fn build_ui<T>(
inner_box.set_css_classes(&["inner-box"]); inner_box.set_css_classes(&["inner-box"]);
inner_box.set_hexpand(true); inner_box.set_hexpand(true);
inner_box.set_vexpand(false); inner_box.set_vexpand(false);
if let Some(halign) = config.halign { if let Some(halign) = config.halign {
inner_box.set_halign(halign.into()); inner_box.set_halign(halign.into());
} }
@ -161,27 +180,29 @@ fn build_ui<T>(
inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_max_children_per_line(config.columns.unwrap());
inner_box.set_activate_on_single_click(true); inner_box.set_activate_on_single_click(true);
let item_provider = Arc::new(Mutex::new(item_provider));
let list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new())); let list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new()));
for entry in elements { build_ui_from_menu_items(
list_items &item_provider.lock().unwrap().get_elements(None),
.lock() &list_items,
.unwrap() // panic here ok? deadlock? &inner_box,
.insert( &config,
add_menu_item(&inner_box, entry, config, sender, &list_items, app, &window), &sender,
entry.clone(), &app,
&window,
); );
}
let items_sort = Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items); let items_sort = ArcMenuMap::clone(&list_items);
inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_sort)); inner_box.set_sort_func(move |child1, child2| {
sort_menu_items_by_score(child1, child2, items_sort.clone())
});
let items_focus = Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items); let items_focus = ArcMenuMap::clone(&list_items);
inner_box.connect_map(move |fb| { inner_box.connect_map(move |fb| {
fb.grab_focus(); fb.grab_focus();
fb.invalidate_sort(); fb.invalidate_sort();
let mut item_lock = items_focus.lock().unwrap(); select_first_visible_child(&items_focus, fb);
select_first_visible_child(&mut *item_lock, fb);
}); });
let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0); let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0);
@ -194,8 +215,9 @@ fn build_ui<T>(
inner_box, inner_box,
app.clone(), app.clone(),
sender.clone(), sender.clone(),
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items), ArcMenuMap::clone(&list_items),
config.clone(), config.clone(),
item_provider,
); );
window.set_child(Widget::NONE); window.set_child(Widget::NONE);
@ -203,27 +225,109 @@ fn build_ui<T>(
animate_window_show(config.clone(), window.clone(), outer_box); animate_window_show(config.clone(), window.clone(), outer_box);
} }
fn build_ui_from_menu_items<T: Clone + 'static>(
items: &Vec<MenuItem<T>>,
list_items: &ArcMenuMap<T>,
inner_box: &FlowBox,
config: &Config,
sender: &MenuItemSender<T>,
app: &Application,
window: &ApplicationWindow,
) {
{
let mut arc_lock = list_items.lock().unwrap();
inner_box.unset_sort_func();
loop {
if let Some(b) = inner_box.child_at_index(0) {
inner_box.remove(&b);
} else {
break;
}
}
for entry in items {
arc_lock.insert(
add_menu_item(&inner_box, entry, config, sender, &list_items, app, window),
(*entry).clone(),
);
}
}
let lic = list_items.clone();
inner_box
.set_sort_func(move |child2, child1| sort_menu_items_by_score(child1, child2, lic.clone()));
inner_box.invalidate_sort();
}
fn setup_key_event_handler<T: Clone + 'static>( fn setup_key_event_handler<T: Clone + 'static>(
window: &ApplicationWindow, window: &ApplicationWindow,
entry_clone: SearchEntry, entry: SearchEntry,
inner_box: FlowBox, inner_box: FlowBox,
app: Application, app: Application,
sender: MenuItemSender<T>, sender: MenuItemSender<T>,
list_items: Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>, list_items: Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>,
config: Config, config: Config,
item_provider: ArcProvider<T>,
) { ) {
let key_controller = EventControllerKey::new(); let key_controller = EventControllerKey::new();
let window_clone = window.clone(); let window_clone = window.clone();
let entry_clone = entry.clone();
key_controller.connect_key_pressed(move |_, key_value, _, _| { key_controller.connect_key_pressed(move |_, key_value, _, _| {
match key_value { handle_key_press(
Key::Escape => { &entry_clone,
&inner_box,
&app,
&sender,
&list_items,
&config,
&item_provider,
&window_clone,
&key_value,
)
});
window.add_controller(key_controller);
}
fn handle_key_press<T: Clone + 'static>(
search_entry: &SearchEntry,
inner_box: &FlowBox,
app: &Application,
sender: &MenuItemSender<T>,
list_items: &ArcMenuMap<T>,
config: &Config,
item_provider: &ArcProvider<T>,
window_clone: &ApplicationWindow,
keyboard_key: &Key,
) -> Propagation {
let update_view = |query: &String, items: Vec<MenuItem<T>>| {
build_ui_from_menu_items(
&items,
&list_items,
&inner_box,
&config,
&sender,
&app,
&window_clone,
);
filter_widgets(query, list_items, &config, &inner_box);
select_first_visible_child(&list_items, &inner_box);
};
let update_view_from_provider = |query: &String| {
let filtered_list = item_provider.lock().unwrap().get_elements(Some(&query));
update_view(query, filtered_list)
};
match keyboard_key {
&Key::Escape => {
if let Err(e) = sender.send(Err(anyhow!("No item selected"))) { if let Err(e) = sender.send(Err(anyhow!("No item selected"))) {
log::error!("failed to send message {e}"); log::error!("failed to send message {e}");
} }
close_gui(app.clone(), window_clone.clone(), &config); close_gui(app.clone(), window_clone.clone(), &config);
} }
Key::Return => { &Key::Return => {
if let Err(e) = handle_selected_item( if let Err(e) = handle_selected_item(
&sender, &sender,
app.clone(), app.clone(),
@ -235,34 +339,55 @@ fn setup_key_event_handler<T: Clone + 'static>(
log::error!("{e}"); log::error!("{e}");
} }
} }
Key::BackSpace => { &Key::BackSpace => {
let mut items = list_items.lock().unwrap(); let mut query = search_entry.text().to_string();
let mut query = entry_clone.text().to_string();
query.pop(); query.pop();
entry_clone.set_text(&query); search_entry.set_text(&query);
filter_widgets(&query, &mut items, &config, &inner_box); update_view_from_provider(&query);
}
&Key::Tab => {
if let Some(fb) = inner_box.selected_children().first() {
if let Some(child) = fb.child() {
let expander = child.downcast::<Expander>().ok();
if let Some(expander) = expander {
expander.set_expanded(true);
} else {
let lock = list_items.lock().unwrap();
let menu_item = lock.get(fb);
if let Some(menu_item) = menu_item {
if let Some(new_items) =
item_provider.lock().unwrap().get_sub_elements(&menu_item)
{
let query = menu_item.label.clone();
drop(lock);
search_entry.set_text(&query);
update_view(&query, new_items);
}
}
}
}
}
return Propagation::Stop;
} }
_ => { _ => {
let mut items = list_items.lock().unwrap(); if let Some(c) = keyboard_key.to_unicode() {
if let Some(c) = key_value.to_unicode() { let current = search_entry.text().to_string();
let current = entry_clone.text().to_string();
let query = format!("{current}{c}"); let query = format!("{current}{c}");
entry_clone.set_text(&query); search_entry.set_text(&query);
filter_widgets(&query, &mut items, &config, &inner_box); update_view_from_provider(&query);
} }
} }
} }
Propagation::Proceed Propagation::Proceed
});
window.add_controller(key_controller);
} }
fn sort_menu_items<T>( fn sort_menu_items_by_score<T: std::clone::Clone>(
child1: &FlowBoxChild, child1: &FlowBoxChild,
child2: &FlowBoxChild, child2: &FlowBoxChild,
items_lock: &Mutex<HashMap<FlowBoxChild, MenuItem<T>>>, items_lock: ArcMenuMap<T>,
) -> Ordering { ) -> Ordering {
let lock = items_lock.lock().unwrap(); let lock = items_lock.lock().unwrap();
let m1 = lock.get(child1); let m1 = lock.get(child1);
@ -302,21 +427,21 @@ fn animate_window_show(config: Config, window: ApplicationWindow, outer_box: gtk
let monitor = display.monitor_at_surface(&surface); let monitor = display.monitor_at_surface(&surface);
if let Some(monitor) = monitor { if let Some(monitor) = monitor {
let geometry = monitor.geometry(); let geometry = monitor.geometry();
let Some(target_width) = percent_or_absolute(&config.width.unwrap(), geometry.width()) let Some(target_width) = percent_or_absolute(config.width.as_ref(), geometry.width())
else { else {
return; return;
}; };
let Some(target_height) = let Some(target_height) =
percent_or_absolute(&config.height.unwrap(), geometry.height()) percent_or_absolute(config.height.as_ref(), geometry.height())
else { else {
return; return;
}; };
animate_window( animate_window(
window.clone(), window.clone(),
config.show_animation.unwrap(), config.show_animation.unwrap_or(Animation::None),
config.show_animation_time.unwrap(), config.show_animation_time.unwrap_or(0),
target_height, target_height,
target_width, target_width,
move || { move || {
@ -348,8 +473,8 @@ where
animate_window( animate_window(
window, window,
config.hide_animation.unwrap(), config.hide_animation.unwrap_or(Animation::None),
config.hide_animation_time.unwrap(), config.hide_animation_time.unwrap_or(0),
target_h, target_h,
target_w, target_w,
on_done_func, on_done_func,
@ -563,7 +688,7 @@ fn add_menu_item<T: Clone + 'static>(
create_menu_row( create_menu_row(
entry_element, entry_element,
config, config,
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc), ArcMenuMap::clone(lock_arc),
sender.clone(), sender.clone(),
app.clone(), app.clone(),
window.clone(), window.clone(),
@ -579,7 +704,7 @@ fn add_menu_item<T: Clone + 'static>(
let menu_row = create_menu_row( let menu_row = create_menu_row(
entry_element, entry_element,
config, config,
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc), ArcMenuMap::clone(lock_arc),
sender.clone(), sender.clone(),
app.clone(), app.clone(),
window.clone(), window.clone(),
@ -595,7 +720,7 @@ fn add_menu_item<T: Clone + 'static>(
let sub_row = create_menu_row( let sub_row = create_menu_row(
sub_item, sub_item,
config, config,
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc), ArcMenuMap::clone(lock_arc),
sender.clone(), sender.clone(),
app.clone(), app.clone(),
window.clone(), window.clone(),
@ -659,7 +784,13 @@ fn create_menu_row<T: Clone + 'static>(
row.add_controller(click); row.add_controller(click);
let row_box = gtk4::Box::new(config.row_bow_orientation.unwrap().into(), 0); let row_box = gtk4::Box::new(
config
.row_bow_orientation
.unwrap_or(config::Orientation::Horizontal)
.into(),
0,
);
row_box.set_hexpand(true); row_box.set_hexpand(true);
row_box.set_vexpand(false); row_box.set_vexpand(false);
row_box.set_halign(Align::Fill); row_box.set_halign(Align::Fill);
@ -690,20 +821,26 @@ fn create_menu_row<T: Clone + 'static>(
label.set_wrap(true); label.set_wrap(true);
row_box.append(&label); row_box.append(&label);
if config.content_halign.unwrap() == config::Align::Start if config
|| config.content_halign.unwrap() == config::Align::Fill .content_halign
.is_some_and(|c| c == config::Align::Start)
|| config
.content_halign
.is_some_and(|c| c == config::Align::Fill)
{ {
label.set_xalign(0.0); label.set_xalign(0.0);
} }
row.upcast() row.upcast()
} }
fn filter_widgets<T>( fn filter_widgets<T: Clone>(
query: &str, query: &str,
items: &mut HashMap<FlowBoxChild, MenuItem<T>>, item_arc: &ArcMenuMap<T>,
config: &Config, config: &Config,
inner_box: &FlowBox, inner_box: &FlowBox,
) { ) {
{
let mut items = item_arc.lock().unwrap();
if items.is_empty() { if items.is_empty() {
for (child, _) in items.iter() { for (child, _) in items.iter() {
child.set_visible(true); child.set_visible(true);
@ -740,7 +877,13 @@ fn filter_widgets<T>(
let (search_sort_score, visible) = match matching { let (search_sort_score, visible) = match matching {
MatchMethod::Fuzzy => { MatchMethod::Fuzzy => {
let score = strsim::normalized_levenshtein(&query, &menu_item_search); let score = strsim::normalized_levenshtein(&query, &menu_item_search);
(score, score > config.fuzzy_min_score.unwrap()) (
score,
score
> config
.fuzzy_min_score
.unwrap_or(config::default_fuzzy_min_score().unwrap_or(0.0)),
)
} }
MatchMethod::Contains => { MatchMethod::Contains => {
if menu_item_search.contains(&query) { if menu_item_search.contains(&query) {
@ -762,14 +905,13 @@ fn filter_widgets<T>(
menu_item.search_sort_score = search_sort_score; menu_item.search_sort_score = search_sort_score;
flowbox_child.set_visible(visible); flowbox_child.set_visible(visible);
} }
}
select_first_visible_child(items, inner_box); inner_box.invalidate_sort();
} }
fn select_first_visible_child<T>( fn select_first_visible_child<T: Clone>(lock: &ArcMenuMap<T>, inner_box: &FlowBox) {
items: &mut HashMap<FlowBoxChild, MenuItem<T>>, let items = lock.lock().unwrap();
inner_box: &FlowBox,
) {
for i in 0..items.len() { for i in 0..items.len() {
let i_32 = i.try_into().unwrap_or(i32::MAX); let i_32 = i.try_into().unwrap_or(i32::MAX);
if let Some(child) = inner_box.child_at_index(i_32) { if let Some(child) = inner_box.child_at_index(i_32) {
@ -784,7 +926,8 @@ fn select_first_visible_child<T>(
// allowed because truncating is fine, we do no need the precision // allowed because truncating is fine, we do no need the precision
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn percent_or_absolute(value: &str, base_value: i32) -> Option<i32> { fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option<i32> {
if let Some(value) = value {
if value.contains('%') { if value.contains('%') {
let value = value.replace('%', "").trim().to_string(); let value = value.replace('%', "").trim().to_string();
match value.parse::<i32>() { match value.parse::<i32>() {
@ -794,11 +937,14 @@ fn percent_or_absolute(value: &str, base_value: i32) -> Option<i32> {
} else { } else {
value.parse::<i32>().ok() value.parse::<i32>().ok()
} }
} else {
None
}
} }
// highly unlikely that we are dealing with > i64 items // highly unlikely that we are dealing with > i64 items
#[allow(clippy::cast_possible_wrap)] #[allow(clippy::cast_possible_wrap)]
pub fn initialize_sort_scores<T>(items: &mut [MenuItem<T>]) { pub fn initialize_sort_scores<T: std::clone::Clone>(items: &mut [MenuItem<T>]) {
let mut regular_score = items.len() as i64; let mut regular_score = items.len() as i64;
items.sort_by(|l, r| l.label.cmp(&r.label)); items.sort_by(|l, r| l.label.cmp(&r.label));

View file

@ -1,13 +1,17 @@
use crate::config::Config; use crate::config::{Config, expand_path};
use crate::desktop::{ use crate::desktop::{
default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale, default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale,
}; };
use crate::gui; use crate::gui::{ItemProvider, MenuItem};
use crate::gui::MenuItem; use crate::{config, desktop, gui};
use anyhow::{Context, anyhow}; use anyhow::{Context, Error, anyhow};
use freedesktop_file_parser::EntryType; use freedesktop_file_parser::EntryType;
use gtk4::Image;
use libc::option;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::prelude::CommandExt; use std::os::unix::prelude::CommandExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@ -19,17 +23,22 @@ struct DRunCache {
run_count: usize, run_count: usize,
} }
/// # Errors #[derive(Clone)]
/// struct DRunProvider<T: std::clone::Clone> {
/// Will return `Err` if it was not able to spawn the process items: Vec<MenuItem<T>>,
pub fn d_run(config: &Config) -> anyhow::Result<()> { cache_path: Option<PathBuf>,
cache: HashMap<String, i64>,
}
impl<T: Clone> DRunProvider<T> {
fn new(menu_item_data: T) -> Self {
let locale_variants = get_locale_variants(); let locale_variants = get_locale_variants();
let default_icon = default_icon().unwrap_or_default(); let default_icon = default_icon().unwrap_or_default();
let (cache_path, mut d_run_cache) = load_d_run_cache(); let (cache_path, d_run_cache) = load_d_run_cache();
let mut entries: Vec<MenuItem<String>> = Vec::new(); let mut entries: Vec<MenuItem<T>> = Vec::new();
for file in find_desktop_files().ok().iter().flatten().filter(|f| { for file in find_desktop_files().iter().filter(|f| {
f.entry.hidden.is_none_or(|hidden| !hidden) f.entry.hidden.is_none_or(|hidden| !hidden)
&& f.entry.no_display.is_none_or(|no_display| !no_display) && f.entry.no_display.is_none_or(|no_display| !no_display)
}) { }) {
@ -73,7 +82,7 @@ pub fn d_run(config: &Config) -> anyhow::Result<()> {
log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}"); log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}");
let sort_score = d_run_cache.get(&name).unwrap_or(&0); let sort_score = d_run_cache.get(&name).unwrap_or(&0);
let mut entry: MenuItem<String> = MenuItem { let mut entry: MenuItem<T> = MenuItem {
label: name, label: name,
icon_path: icon.clone(), icon_path: icon.clone(),
action, action,
@ -81,7 +90,7 @@ pub fn d_run(config: &Config) -> anyhow::Result<()> {
working_dir: working_dir.clone(), working_dir: working_dir.clone(),
initial_sort_score: -(*sort_score), initial_sort_score: -(*sort_score),
search_sort_score: 0.0, search_sort_score: 0.0,
data: None, data: Some(menu_item_data.clone()),
}; };
file.actions.iter().for_each(|(_, action)| { file.actions.iter().for_each(|(_, action)| {
@ -117,29 +126,313 @@ pub fn d_run(config: &Config) -> anyhow::Result<()> {
gui::initialize_sort_scores(&mut entries); gui::initialize_sort_scores(&mut entries);
// todo ues a arc instead of cloning the config DRunProvider {
let selection_result = gui::show(config.clone(), entries.clone()); items: entries,
match selection_result { cache_path,
Ok(selected_item) => { cache: d_run_cache,
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:?}");
impl<T: std::clone::Clone> ItemProvider<T> for DRunProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> {
self.items.clone()
}
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> Option<Vec<MenuItem<T>>> {
None
}
}
#[derive(Debug, Clone, PartialEq)]
enum AutoRunType {
Math,
DRun,
File,
Ssh,
WebSearch,
Emoji,
Run,
}
#[derive(Clone)]
struct AutoItemProvider {
drun_provider: DRunProvider<AutoRunType>,
last_result: Option<Vec<MenuItem<AutoRunType>>>,
}
impl AutoItemProvider {
fn new() -> Self {
AutoItemProvider {
drun_provider: DRunProvider::new(AutoRunType::DRun),
last_result: None,
} }
} }
if let Some(action) = selected_item.action { fn auto_run_handle_files(&mut self, trimmed_search: &str) -> Vec<MenuItem<AutoRunType>> {
spawn_fork(&action, selected_item.working_dir.as_ref())?; let folder_icon = "inode-directory";
let path = config::expand_path(trimmed_search);
let mut items: Vec<MenuItem<AutoRunType>> = Vec::new();
if !path.exists() {
if let Some(last) = &self.last_result {
if !last.is_empty()
&& last.first().is_some_and(|l| {
l.as_ref()
.data
.as_ref()
.is_some_and(|t| t == &AutoRunType::File)
})
{
return last.clone();
} }
} }
Err(e) => {
log::error!("{e}"); return vec![];
}
if path.is_dir() {
for entry in path.read_dir().unwrap() {
if let Ok(entry) = entry {
let mut path_str = entry.path().to_str().unwrap_or("").to_string();
if trimmed_search.starts_with("~") {
if let Some(home_dir) = dirs::home_dir() {
path_str = path_str.replace(home_dir.to_str().unwrap_or(""), "~");
}
}
if entry.path().is_dir() {
path_str += "/";
}
items.push({
MenuItem {
label: path_str.clone(),
icon_path: if entry.path().is_dir() {
Some(folder_icon.to_owned())
} else {
Some(resolve_icon_for_name(entry.path()))
},
action: Some(format!("xdg-open {path_str}")),
sub_elements: vec![],
working_dir: None,
initial_sort_score: 0,
search_sort_score: 0.0,
data: Some(AutoRunType::File),
}
});
}
}
} else {
items.push({
MenuItem {
label: trimmed_search.to_owned(),
icon_path: Some(resolve_icon_for_name(PathBuf::from(trimmed_search))),
action: Some(format!("xdg-open {trimmed_search}")),
sub_elements: vec![],
working_dir: None,
initial_sort_score: 0,
search_sort_score: 0.0,
data: Some(AutoRunType::File),
}
});
}
self.last_result = Some(items.clone());
items
}
}
fn resolve_icon_for_name(path: PathBuf) -> String {
// todo use https://docs.rs/tree_magic_mini/latest/tree_magic_mini/ instead
if let Ok(metadata) = fs::symlink_metadata(&path) {
if metadata.file_type().is_symlink() {
return "inode-symlink".to_owned();
} else if metadata.is_dir() {
return "inode-directory".to_owned();
} else if metadata.permissions().mode() & 0o111 != 0 {
return "application-x-executable".to_owned();
}
}
let file_name = path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("")
.to_lowercase();
let extension = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
match extension.as_str() {
"sh" | "py" | "rb" | "pl" | "bash" => "text-x-script".to_owned(),
"c" | "cpp" | "rs" | "java" | "js" | "h" | "hpp" => "text-x-generic".to_owned(),
"txt" | "md" | "log" => "text-x-generic".to_owned(),
"html" | "htm" => "text-html".to_owned(),
"jpg" | "jpeg" | "png" | "gif" | "svg" | "webp" => "image-x-generic".to_owned(),
"mp3" | "wav" | "ogg" => "audio-x-generic".to_owned(),
"mp4" | "mkv" | "avi" => "video-x-generic".to_owned(),
"ttf" | "otf" | "woff" => "font-x-generic".to_owned(),
"zip" | "tar" | "gz" | "xz" | "7z" | "lz4" => "package-x-generic".to_owned(),
"deb" | "rpm" | "apk" => "x-package-repository".to_owned(),
"odt" => "x-office-document".to_owned(),
"ott" => "x-office-document-template".to_owned(),
"ods" => "x-office-spreadsheet".to_owned(),
"ots" => "x-office-spreadsheet-template".to_owned(),
"odp" => "x-office-presentation".to_owned(),
"otp" => "x-office-presentation-template".to_owned(),
"odg" => "x-office-drawing".to_owned(),
"vcf" => "x-office-addressbook".to_owned(),
_ => "application-x-generic".to_owned(),
}
}
fn contains_math_functions_or_starts_with_number(input: &str) -> bool {
// Regex for function names (word boundaries to match whole words)
let math_functions = r"\b(sqrt|abs|exp|ln|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|floor|ceil|round|signum|min|max|pi|e)\b";
// Regex for strings that start with a number (including decimals)
let starts_with_number = r"^\s*[+-]?(\d+(\.\d*)?|\.\d+)";
let math_regex = Regex::new(math_functions).unwrap();
let number_regex = Regex::new(starts_with_number).unwrap();
math_regex.is_match(input) || number_regex.is_match(input)
}
impl ItemProvider<AutoRunType> for AutoItemProvider {
fn get_elements(&mut self, search_opt: Option<&str>) -> Vec<MenuItem<AutoRunType>> {
if let Some(search) = search_opt {
let trimmed_search = search.trim();
if trimmed_search.is_empty() {
self.drun_provider.get_elements(search_opt)
} else if contains_math_functions_or_starts_with_number(trimmed_search) {
let result = match meval::eval_str(trimmed_search) {
Ok(result) => result.to_string(),
Err(e) => format!("failed to calculate {e:?}"),
};
let item = MenuItem {
label: result,
icon_path: None,
action: None,
sub_elements: vec![],
working_dir: None,
initial_sort_score: 0,
search_sort_score: 0.0,
data: Some(AutoRunType::Math),
};
return vec![item];
} else if trimmed_search.starts_with("$")
|| trimmed_search.starts_with("/")
|| trimmed_search.starts_with("~")
{
self.auto_run_handle_files(trimmed_search)
} else {
return self.drun_provider.get_elements(search_opt);
}
} else {
self.drun_provider.get_elements(search_opt)
}
}
fn get_sub_elements(
&mut self,
item: &MenuItem<AutoRunType>,
) -> Option<Vec<MenuItem<AutoRunType>>> {
Some(self.get_elements(Some(item.label.as_ref())))
}
}
/// # Errors
///
/// Will return `Err` if it was not able to spawn the process
pub fn d_run(config: &mut Config) -> anyhow::Result<()> {
let provider = DRunProvider::new("".to_owned());
let cache_path = provider.cache_path.clone();
let mut cache = provider.cache.clone();
if config.prompt.is_none() {
config.prompt = Some("drun".to_owned());
}
// 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)?;
}
Err(_) => {
log::error!("No item selected");
} }
} }
Ok(()) Ok(())
} }
pub fn auto(config: &mut Config) -> anyhow::Result<()> {
let provider = AutoItemProvider::new();
let cache_path = provider.drun_provider.cache_path.clone();
let mut cache = provider.drun_provider.cache.clone();
if config.prompt.is_none() {
config.prompt = Some("auto".to_owned());
}
// todo ues a arc instead of cloning the config
let selection_result = gui::show(config.clone(), provider);
match selection_result {
Ok(selection_result) => {
if let Some(data) = &selection_result.data {
match data {
AutoRunType::Math => {}
AutoRunType::DRun => {
update_drun_cache_and_run(cache_path, &mut cache, selection_result)?;
}
AutoRunType::File => {
if let Some(action) = selection_result.action {
spawn_fork(&action, selection_result.working_dir.as_ref())?
}
}
_ => {
todo!("not supported yet");
}
}
}
}
Err(_) => {
log::error!("No item selected");
}
}
Ok(())
}
fn update_drun_cache_and_run<T: Clone>(
cache_path: Option<PathBuf>,
cache: &mut HashMap<String, i64>,
selection_result: MenuItem<T>,
) -> Result<(), Error> {
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) {
log::warn!("cannot save drun cache {e:?}");
}
}
if let Some(action) = selection_result.action {
spawn_fork(&action, selection_result.working_dir.as_ref())
} else {
Err(anyhow::anyhow!("cannot find drun action"))
}
}
fn load_d_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) { fn load_d_run_cache() -> (Option<PathBuf>, HashMap<String, i64>) {
let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun"));
let d_run_cache = { let d_run_cache = {
@ -213,6 +506,7 @@ fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> {
.iter() .iter()
.skip(1) .skip(1)
.filter(|arg| !arg.starts_with('%')) .filter(|arg| !arg.starts_with('%'))
.map(|arg| expand_path(arg))
.collect(); .collect();
unsafe { unsafe {

View file

@ -1,6 +1,7 @@
use std::env; use std::env;
use anyhow::anyhow; use anyhow::anyhow;
use worf_lib::config::Mode;
use worf_lib::{config, mode}; use worf_lib::{config, mode};
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -12,19 +13,22 @@ fn main() -> anyhow::Result<()> {
.init(); .init();
let args = config::parse_args(); let args = config::parse_args();
let config = config::load_config(Some(args))?; let mut config = config::load_config(Some(args)).map_err(|e| anyhow!(e))?;
if let Some(show) = &config.show { if let Some(show) = &config.show {
match show { match show {
config::Mode::Run => { Mode::Run => {
todo!("run not implemented") todo!("run not implemented")
} }
config::Mode::Drun => { Mode::Drun => {
mode::d_run(&config)?; mode::d_run(&mut config)?;
} }
config::Mode::Dmenu => { Mode::Dmenu => {
todo!("dmenu not implemented") todo!("dmenu not implemented")
} }
Mode::Auto => {
mode::auto(&mut config)?;
}
} }
Ok(()) Ok(())

8
styles/dmenu/config.toml Normal file
View file

@ -0,0 +1,8 @@
image_size=0
columns=999
orientation="Horizontal"
row_bow_orientation="Horizontal"
content_halign="Center"
height="25"
width="100%"
valign="Start"

66
styles/dmenu/style.css Normal file
View file

@ -0,0 +1,66 @@
* {
font-family: DejaVu;
margin: 0;
padding: 0;
}
#window {
all: unset;
background-color: rgba(33, 33, 33, 1);
margin-left: 10px;
margin-right: 10px;
border-radius: 6px;
}
#window #outer-box {
/* The name of the search bar */
/* The name of the scrolled window containing all of the entries */
border: none;
}
#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);
font-size: 1rem;
}
#window #outer-box #scroll #inner-box #entry {
color: #fff;
background-color: rgba(32, 32, 32, 0.1);
border-radius: 0.5rem;
border-bottom: 5px solid rgba(32, 32, 32, 0.1);
}
#window #outer-box #scroll #inner-box #entry #img {
}
#window #outer-box #scroll #inner-box #entry:selected {
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
outline: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
#row:hover {
background-color: rgba(255, 255, 255, 0);
outline: inherit;
}
#window #outer-box #scroll #inner-box #entry:hover {
background-color: rgba(255, 255, 255, 0.1);
outline: inherit;
}
#label {
margin-top: 1rem;
margin-bottom: 0;
}

View file

@ -3,6 +3,6 @@ columns=6
orientation="Vertical" orientation="Vertical"
row_bow_orientation="Vertical" row_bow_orientation="Vertical"
content_halign="Center" content_halign="Center"
height="110%" height="105%"
width="100%" width="100%"
valign="Start" valign="Start"

View file

@ -6,7 +6,7 @@
all: unset; all: unset;
background-color: rgba(33, 33, 33, 0.76); /* Matches #212121BB */ background-color: rgba(33, 33, 33, 0.76); /* Matches #212121BB */
border-radius: 0; border-radius: 0;
padding-top: 5rem; padding-top: 3rem;
} }
#window #outer-box { #window #outer-box {
@ -17,7 +17,7 @@
color: #f2f2f2; color: #f2f2f2;
border-bottom: 2px solid rgba(214, 174, 0, 1); border-bottom: 2px solid rgba(214, 174, 0, 1);
font-size: 1rem; font-size: 1rem;
margin: 1rem 40rem; margin: 1rem 50rem 2rem;
} }
#window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active { #window #outer-box #input:focus, #window #outer-box #input:focus-visible, #window #outer-box #input:active {

View file

@ -0,0 +1,5 @@
image_size=48
columns=1
height="60%"
width="70%"
valign="Start"

68
styles/relaxed/style.css Normal file
View file

@ -0,0 +1,68 @@
* {
font-family: DejaVu;
}
#window {
all: unset;
background-color: rgba(33, 33, 33, 0.8); /* Matches #212121BB */
border-radius: 0;
}
#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 #inner-box #entry {
color: #fff;
background-color: rgba(32, 32, 32, 0.1);
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 {
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 {
background-color: rgba(255, 255, 255, 0);
outline: inherit;
}
#window #outer-box #scroll #inner-box #entry:hover {
background-color: rgba(255, 255, 255, 0.1);
outline: inherit;
}
#label {
margin-top: 1rem;
margin-bottom: 0;
}