mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-27 05:18:33 +01:00
feat: icons implemented
This commit is contained in:
parent
1aa6dd1f53
commit
f8d0ce85ea
2 changed files with 114 additions and 44 deletions
|
@ -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"
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue