2023-02-02 23:52:59 +01:00
|
|
|
use std::{
|
|
|
|
|
cmp::Ordering,
|
|
|
|
|
collections::HashSet,
|
|
|
|
|
error::Error,
|
2026-10-03 09:33:12 +00:00
|
|
|
fs::read_to_string,
|
2026-10-03 09:15:53 +00:00
|
|
|
hash::{Hash, Hasher},
|
2023-02-02 23:52:59 +01:00
|
|
|
io::{Error as io_error, ErrorKind},
|
2026-10-03 09:15:53 +00:00
|
|
|
path::Path,
|
2023-02-02 23:52:59 +01:00
|
|
|
};
|
|
|
|
|
|
2023-01-11 00:28:54 +01:00
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
|
2023-04-04 01:53:02 +02:00
|
|
|
use crate::utils::{inish, xdg};
|
2023-02-02 23:52:59 +01:00
|
|
|
|
2026-10-03 09:33:12 +00:00
|
|
|
#[derive(Clone, Debug, Eq)]
|
2023-01-11 00:28:54 +01:00
|
|
|
pub struct Desktop {
|
|
|
|
|
pub entry_type: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub no_display: bool,
|
|
|
|
|
pub hidden: bool,
|
|
|
|
|
pub exec: Option<String>,
|
|
|
|
|
pub url: Option<String>,
|
|
|
|
|
pub term: bool,
|
|
|
|
|
pub keywords: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Desktop {
|
2026-10-03 09:15:53 +00:00
|
|
|
fn parse(f: &Path) -> Result<Desktop, Box<dyn Error>> {
|
2023-02-02 23:52:59 +01:00
|
|
|
let s = read_to_string(f)?;
|
|
|
|
|
let config = inish::parse(&s)?;
|
|
|
|
|
match config.get("Desktop Entry") {
|
|
|
|
|
Some(section) => Ok(Desktop {
|
|
|
|
|
entry_type: section.get("Type").unwrap_or(&"").to_string(),
|
|
|
|
|
name: section.get("Name").unwrap_or(&"").to_string(),
|
|
|
|
|
term: section.get("Terminal").unwrap_or(&"") == &"true",
|
|
|
|
|
no_display: section.get("NoDisplay").unwrap_or(&"") == &"true",
|
|
|
|
|
hidden: section.get("Hidden").unwrap_or(&"") == &"true",
|
|
|
|
|
exec: section.get("Exec").map(|x| x.to_string()),
|
|
|
|
|
url: section.get("URL").map(|x| x.to_string()),
|
|
|
|
|
keywords: section
|
2023-01-11 00:28:54 +01:00
|
|
|
.get("Keywords")
|
|
|
|
|
.map(|x| {
|
|
|
|
|
x.split(';')
|
|
|
|
|
.map(|y| y.trim().to_string())
|
2026-10-03 09:20:25 +00:00
|
|
|
.filter(|z| !z.is_empty())
|
2023-01-11 00:28:54 +01:00
|
|
|
.collect()
|
|
|
|
|
})
|
2026-10-03 09:20:25 +00:00
|
|
|
.unwrap_or_else(std::vec::Vec::new),
|
2023-01-11 00:28:54 +01:00
|
|
|
}),
|
|
|
|
|
None => Err(Box::new(io_error::new(
|
|
|
|
|
ErrorKind::NotFound,
|
|
|
|
|
"no desktop entry in file",
|
|
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Ord for Desktop {
|
|
|
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
|
|
|
self.name.cmp(&other.name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PartialOrd for Desktop {
|
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
|
|
Some(self.cmp(other))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PartialEq for Desktop {
|
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
|
self.name == other.name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:15:53 +00:00
|
|
|
// Must agree with PartialEq: equal entries (same name) must hash equally.
|
|
|
|
|
impl Hash for Desktop {
|
|
|
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
|
|
self.name.hash(state)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:33:12 +00:00
|
|
|
/// Scan every XDG applications directory for launchable entries.
|
|
|
|
|
///
|
|
|
|
|
/// This used to be cached as JSON under XDG_CACHE_HOME and refreshed by
|
|
|
|
|
/// hand with `--desktop-refresh`. The scan is a few milliseconds over
|
|
|
|
|
/// local /usr/share and runs on a background thread, while the cache sat
|
|
|
|
|
/// on the (possibly NFS-mounted) home directory and went stale whenever a
|
|
|
|
|
/// package was installed. So: no cache.
|
2023-01-11 00:28:54 +01:00
|
|
|
pub fn load_desktop_files() -> Vec<Desktop> {
|
2023-04-04 01:53:02 +02:00
|
|
|
let dirs = xdg::data_folders();
|
2023-02-02 23:52:59 +01:00
|
|
|
|
|
|
|
|
let mut desktop: Vec<Desktop> = Vec::new();
|
|
|
|
|
let mut seen: HashSet<String> = HashSet::new();
|
|
|
|
|
|
|
|
|
|
for dir in dirs {
|
2026-10-03 09:16:15 +00:00
|
|
|
for entry in WalkDir::new(format!("{}/applications", dir))
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
{
|
2026-10-03 09:15:53 +00:00
|
|
|
// Entries are often symlinks, so only rule out directories.
|
|
|
|
|
if entry.file_type().is_dir()
|
|
|
|
|
|| entry.path().extension().and_then(|e| e.to_str()) != Some("desktop")
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
if let Ok(d) = Desktop::parse(entry.path()) {
|
2023-02-02 23:52:59 +01:00
|
|
|
if d.hidden
|
|
|
|
|
|| d.no_display
|
|
|
|
|
|| (d.entry_type != "Application" && d.entry_type != "Link")
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if !seen.contains(&d.name) {
|
|
|
|
|
seen.insert(d.name.to_string());
|
|
|
|
|
desktop.push(d);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
desktop
|
2023-01-11 00:28:54 +01:00
|
|
|
}
|