From f8d0ce85eaf03502aa557ea0c6822a8ad82ee00f Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Fri, 28 Jul 2023 23:07:06 -0700 Subject: [PATCH] feat: icons implemented --- plugin-desktop/Cargo.toml | 1 + plugin-desktop/src/main.rs | 157 ++++++++++++++++++++++++++----------- 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/plugin-desktop/Cargo.toml b/plugin-desktop/Cargo.toml index baa4064..1936159 100644 --- a/plugin-desktop/Cargo.toml +++ b/plugin-desktop/Cargo.toml @@ -11,4 +11,5 @@ rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } rust-ini = { version = "0.19.0", features = ["unicase"] } serde_json = "1.0.103" shellexpand = "3.1.0" +thiserror = "1.0.44" walkdir = "2.3.3" diff --git a/plugin-desktop/src/main.rs b/plugin-desktop/src/main.rs index 42b23ab..4f4209f 100644 --- a/plugin-desktop/src/main.rs +++ b/plugin-desktop/src/main.rs @@ -1,10 +1,11 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::{fs::read_to_string, path::Path}; use freedesktop_desktop_entry::DesktopEntry; use ini::Ini; use rmenu_plugin::{Action, Entry}; +use thiserror::Error; use walkdir::WalkDir; static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS"; @@ -13,35 +14,71 @@ static XDG_DATA_DEFAULT: &'static str = "/usr/share:/usr/local/share"; static XDG_CONFIG_DEFAULT: &'static str = "~/.config"; static DEFAULT_THEME: &'static str = "hicolor"; +#[derive(Error, Debug)] +enum ProcessError { + #[error("Failed to Read Desktop File")] + FileError(#[from] std::io::Error), + #[error("Invalid Desktop File")] + InvalidFile(#[from] freedesktop_desktop_entry::DecodeError), + #[error("No Such Attribute")] + InvalidAttr(&'static str), +} + /// Retrieve XDG-CONFIG-HOME Directory #[inline] -fn config_dir(dir: &str) -> PathBuf { +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()) } /// Determine XDG Icon Theme based on Preexisting Configuration Files -fn find_theme(cfgdir: &PathBuf) -> String { - vec![ +fn find_theme(cfgdir: &PathBuf) -> Vec { + let mut themes: Vec = vec![ ("kdeglobals", "Icons", "Theme"), - ("gtk-3.0/settings.ini", "Settings", "gtk-icon-theme-name"), ("gtk-4.0/settings.ini", "Settings", "gtk-icon-theme-name"), + ("gtk-3.0/settings.ini", "Settings", "gtk-icon-theme-name"), ] .into_iter() - .find_map(|(path, sec, key)| { + .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()) }) - .unwrap_or_else(|| DEFAULT_THEME.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 +} + /// Parse and Categorize Icons Within the Specified Path -fn find_icons(path: &PathBuf) -> Icons { - WalkDir::new(path) +fn find_icons(path: &PathBuf, sizes: (usize, usize, usize)) -> Vec { + let sizes = calculate_sizes(sizes); + let icons: Icons = WalkDir::new(path) // collect list of directories of icon subdirs .max_depth(1) .into_iter() @@ -69,12 +106,22 @@ fn find_icons(path: &PathBuf) -> Icons { .collect(); (name, group) }) - .collect() -} - -/// Find Best Icon Match for the Given Name -fn match_icon<'a>(icons: &'a Icons, name: &str, size: usize) -> Option<&'a PathBuf> { - todo!("implement icon matching to specified name") + .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 } /// Retrieve XDG-DATA Directories @@ -85,18 +132,23 @@ fn data_dirs(dir: &str) -> Vec { .map(|p| shellexpand::tilde(p).to_string()) .map(PathBuf::from) .map(|p| p.join(dir.to_owned())) + .filter(|p| p.exists()) .collect() } /// Parse XDG Desktop Entry into RMenu Entry -fn parse_desktop(path: &Path, 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(); +fn parse_desktop(path: &Path, locale: Option<&str>) -> Result { + let bytes = read_to_string(path)?; + let entry = DesktopEntry::decode(&path, &bytes)?; + let name = entry + .name(locale) + .ok_or(ProcessError::InvalidAttr("Name"))? + .to_string(); let icon = entry.icon().map(|s| s.to_string()); let comment = entry.comment(locale).map(|s| s.to_string()); let actions: Vec = entry - .actions()? + .actions() + .unwrap_or("") .split(";") .into_iter() .filter(|a| a.len() > 0) @@ -110,7 +162,7 @@ fn parse_desktop(path: &Path, locale: Option<&str>) -> Option { }) }) .collect(); - Some(Entry { + Ok(Entry { name, actions, comment, @@ -126,30 +178,47 @@ fn find_desktops(path: PathBuf, locale: Option<&str>) -> Vec { .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)) + .filter_map(|e| parse_desktop(e.path(), locale).ok()) .collect() } -fn main() { - let path = PathBuf::from("/usr/share/icons/hicolor"); - let icons = find_icons(&path); - icons - .into_iter() - .map(|(k, v)| { - println!("category: {k:?}"); - v.into_iter() - .map(|(name, path)| { - println!(" - {name:?}"); - }) - .last() - }) - .last(); - - // data_dirs("applications") - // .into_iter() - // .map(|p| find_desktops(p, locale)) - // .flatten() - // .filter_map(|e| serde_json::to_string(&e).ok()) - // .map(|s| println!("{}", s)) - // .last(); +/// 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()); + } + } + } + } + e +} + +fn main() { + let locale = Some("en"); + let sizes = (32, 64, 128); + // build a collection of icons for configured themes + let cfgdir = config_dir(); + let themes = find_theme(&cfgdir); + let icons: Vec = data_dirs("icons") + // generate list of icon-paths that exist + .iter() + .map(|d| themes.iter().map(|t| d.join(t))) + .flatten() + .filter(|t| t.exists()) + .map(|t| find_icons(&t, sizes)) + .flatten() + .collect(); + + // retrieve desktop applications and assign icons before printing results + data_dirs("applications") + .into_iter() + .map(|p| find_desktops(p, locale)) + .flatten() + .map(|e| assign_icons(&icons, e)) + .filter_map(|e| serde_json::to_string(&e).ok()) + .map(|s| println!("{}", s)) + .last(); }