feat: icons implemented

This commit is contained in:
imgurbot12 2023-07-28 23:07:06 -07:00
parent 1aa6dd1f53
commit f8d0ce85ea
2 changed files with 114 additions and 44 deletions

View File

@ -11,4 +11,5 @@ rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
rust-ini = { version = "0.19.0", features = ["unicase"] } rust-ini = { version = "0.19.0", features = ["unicase"] }
serde_json = "1.0.103" serde_json = "1.0.103"
shellexpand = "3.1.0" shellexpand = "3.1.0"
thiserror = "1.0.44"
walkdir = "2.3.3" walkdir = "2.3.3"

View File

@ -1,10 +1,11 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs::read_to_string, path::Path}; use std::{fs::read_to_string, path::Path};
use freedesktop_desktop_entry::DesktopEntry; use freedesktop_desktop_entry::DesktopEntry;
use ini::Ini; use ini::Ini;
use rmenu_plugin::{Action, Entry}; use rmenu_plugin::{Action, Entry};
use thiserror::Error;
use walkdir::WalkDir; use walkdir::WalkDir;
static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS"; 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 XDG_CONFIG_DEFAULT: &'static str = "~/.config";
static DEFAULT_THEME: &'static str = "hicolor"; 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 /// Retrieve XDG-CONFIG-HOME Directory
#[inline] #[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()); let path = std::env::var(XDG_CONFIG_ENV).unwrap_or_else(|_| XDG_CONFIG_DEFAULT.to_string());
PathBuf::from(shellexpand::tilde(&path).to_string()) PathBuf::from(shellexpand::tilde(&path).to_string())
} }
/// Determine XDG Icon Theme based on Preexisting Configuration Files /// Determine XDG Icon Theme based on Preexisting Configuration Files
fn find_theme(cfgdir: &PathBuf) -> String { fn find_theme(cfgdir: &PathBuf) -> Vec<String> {
vec![ let mut themes: Vec<String> = vec![
("kdeglobals", "Icons", "Theme"), ("kdeglobals", "Icons", "Theme"),
("gtk-3.0/settings.ini", "Settings", "gtk-icon-theme-name"),
("gtk-4.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() .into_iter()
.find_map(|(path, sec, key)| { .filter_map(|(path, sec, key)| {
let path = cfgdir.join(path); let path = cfgdir.join(path);
let ini = Ini::load_from_file(path).ok()?; let ini = Ini::load_from_file(path).ok()?;
ini.get_from(Some(sec), key).map(|s| s.to_string()) 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<String, PathBuf>; type IconGroup = HashMap<String, PathBuf>;
type Icons = HashMap<String, IconGroup>; type Icons = HashMap<String, IconGroup>;
/// Precalculate prefferred sizes folders
fn calculate_sizes(range: (usize, usize, usize)) -> HashSet<String> {
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 /// Parse and Categorize Icons Within the Specified Path
fn find_icons(path: &PathBuf) -> Icons { fn find_icons(path: &PathBuf, sizes: (usize, usize, usize)) -> Vec<IconGroup> {
WalkDir::new(path) let sizes = calculate_sizes(sizes);
let icons: Icons = WalkDir::new(path)
// collect list of directories of icon subdirs // collect list of directories of icon subdirs
.max_depth(1) .max_depth(1)
.into_iter() .into_iter()
@ -69,12 +106,22 @@ fn find_icons(path: &PathBuf) -> Icons {
.collect(); .collect();
(name, group) (name, group)
}) })
.collect() .collect();
} // organize icon groups according to prefference
let mut priority = vec![];
/// Find Best Icon Match for the Given Name let mut others = vec![];
fn match_icon<'a>(icons: &'a Icons, name: &str, size: usize) -> Option<&'a PathBuf> { icons
todo!("implement icon matching to specified name") .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 /// Retrieve XDG-DATA Directories
@ -85,18 +132,23 @@ fn data_dirs(dir: &str) -> Vec<PathBuf> {
.map(|p| shellexpand::tilde(p).to_string()) .map(|p| shellexpand::tilde(p).to_string())
.map(PathBuf::from) .map(PathBuf::from)
.map(|p| p.join(dir.to_owned())) .map(|p| p.join(dir.to_owned()))
.filter(|p| p.exists())
.collect() .collect()
} }
/// Parse XDG Desktop Entry into RMenu Entry /// Parse XDG Desktop Entry into RMenu Entry
fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> { fn parse_desktop(path: &Path, locale: Option<&str>) -> Result<Entry, ProcessError> {
let bytes = read_to_string(path).ok()?; let bytes = read_to_string(path)?;
let entry = DesktopEntry::decode(&path, &bytes).ok()?; let entry = DesktopEntry::decode(&path, &bytes)?;
let name = entry.name(locale)?.to_string(); let name = entry
.name(locale)
.ok_or(ProcessError::InvalidAttr("Name"))?
.to_string();
let icon = entry.icon().map(|s| s.to_string()); let icon = entry.icon().map(|s| s.to_string());
let comment = entry.comment(locale).map(|s| s.to_string()); let comment = entry.comment(locale).map(|s| s.to_string());
let actions: Vec<Action> = entry let actions: Vec<Action> = entry
.actions()? .actions()
.unwrap_or("")
.split(";") .split(";")
.into_iter() .into_iter()
.filter(|a| a.len() > 0) .filter(|a| a.len() > 0)
@ -110,7 +162,7 @@ fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> {
}) })
}) })
.collect(); .collect();
Some(Entry { Ok(Entry {
name, name,
actions, actions,
comment, comment,
@ -126,30 +178,47 @@ fn find_desktops(path: PathBuf, locale: Option<&str>) -> Vec<Entry> {
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".desktop")) .filter(|e| e.file_name().to_string_lossy().ends_with(".desktop"))
.filter(|e| e.file_type().is_file()) .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() .collect()
} }
fn main() { /// Find and Assign Icons from Icon-Cache when Possible
let path = PathBuf::from("/usr/share/icons/hicolor"); fn assign_icons(icons: &Vec<IconGroup>, mut e: Entry) -> Entry {
let icons = find_icons(&path); if let Some(name) = e.icon.as_ref() {
icons if !name.contains("/") {
.into_iter() if let Some(path) = icons.iter().find_map(|i| i.get(name)) {
.map(|(k, v)| { if let Some(fpath) = path.to_str() {
println!("category: {k:?}"); e.icon = Some(fpath.to_owned());
v.into_iter() }
.map(|(name, path)| { }
println!(" - {name:?}"); }
}) }
.last() e
}) }
.last();
fn main() {
// data_dirs("applications") let locale = Some("en");
// .into_iter() let sizes = (32, 64, 128);
// .map(|p| find_desktops(p, locale)) // build a collection of icons for configured themes
// .flatten() let cfgdir = config_dir();
// .filter_map(|e| serde_json::to_string(&e).ok()) let themes = find_theme(&cfgdir);
// .map(|s| println!("{}", s)) let icons: Vec<IconGroup> = data_dirs("icons")
// .last(); // 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();
} }