diff --git a/Cargo.toml b/Cargo.toml index 6d37605..88c38ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,4 @@ members = [ "rmenu-plugin", "plugin-run", "plugin-desktop", - "plugin-desktop2", ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ba7794 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +# RMenu Installation/Deployment Configuration + +CARGO=cargo +FLAGS=--release + +DEST=$(HOME)/.config/rmenu + +install: build deploy + +deploy: + mkdir -p ${DEST} + cp -vf ./target/release/desktop ${DEST}/drun + cp -vf ./target/release/run ${DEST}/run + cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml + cp -vf ./rmenu/public/default.css ${DEST}/style.css + +build: build-rmenu build-plugins + +build-rmenu: + ${CARGO} build -p rmenu ${FLAGS} + +build-plugins: + ${CARGO} build -p run ${FLAGS} + ${CARGO} build -p desktop ${FLAGS} diff --git a/plugin-desktop/Cargo.toml b/plugin-desktop/Cargo.toml index 956c433..442dd79 100644 --- a/plugin-desktop/Cargo.toml +++ b/plugin-desktop/Cargo.toml @@ -7,9 +7,13 @@ edition = "2021" [dependencies] freedesktop-desktop-entry = "0.5.0" +freedesktop-icons = "0.2.3" +log = "0.4.19" +once_cell = "1.18.0" regex = "1.9.1" rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } -rust-ini = { version = "0.19.0", features = ["unicase"] } -serde_json = "1.0.103" +rust-ini = "0.19.0" +serde_json = "1.0.104" shellexpand = "3.1.0" +thiserror = "1.0.44" walkdir = "2.3.3" diff --git a/plugin-desktop2/src/icons.rs b/plugin-desktop/src/icons.rs similarity index 100% rename from plugin-desktop2/src/icons.rs rename to plugin-desktop/src/icons.rs diff --git a/plugin-desktop/src/main.rs b/plugin-desktop/src/main.rs index 0cbb306..57497fc 100644 --- a/plugin-desktop/src/main.rs +++ b/plugin-desktop/src/main.rs @@ -1,18 +1,20 @@ -use std::collections::{HashMap, HashSet}; +use std::fs::read_to_string; use std::path::PathBuf; -use std::{fs::read_to_string, path::Path}; -use freedesktop_desktop_entry::DesktopEntry; -use ini::Ini; +use freedesktop_desktop_entry::{DesktopEntry, Iter}; +use once_cell::sync::Lazy; use regex::Regex; use rmenu_plugin::{Action, Entry, Method}; -use walkdir::WalkDir; + +mod icons; static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS"; static XDG_CONFIG_ENV: &'static str = "XDG_CONFIG_HOME"; static XDG_DATA_DEFAULT: &'static str = "/usr/share:/usr/local/share"; static XDG_CONFIG_DEFAULT: &'static str = "~/.config"; -static DEFAULT_THEME: &'static str = "hicolor"; + +static EXEC_RGX: Lazy = + Lazy::new(|| Regex::new(r"%\w").expect("Failed to Build Exec Regex")); /// Retrieve XDG-CONFIG-HOME Directory #[inline] @@ -21,140 +23,6 @@ fn config_dir() -> PathBuf { PathBuf::from(shellexpand::tilde(&path).to_string()) } -/// Determine XDG Icon Theme based on Preexisting Configuration Files -fn find_theme(cfgdir: &PathBuf) -> Vec { - let mut themes: Vec = vec![ - ("kdeglobals", "Icons", "Theme"), - ("gtk-4.0/settings.ini", "Settings", "gtk-icon-theme-name"), - ("gtk-3.0/settings.ini", "Settings", "gtk-icon-theme-name"), - ] - .into_iter() - .filter_map(|(path, sec, key)| { - let path = cfgdir.join(path); - let ini = Ini::load_from_file(path).ok()?; - ini.get_from(Some(sec), key).map(|s| s.to_string()) - }) - .collect(); - let default = DEFAULT_THEME.to_string(); - if !themes.contains(&default) { - themes.push(default); - } - themes -} - -type IconGroup = HashMap; -type Icons = HashMap; - -/// Precalculate prefferred sizes folders -fn calculate_sizes(range: (usize, usize, usize)) -> HashSet { - let (min, preffered, max) = range; - let mut size = preffered.clone(); - let mut sizes = HashSet::new(); - while size < max { - sizes.insert(format!("{size}x{size}")); - sizes.insert(format!("{size}x{size}@2")); - size *= 2; - } - // attempt to match sizes down to lowest minimum - let mut size = preffered.clone(); - while size > min { - sizes.insert(format!("{size}x{size}")); - sizes.insert(format!("{size}x{size}@2")); - size /= 2; - } - sizes -} - -#[inline(always)] -fn is_valid_icon(name: &str) -> bool { - name.ends_with(".png") || name.ends_with(".svg") -} - -/// Parse Icon-Name from Filename -#[inline] -fn icon_name(name: &str) -> String { - name.rsplit_once(".") - .map(|(i, _)| i) - .unwrap_or(&name) - .to_owned() -} - -/// Parse and Categorize Icons Within the Specified Path -fn find_icons(path: &PathBuf, sizes: (usize, usize, usize)) -> Vec { - let sizes = calculate_sizes(sizes); - let mut extras = IconGroup::new(); - let icons: Icons = WalkDir::new(path) - // collect list of directories of icon subdirs - .max_depth(1) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter_map(|e| { - let name = e.file_name().to_str()?; - let path = e.path().to_owned(); - match e.file_type().is_dir() { - true => Some((name.to_owned(), path)), - false => { - if is_valid_icon(name) { - extras.insert(icon_name(name), path); - } - None - } - } - }) - // iterate content within subdirs - .map(|(name, path)| { - let group = WalkDir::new(path) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .filter_map(|e| { - let name = e.file_name().to_str()?; - if is_valid_icon(name) { - return Some((icon_name(name), e.path().to_owned())); - } - None - }) - .collect(); - (name, group) - }) - .collect(); - // organize icon groups according to prefference - let mut priority = vec![]; - let mut others = vec![]; - icons - .into_iter() - .map(|(folder, group)| match sizes.contains(&folder) { - true => priority.push(group), - false => match folder.contains("x") { - false => others.push(group), - _ => {} - }, - }) - .last(); - priority.append(&mut others); - priority.push(extras); - priority -} - -/// Retrieve Extras in Base Icon Directories -fn find_icon_extras(path: &PathBuf) -> IconGroup { - WalkDir::new(path) - .max_depth(1) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .filter_map(|e| { - let name = e.file_name().to_str()?; - if is_valid_icon(name) { - return Some((icon_name(&name), e.path().to_owned())); - } - None - }) - .collect() -} - /// Retrieve XDG-DATA Directories fn data_dirs(dir: &str) -> Vec { std::env::var(XDG_DATA_ENV) @@ -167,23 +35,24 @@ fn data_dirs(dir: &str) -> Vec { .collect() } +/// Modify Exec Statements to Remove %u/%f/etc... #[inline(always)] -fn fix_exec(exec: &str, ematch: &Regex) -> String { - ematch.replace_all(exec, "").trim().to_string() +fn fix_exec(exec: &str) -> String { + EXEC_RGX.replace_all(exec, "").trim().to_string() } /// Parse XDG Desktop Entry into RMenu Entry -fn parse_desktop(path: &Path, locale: Option<&str>, ematch: &Regex) -> Option { +fn parse_desktop(path: &PathBuf, locale: Option<&str>) -> Option { let bytes = read_to_string(path).ok()?; let entry = DesktopEntry::decode(&path, &bytes).ok()?; let name = entry.name(locale)?.to_string(); - let icon = entry.icon().map(|s| s.to_string()); + let icon = entry.icon().map(|i| i.to_string()); let comment = entry.comment(locale).map(|s| s.to_string()); let terminal = entry.terminal(); let mut actions = match entry.exec() { Some(exec) => vec![Action { name: "main".to_string(), - exec: Method::new(fix_exec(exec, ematch), terminal), + exec: Method::new(fix_exec(exec), terminal), comment: None, }], None => vec![], @@ -200,7 +69,7 @@ fn parse_desktop(path: &Path, locale: Option<&str>, ematch: &Regex) -> Option, ematch: &Regex) -> Option, ematch: &Regex) -> Vec { - WalkDir::new(path) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_name().to_string_lossy().ends_with(".desktop")) - .filter(|e| e.file_type().is_file()) - .filter_map(|e| parse_desktop(e.path(), locale, ematch)) - .collect() -} - -/// Find and Assign Icons from Icon-Cache when Possible -fn assign_icons(icons: &Vec, mut e: Entry) -> Entry { - if let Some(name) = e.icon.as_ref() { - if !name.contains("/") { - if let Some(path) = icons.iter().find_map(|i| i.get(name)) { - if let Some(fpath) = path.to_str() { - e.icon = Some(fpath.to_owned()); - } +/// Assign XDG Icon based on Desktop-Entry +fn assign_icon(icon: String, map: &icons::IconMap) -> Option { + if !icon.contains("/") { + if let Some(icon) = map.get(&icon) { + if let Some(path) = icon.to_str() { + return Some(path.to_owned()); } } } - e + Some(icon) } fn main() { let locale = Some("en"); - let sizes = (32, 64, 128); - // build regex desktop formatter args - let ematch = regex::Regex::new(r"%\w").expect("Failed Regex Compile"); - // build a collection of icons for configured themes - let cfgdir = config_dir(); - let themes = find_theme(&cfgdir); - let icon_paths = data_dirs("icons"); - let mut icons: Vec = icon_paths - // generate list of icon-paths that exist - .iter() - .map(|d| themes.iter().map(|t| d.join(t))) - .flatten() - .filter(|t| t.exists()) - // append icon-paths within supported themes - .map(|t| find_icons(&t, sizes)) - .flatten() - .collect(); - // add extra icons found in base folders - icons.extend(icon_paths.iter().map(|p| find_icon_extras(p))); - // retrieve desktop applications and sort alphabetically - let mut applications: Vec = data_dirs("applications") + let sizes = vec![64, 32, 96, 22, 128]; + + // collect icons + let cfg = config_dir(); + let spec = icons::IconSpec::standard(&cfg, sizes); + let icons = icons::collect_icons(spec); + + // collect applications + let app_paths = data_dirs("applications"); + let mut desktops: Vec = Iter::new(app_paths) .into_iter() - .map(|p| find_desktops(p, locale, &ematch)) - .flatten() + .filter_map(|f| parse_desktop(&f, locale)) + .map(|mut e| { + e.icon = e.icon.and_then(|s| assign_icon(s, &icons)); + e + }) .collect(); - applications.sort_by_cached_key(|e| e.name.to_owned()); - // assign icons and print results - applications + + desktops.sort_by_cached_key(|e| e.name.to_owned()); + desktops .into_iter() - .map(|e| assign_icons(&icons, e)) .filter_map(|e| serde_json::to_string(&e).ok()) .map(|s| println!("{}", s)) .last(); diff --git a/plugin-desktop2/Cargo.toml b/plugin-desktop2/Cargo.toml deleted file mode 100644 index 23f8f5c..0000000 --- a/plugin-desktop2/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "desktop2" -version = "0.0.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -freedesktop-desktop-entry = "0.5.0" -freedesktop-icons = "0.2.3" -log = "0.4.19" -once_cell = "1.18.0" -regex = "1.9.1" -rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } -rust-ini = "0.19.0" -serde_json = "1.0.104" -shellexpand = "3.1.0" -thiserror = "1.0.44" -walkdir = "2.3.3" diff --git a/plugin-desktop2/src/main.rs b/plugin-desktop2/src/main.rs deleted file mode 100644 index 57497fc..0000000 --- a/plugin-desktop2/src/main.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::fs::read_to_string; -use std::path::PathBuf; - -use freedesktop_desktop_entry::{DesktopEntry, Iter}; -use once_cell::sync::Lazy; -use regex::Regex; -use rmenu_plugin::{Action, Entry, Method}; - -mod icons; - -static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS"; -static XDG_CONFIG_ENV: &'static str = "XDG_CONFIG_HOME"; -static XDG_DATA_DEFAULT: &'static str = "/usr/share:/usr/local/share"; -static XDG_CONFIG_DEFAULT: &'static str = "~/.config"; - -static EXEC_RGX: Lazy = - Lazy::new(|| Regex::new(r"%\w").expect("Failed to Build Exec Regex")); - -/// Retrieve XDG-CONFIG-HOME Directory -#[inline] -fn config_dir() -> PathBuf { - let path = std::env::var(XDG_CONFIG_ENV).unwrap_or_else(|_| XDG_CONFIG_DEFAULT.to_string()); - PathBuf::from(shellexpand::tilde(&path).to_string()) -} - -/// Retrieve XDG-DATA Directories -fn data_dirs(dir: &str) -> Vec { - std::env::var(XDG_DATA_ENV) - .unwrap_or_else(|_| XDG_DATA_DEFAULT.to_string()) - .split(":") - .map(|p| shellexpand::tilde(p).to_string()) - .map(PathBuf::from) - .map(|p| p.join(dir.to_owned())) - .filter(|p| p.exists()) - .collect() -} - -/// Modify Exec Statements to Remove %u/%f/etc... -#[inline(always)] -fn fix_exec(exec: &str) -> String { - EXEC_RGX.replace_all(exec, "").trim().to_string() -} - -/// Parse XDG Desktop Entry into RMenu Entry -fn parse_desktop(path: &PathBuf, locale: Option<&str>) -> Option { - let bytes = read_to_string(path).ok()?; - let entry = DesktopEntry::decode(&path, &bytes).ok()?; - let name = entry.name(locale)?.to_string(); - let icon = entry.icon().map(|i| i.to_string()); - let comment = entry.comment(locale).map(|s| s.to_string()); - let terminal = entry.terminal(); - let mut actions = match entry.exec() { - Some(exec) => vec![Action { - name: "main".to_string(), - exec: Method::new(fix_exec(exec), terminal), - comment: None, - }], - None => vec![], - }; - actions.extend( - entry - .actions() - .unwrap_or("") - .split(";") - .into_iter() - .filter(|a| a.len() > 0) - .filter_map(|a| { - let name = entry.action_name(a, locale)?; - let exec = entry.action_exec(a)?; - Some(Action { - name: name.to_string(), - exec: Method::new(fix_exec(exec), terminal), - comment: None, - }) - }), - ); - Some(Entry { - name, - actions, - comment, - icon, - }) -} - -/// Assign XDG Icon based on Desktop-Entry -fn assign_icon(icon: String, map: &icons::IconMap) -> Option { - if !icon.contains("/") { - if let Some(icon) = map.get(&icon) { - if let Some(path) = icon.to_str() { - return Some(path.to_owned()); - } - } - } - Some(icon) -} - -fn main() { - let locale = Some("en"); - let sizes = vec![64, 32, 96, 22, 128]; - - // collect icons - let cfg = config_dir(); - let spec = icons::IconSpec::standard(&cfg, sizes); - let icons = icons::collect_icons(spec); - - // collect applications - let app_paths = data_dirs("applications"); - let mut desktops: Vec = Iter::new(app_paths) - .into_iter() - .filter_map(|f| parse_desktop(&f, locale)) - .map(|mut e| { - e.icon = e.icon.and_then(|s| assign_icon(s, &icons)); - e - }) - .collect(); - - desktops.sort_by_cached_key(|e| e.name.to_owned()); - desktops - .into_iter() - .filter_map(|e| serde_json::to_string(&e).ok()) - .map(|s| println!("{}", s)) - .last(); -}