From f55813123360a2dfbfa778a2cf644479c6c825cc Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 21 Apr 2025 02:08:38 +0200 Subject: [PATCH] add auto mode --- Cargo.lock | 23 ++ Cargo.toml | 1 + src/lib/config.rs | 102 ++++--- src/lib/desktop.rs | 142 +++++----- src/lib/gui.rs | 434 +++++++++++++++++++---------- src/lib/mode.rs | 504 +++++++++++++++++++++++++++------- src/main.rs | 14 +- styles/dmenu/config.toml | 8 + styles/dmenu/style.css | 66 +++++ styles/fullscreen/config.toml | 2 +- styles/fullscreen/style.css | 4 +- styles/relaxed/config.toml | 5 + styles/relaxed/style.css | 68 +++++ 13 files changed, 1011 insertions(+), 362 deletions(-) create mode 100644 styles/dmenu/config.toml create mode 100644 styles/dmenu/style.css create mode 100644 styles/relaxed/config.toml create mode 100644 styles/relaxed/style.css diff --git a/Cargo.lock b/Cargo.lock index ba645a0..7cabb2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "freedesktop-file-parser" version = "0.1.3" @@ -1013,6 +1019,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "meval" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" +dependencies = [ + "fnv", + "nom", +] + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1033,6 +1049,12 @@ dependencies = [ "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]] name = "num-traits" version = "0.2.19" @@ -1726,6 +1748,7 @@ dependencies = [ "hyprland", "libc", "log", + "meval", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 15ebdf3..42dfd2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ freedesktop-file-parser = "0.1.3" strsim = "0.11.1" dirs = "6.0.0" which = "7.0.3" +meval = "0.2.0" diff --git a/src/lib/config.rs b/src/lib/config.rs index b7d5e6c..c77310c 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -1,13 +1,28 @@ use std::path::PathBuf; 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 serde::{Deserialize, Serialize}; use serde_json::Value; 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)] pub enum MatchMethod { Fuzzy, @@ -46,6 +61,9 @@ pub enum Mode { /// reads from stdin and displays options which when selected will be output to stdout. Dmenu, + + /// tries to determine automatically what to do + Auto, } #[derive(Debug, Error)] @@ -62,6 +80,7 @@ impl FromStr for Mode { "run" => Ok(Mode::Run), "drun" => Ok(Mode::Drun), "dmenu" => Ok(Mode::Dmenu), + "auto" => Ok(Mode::Auto), _ => Err(ArgsError::InvalidParameter( 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) -> Result { 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) -> Result { + 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] pub fn path_alternatives(base_paths: Vec>, sub_path: &PathBuf) -> Vec { base_paths @@ -635,41 +665,28 @@ pub fn resolve_path( /// * 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().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, - ) - }); - +pub fn load_config(args_opt: Option) -> Result { + let config_path = conf_path(args_opt.as_ref().map(|c| c.config.clone()).flatten()); match config_path { - Some(path) => { - let toml_content = fs::read_to_string(path)?; - let mut config: Config = toml::from_str(&toml_content)?; + Ok(path) => { + let 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 { - 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() { match &merge_result.show { None => {} - Some(mode) => { - match mode { - Mode::Run => 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()), - } - } + Some(mode) => match mode { + Mode::Run => 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()), + _ => {} + }, } } @@ -678,9 +695,32 @@ pub fn load_config(args_opt: Option) -> Result { 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 /// diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 62b947b..0681c72 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use freedesktop_file_parser::DesktopFile; +use gdk4::Display; use gtk4::prelude::*; use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use home::home_dir; @@ -10,88 +11,71 @@ use std::path::Path; use std::path::PathBuf; use std::{env, fs, string}; -pub struct IconResolver { - cache: HashMap, +#[derive(Debug)] +pub enum DesktopError { + MissingIcon, } -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(&mut self, icon_name: &str) -> String { - if let Some(icon_path) = self.cache.get(icon_name) { - info!("Fetching {icon_name} from cache"); - return icon_path.to_owned(); - } - - info!("Loading icon for {icon_name}"); - let icon = fetch_icon_from_theme(icon_name) - .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 - .entry(icon_name.to_owned()) - .or_insert_with(|| icon.unwrap_or_default()) - .to_owned() - } -} +// +// #[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() -> anyhow::Result { - fetch_icon_from_theme("image-missing") +pub fn default_icon() -> Result { + fetch_icon_from_theme("image-missing").map_err(|e| DesktopError::MissingIcon) } -// 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 { +fn fetch_icon_from_theme(icon_name: &str) -> Result { let display = gtk4::gdk::Display::default(); if display.is_none() { 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 icon = theme.lookup_icon( icon_name, &[], @@ -106,17 +90,25 @@ fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result { .and_then(|file| file.path()) .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), } } -fn fetch_icon_from_common_dirs(icon_name: &str) -> Option { +pub fn fetch_icon_from_common_dirs(icon_name: &str) -> Result { let mut paths = vec![ PathBuf::from("/usr/local/share/icons"), PathBuf::from("/usr/share/icons"), PathBuf::from("/usr/share/pixmaps"), - // /usr/share/icons contains the theme icons, handled via separate function ]; if let Some(home) = home_dir() { @@ -133,6 +125,7 @@ fn fetch_icon_from_common_dirs(icon_name: &str) -> Option { find_file_case_insensitive(dir.as_path(), &formatted_name) .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> { @@ -156,7 +149,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option anyhow::Result> { +pub fn find_desktop_files() -> Vec { let mut paths = vec![ PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/local/share/applications"), @@ -168,6 +161,7 @@ pub fn find_desktop_files() -> anyhow::Result> { } if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") { + // todo use dirs:: instead paths.push(PathBuf::from(xdg_data_home).join(".applications")); } @@ -175,7 +169,7 @@ pub fn find_desktop_files() -> anyhow::Result> { 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 .into_iter() @@ -190,7 +184,7 @@ pub fn find_desktop_files() -> anyhow::Result> { }) }) .collect(); - Ok(p) + p } #[must_use] diff --git a/src/lib/gui.rs b/src/lib/gui.rs index f746d4e..9f36531 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::ops::DerefMut; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -19,15 +20,21 @@ use gtk4::{ ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk, }; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; -use gtk4_layer_shell::{KeyboardMode, LayerShell}; +use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell}; use log; use crate::config; use crate::config::{Animation, Config, MatchMethod}; type ArcMenuMap = Arc>>>; +type ArcProvider = Arc>>; type MenuItemSender = Sender, anyhow::Error>>; +pub trait ItemProvider { + fn get_elements(&mut self, search: Option<&str>) -> Vec>; + fn get_sub_elements(&mut self, item: &MenuItem) -> Option>>; +} + impl From for Orientation { fn from(orientation: config::Orientation) -> Self { match orientation { @@ -47,8 +54,8 @@ impl From for Align { } } -#[derive(Clone)] -pub struct MenuItem { +#[derive(Clone, PartialEq)] +pub struct MenuItem { pub label: String, // todo support empty label? pub icon_path: Option, pub action: Option, @@ -61,12 +68,19 @@ pub struct MenuItem { pub data: Option, } +impl AsRef> for MenuItem { + fn as_ref(&self) -> &MenuItem { + self + } +} + /// # Errors /// /// Will return Err when the channel between the UI and this is broken -pub fn show(config: Config, elements: Vec>) -> Result, anyhow::Error> +pub fn show(config: Config, item_provider: P) -> Result, anyhow::Error> where T: Clone + 'static, + P: ItemProvider + 'static + Clone, { if let Some(ref css) = config.style { let provider = CssProvider::new(); @@ -85,7 +99,7 @@ where let (sender, receiver) = channel::bounded(1); app.connect_activate(move |app| { - build_ui(&config, &elements, &sender, app); + build_ui(&config, item_provider.clone(), &sender, app); }); let gtk_args: [&str; 0] = []; @@ -93,13 +107,14 @@ where receiver.recv()? } -fn build_ui( +fn build_ui( config: &Config, - elements: &Vec>, + item_provider: P, sender: &Sender, anyhow::Error>>, app: &Application, ) where T: Clone + 'static, + P: ItemProvider + 'static, { let window = ApplicationWindow::builder() .application(app) @@ -119,6 +134,9 @@ fn build_ui( 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); outer_box.set_widget_name("outer-box"); @@ -126,7 +144,7 @@ fn build_ui( entry.set_widget_name("input"); entry.set_css_classes(&["input"]); entry.set_placeholder_text(config.prompt.as_deref()); - entry.set_sensitive(false); + entry.set_can_focus(false); outer_box.append(&entry); let scroll = ScrolledWindow::new(); @@ -145,6 +163,7 @@ fn build_ui( inner_box.set_css_classes(&["inner-box"]); inner_box.set_hexpand(true); inner_box.set_vexpand(false); + if let Some(halign) = config.halign { inner_box.set_halign(halign.into()); } @@ -161,27 +180,29 @@ fn build_ui( inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_activate_on_single_click(true); + let item_provider = Arc::new(Mutex::new(item_provider)); 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, &list_items, app, &window), - entry.clone(), - ); - } + build_ui_from_menu_items( + &item_provider.lock().unwrap().get_elements(None), + &list_items, + &inner_box, + &config, + &sender, + &app, + &window, + ); - let items_sort = Arc::>>>::clone(&list_items); - inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_sort)); + let items_sort = ArcMenuMap::clone(&list_items); + inner_box.set_sort_func(move |child1, child2| { + sort_menu_items_by_score(child1, child2, items_sort.clone()) + }); - let items_focus = Arc::>>>::clone(&list_items); + let items_focus = ArcMenuMap::clone(&list_items); inner_box.connect_map(move |fb| { fb.grab_focus(); fb.invalidate_sort(); - let mut item_lock = items_focus.lock().unwrap(); - select_first_visible_child(&mut *item_lock, fb); + select_first_visible_child(&items_focus, fb); }); let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0); @@ -194,8 +215,9 @@ fn build_ui( inner_box, app.clone(), sender.clone(), - Arc::>>>::clone(&list_items), + ArcMenuMap::clone(&list_items), config.clone(), + item_provider, ); window.set_child(Widget::NONE); @@ -203,66 +225,169 @@ fn build_ui( animate_window_show(config.clone(), window.clone(), outer_box); } +fn build_ui_from_menu_items( + items: &Vec>, + list_items: &ArcMenuMap, + inner_box: &FlowBox, + config: &Config, + sender: &MenuItemSender, + 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( window: &ApplicationWindow, - entry_clone: SearchEntry, + entry: SearchEntry, inner_box: FlowBox, app: Application, sender: MenuItemSender, list_items: Arc>>>, config: Config, + item_provider: ArcProvider, ) { let key_controller = EventControllerKey::new(); let window_clone = window.clone(); + let entry_clone = entry.clone(); 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}"); - } - close_gui(app.clone(), window_clone.clone(), &config); - } - Key::Return => { - if let Err(e) = handle_selected_item( - &sender, - app.clone(), - window_clone.clone(), - &config, - &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 + handle_key_press( + &entry_clone, + &inner_box, + &app, + &sender, + &list_items, + &config, + &item_provider, + &window_clone, + &key_value, + ) }); + window.add_controller(key_controller); } -fn sort_menu_items( +fn handle_key_press( + search_entry: &SearchEntry, + inner_box: &FlowBox, + app: &Application, + sender: &MenuItemSender, + list_items: &ArcMenuMap, + config: &Config, + item_provider: &ArcProvider, + window_clone: &ApplicationWindow, + keyboard_key: &Key, +) -> Propagation { + let update_view = |query: &String, items: Vec>| { + 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"))) { + log::error!("failed to send message {e}"); + } + close_gui(app.clone(), window_clone.clone(), &config); + } + &Key::Return => { + if let Err(e) = handle_selected_item( + &sender, + app.clone(), + window_clone.clone(), + &config, + &inner_box, + &list_items, + ) { + log::error!("{e}"); + } + } + &Key::BackSpace => { + let mut query = search_entry.text().to_string(); + query.pop(); + + search_entry.set_text(&query); + 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::().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; + } + _ => { + if let Some(c) = keyboard_key.to_unicode() { + let current = search_entry.text().to_string(); + let query = format!("{current}{c}"); + search_entry.set_text(&query); + update_view_from_provider(&query); + } + } + } + + Propagation::Proceed +} + +fn sort_menu_items_by_score( child1: &FlowBoxChild, child2: &FlowBoxChild, - items_lock: &Mutex>>, + items_lock: ArcMenuMap, ) -> Ordering { let lock = items_lock.lock().unwrap(); 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); if let Some(monitor) = monitor { 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 { return; }; let Some(target_height) = - percent_or_absolute(&config.height.unwrap(), geometry.height()) + percent_or_absolute(config.height.as_ref(), geometry.height()) else { return; }; animate_window( window.clone(), - config.show_animation.unwrap(), - config.show_animation_time.unwrap(), + config.show_animation.unwrap_or(Animation::None), + config.show_animation_time.unwrap_or(0), target_height, target_width, move || { @@ -348,8 +473,8 @@ where animate_window( window, - config.hide_animation.unwrap(), - config.hide_animation_time.unwrap(), + config.hide_animation.unwrap_or(Animation::None), + config.hide_animation_time.unwrap_or(0), target_h, target_w, on_done_func, @@ -563,7 +688,7 @@ fn add_menu_item( create_menu_row( entry_element, config, - Arc::>>>::clone(lock_arc), + ArcMenuMap::clone(lock_arc), sender.clone(), app.clone(), window.clone(), @@ -579,7 +704,7 @@ fn add_menu_item( let menu_row = create_menu_row( entry_element, config, - Arc::>>>::clone(lock_arc), + ArcMenuMap::clone(lock_arc), sender.clone(), app.clone(), window.clone(), @@ -595,7 +720,7 @@ fn add_menu_item( let sub_row = create_menu_row( sub_item, config, - Arc::>>>::clone(lock_arc), + ArcMenuMap::clone(lock_arc), sender.clone(), app.clone(), window.clone(), @@ -659,7 +784,13 @@ fn create_menu_row( 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_vexpand(false); row_box.set_halign(Align::Fill); @@ -690,86 +821,97 @@ fn create_menu_row( label.set_wrap(true); row_box.append(&label); - if config.content_halign.unwrap() == config::Align::Start - || config.content_halign.unwrap() == config::Align::Fill + if config + .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); } row.upcast() } -fn filter_widgets( +fn filter_widgets( query: &str, - items: &mut HashMap>, + item_arc: &ArcMenuMap, config: &Config, inner_box: &FlowBox, ) { - if items.is_empty() { - for (child, _) in items.iter() { - 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); + { + let mut items = item_arc.lock().unwrap(); + if items.is_empty() { + for (child, _) in items.iter() { + child.set_visible(true); } - } - return; - } - let query = query.to_owned().to_lowercase(); - for (flowbox_child, menu_item) in items.iter_mut() { - 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) + 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); } } - MatchMethod::MultiContains => { - let score = query - .split(' ') - .filter(|i| menu_item_search.contains(i)) - .map(|_| 1.0) - .sum(); - (score, score > 0.0) - } - }; + return; + } - menu_item.search_sort_score = search_sort_score; - flowbox_child.set_visible(visible); + let query = query.to_owned().to_lowercase(); + for (flowbox_child, menu_item) in items.iter_mut() { + 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_or(config::default_fuzzy_min_score().unwrap_or(0.0)), + ) + } + 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; + flowbox_child.set_visible(visible); + } } - select_first_visible_child(items, inner_box); + inner_box.invalidate_sort(); } -fn select_first_visible_child( - items: &mut HashMap>, - inner_box: &FlowBox, -) { +fn select_first_visible_child(lock: &ArcMenuMap, inner_box: &FlowBox) { + let items = lock.lock().unwrap(); for i in 0..items.len() { let i_32 = i.try_into().unwrap_or(i32::MAX); if let Some(child) = inner_box.child_at_index(i_32) { @@ -784,21 +926,25 @@ fn select_first_visible_child( // 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, +fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option { + if let Some(value) = value { + 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() } } else { - value.parse::().ok() + None } } // highly unlikely that we are dealing with > i64 items #[allow(clippy::cast_possible_wrap)] -pub fn initialize_sort_scores(items: &mut [MenuItem]) { +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)); diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 27ec831..9973c7c 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,13 +1,17 @@ -use crate::config::Config; +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::MenuItem; -use anyhow::{Context, anyhow}; +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}; @@ -19,127 +23,416 @@ struct DRunCache { run_count: usize, } -/// # 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().unwrap_or_default(); +#[derive(Clone)] +struct DRunProvider { + items: Vec>, + cache_path: Option, + cache: HashMap, +} - let (cache_path, mut d_run_cache) = load_d_run_cache(); +impl DRunProvider { + fn new(menu_item_data: T) -> Self { + let locale_variants = get_locale_variants(); + let default_icon = default_icon().unwrap_or_default(); - let mut entries: Vec> = Vec::new(); - 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 Some(name) = lookup_name_with_locale( - &locale_variants, - &file.entry.name.variants, - &file.entry.name.default, - ) else { - log::warn!("Skipping desktop entry without name {file:?}"); - continue; - }; + let (cache_path, d_run_cache) = load_d_run_cache(); - let (action, working_dir) = match &file.entry.entry_type { - EntryType::Application(app) => (app.exec.clone(), app.path.clone()), - _ => (None, None), - }; - - let cmd_exists = action - .as_ref() - .and_then(|a| { - a.split(' ') - .next() - .map(|cmd| cmd.replace('"', "")) - .map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok()) - }) - .unwrap_or(false); - - if !cmd_exists { - log::warn!( - "Skipping desktop entry for {name:?} because action {action:?} does not exist" - ); - 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)| { - if let Some(action_name) = lookup_name_with_locale( + let mut entries: Vec> = Vec::new(); + for file in find_desktop_files().iter().filter(|f| { + f.entry.hidden.is_none_or(|hidden| !hidden) + && f.entry.no_display.is_none_or(|no_display| !no_display) + }) { + let Some(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.clone()); + &file.entry.name.variants, + &file.entry.name.default, + ) else { + log::warn!("Skipping desktop entry without name {file:?}"); + continue; + }; - log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); + let (action, working_dir) = match &file.entry.entry_type { + EntryType::Application(app) => (app.exec.clone(), app.path.clone()), + _ => (None, None), + }; - 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); - } - }); + let cmd_exists = action + .as_ref() + .and_then(|a| { + a.split(' ') + .next() + .map(|cmd| cmd.replace('"', "")) + .map(|cmd| PathBuf::from(&cmd).exists() || which::which(&cmd).is_ok()) + }) + .unwrap_or(false); - entries.push(entry); + if !cmd_exists { + log::warn!( + "Skipping desktop entry for {name:?} because action {action:?} does not exist" + ); + 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: Some(menu_item_data.clone()), + }; + + file.actions.iter().for_each(|(_, action)| { + 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.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); + } + + gui::initialize_sort_scores(&mut entries); + + DRunProvider { + items: entries, + cache_path, + cache: d_run_cache, + } + } +} + +impl ItemProvider for DRunProvider { + fn get_elements(&mut self, _: Option<&str>) -> Vec> { + self.items.clone() } - gui::initialize_sort_scores(&mut entries); + fn get_sub_elements(&mut self, item: &MenuItem) -> Option>> { + None + } +} - // 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:?}"); +#[derive(Debug, Clone, PartialEq)] +enum AutoRunType { + Math, + DRun, + File, + Ssh, + WebSearch, + Emoji, + Run, +} + +#[derive(Clone)] +struct AutoItemProvider { + drun_provider: DRunProvider, + last_result: Option>>, +} + +impl AutoItemProvider { + fn new() -> Self { + AutoItemProvider { + drun_provider: DRunProvider::new(AutoRunType::DRun), + + last_result: None, + } + } + + fn auto_run_handle_files(&mut self, trimmed_search: &str) -> Vec> { + let folder_icon = "inode-directory"; + + let path = config::expand_path(trimmed_search); + let mut items: Vec> = 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(); } } - if let Some(action) = selected_item.action { - spawn_fork(&action, selected_item.working_dir.as_ref())?; - } + return vec![]; } - Err(e) => { - log::error!("{e}"); + + 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 for AutoItemProvider { + fn get_elements(&mut self, search_opt: Option<&str>) -> Vec> { + 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, + ) -> Option>> { + 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(()) } +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( + cache_path: Option, + cache: &mut HashMap, + selection_result: MenuItem, +) -> 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, HashMap) { let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); let d_run_cache = { @@ -213,6 +506,7 @@ fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> { .iter() .skip(1) .filter(|arg| !arg.starts_with('%')) + .map(|arg| expand_path(arg)) .collect(); unsafe { diff --git a/src/main.rs b/src/main.rs index 8be9da2..4b7bd37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::env; use anyhow::anyhow; +use worf_lib::config::Mode; use worf_lib::{config, mode}; fn main() -> anyhow::Result<()> { @@ -12,19 +13,22 @@ fn main() -> anyhow::Result<()> { .init(); 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 { match show { - config::Mode::Run => { + Mode::Run => { todo!("run not implemented") } - config::Mode::Drun => { - mode::d_run(&config)?; + Mode::Drun => { + mode::d_run(&mut config)?; } - config::Mode::Dmenu => { + Mode::Dmenu => { todo!("dmenu not implemented") } + Mode::Auto => { + mode::auto(&mut config)?; + } } Ok(()) diff --git a/styles/dmenu/config.toml b/styles/dmenu/config.toml new file mode 100644 index 0000000..2a986cb --- /dev/null +++ b/styles/dmenu/config.toml @@ -0,0 +1,8 @@ +image_size=0 +columns=999 +orientation="Horizontal" +row_bow_orientation="Horizontal" +content_halign="Center" +height="25" +width="100%" +valign="Start" diff --git a/styles/dmenu/style.css b/styles/dmenu/style.css new file mode 100644 index 0000000..4a68708 --- /dev/null +++ b/styles/dmenu/style.css @@ -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; +} diff --git a/styles/fullscreen/config.toml b/styles/fullscreen/config.toml index 77c56d8..519d540 100644 --- a/styles/fullscreen/config.toml +++ b/styles/fullscreen/config.toml @@ -3,6 +3,6 @@ columns=6 orientation="Vertical" row_bow_orientation="Vertical" content_halign="Center" -height="110%" +height="105%" width="100%" valign="Start" diff --git a/styles/fullscreen/style.css b/styles/fullscreen/style.css index d6288ac..434cf10 100644 --- a/styles/fullscreen/style.css +++ b/styles/fullscreen/style.css @@ -6,7 +6,7 @@ all: unset; background-color: rgba(33, 33, 33, 0.76); /* Matches #212121BB */ border-radius: 0; - padding-top: 5rem; + padding-top: 3rem; } #window #outer-box { @@ -17,7 +17,7 @@ color: #f2f2f2; border-bottom: 2px solid rgba(214, 174, 0, 1); 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 { diff --git a/styles/relaxed/config.toml b/styles/relaxed/config.toml new file mode 100644 index 0000000..dc12027 --- /dev/null +++ b/styles/relaxed/config.toml @@ -0,0 +1,5 @@ +image_size=48 +columns=1 +height="60%" +width="70%" +valign="Start" diff --git a/styles/relaxed/style.css b/styles/relaxed/style.css new file mode 100644 index 0000000..435aa23 --- /dev/null +++ b/styles/relaxed/style.css @@ -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; +}