Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: implement full desktop spec #120

Merged
merged 10 commits into from
Apr 9, 2024
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 86 additions & 44 deletions client/src/plugin/applications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,20 @@ pub struct ApplicationsPlugin {
entries: Vec<crate::model::Entry>,
}

fn read_desktop_entry(path: &std::path::PathBuf) -> anyhow::Result<crate::model::Entry> {
let pathstr = path.to_str().unwrap_or("");
let bytes = std::fs::read_to_string(path)?;
let desktop_entry = freedesktop_desktop_entry::DesktopEntry::decode(path, &bytes)?;
fn to_entry(
desktop_entry: &freedesktop_desktop_entry::DesktopEntry,
terminal_command: Option<String>,
) -> Option<crate::model::Entry> {
let locale = std::env::var("LANG").unwrap_or(String::from("en_US"));
let title = desktop_entry.name(Some(&locale))?.to_string();

if !is_visible(&desktop_entry) {
return Err(anyhow::anyhow!(
"Desktop entry at path '{}' is hidden.",
pathstr
));
log::debug!(target: "applications", "Desktop entry with name '{}' will be hidden because of its properties.", title);
return None;
}

let locale = std::env::var("LANG").unwrap_or(String::from("en_US"));
let title = desktop_entry
.name(Some(&locale))
.context(format!(
"Desktop entry at path '{}' is missing the 'name' field.",
pathstr
))?
.to_string();

let cmd = desktop_entry
.exec()
.context(format!(
"Desktop entry at path '{}' is missing the 'exec' field.",
pathstr
))?
let mut cmd: Vec<String> = desktop_entry
.exec()?
.split_ascii_whitespace()
.filter_map(|s| {
if s.starts_with('%') {
Expand All @@ -42,13 +29,23 @@ fn read_desktop_entry(path: &std::path::PathBuf) -> anyhow::Result<crate::model:
})
.collect();

if desktop_entry.terminal() {
if terminal_command.is_none() {
log::warn!(target: "applications", "Desktop entry with name '{}' will be hidden because no terminal emulator was found to launch it with.", title);
return None;
}

cmd.insert(0, terminal_command.unwrap());
cmd.insert(1, "-e".into());
}

let mut meta = desktop_entry
.keywords()
.unwrap_or(std::borrow::Cow::from(""))
.replace(';', " ");
meta.push_str(" Applications Apps");

Ok(crate::model::Entry {
Some(crate::model::Entry {
id: desktop_entry.appid.to_string(),
title,
action: String::from("open"),
Expand All @@ -58,6 +55,25 @@ fn read_desktop_entry(path: &std::path::PathBuf) -> anyhow::Result<crate::model:
}

fn is_visible(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> bool {
if desktop_entry.type_() != Some("Application") {
return false;
}

if desktop_entry.desktop_entry("Hidden") == Some("true") {
return false;
}

if desktop_entry.no_display() {
return false;
}

// filter entries where Exec == false
if let Some(exec) = desktop_entry.exec() {
if exec.to_ascii_lowercase() == "false" {
return false;
}
}

let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or(String::from("sway"));
// filter entries where NotShowIn == current desktop
if let Some(not_show_in) = desktop_entry.desktop_entry("NotShowIn") {
Expand All @@ -72,24 +88,37 @@ fn is_visible(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> bool {
if let Some(only_show_in) = desktop_entry.only_show_in() {
let only_show_in_desktops = only_show_in.to_ascii_lowercase();

if !only_show_in_desktops.split(';').any(|d| d == desktop) {
if !only_show_in_desktops.split(';').all(|d| d != desktop) {
return false;
}
}

// filter entries where NoDisplay != true
if desktop_entry.no_display() {
return false;
true
}

fn terminal_command(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> Option<String> {
if !desktop_entry
.categories()?
.split(";")
.any(|category| category == "TerminalEmulator")
{
return None;
}
return desktop_entry
.exec()?
.split_ascii_whitespace()
.nth(0)
.map(String::from);
}

// filter entries where Exec == false
if let Some(exec) = desktop_entry.exec() {
if exec.to_ascii_lowercase() == "false" {
return false;
}
fn name(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> String {
let locale = std::env::var("LANG").unwrap_or(String::from("en_US"));
let name_option = desktop_entry.name(Some(&locale));
if name_option.is_none() {
return "".into();
}
friedow marked this conversation as resolved.
Show resolved Hide resolved

true
name_option.unwrap().to_string()
}

impl Plugin for ApplicationsPlugin {
Expand Down Expand Up @@ -120,19 +149,32 @@ impl Plugin for ApplicationsPlugin {
fn update_entries(&mut self) -> anyhow::Result<()> {
self.entries.clear();

let paths =
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths());
self.entries = paths
.filter_map(|path| {
let desktop_entry_result = read_desktop_entry(&path);
if let Err(error) = desktop_entry_result {
log::warn!(target: "applications", "Skipping desktop entry: '{:?}'.", error);
return None;
}
desktop_entry_result.ok()
let mut paths: Vec<std::path::PathBuf> =
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.collect();

let mut bytes_collection: Vec<(&std::path::PathBuf, String)> = paths
.iter()
.filter_map(|path| Some((path, std::fs::read_to_string(path).ok()?)))
.collect();

let mut desktop_entries: Vec<freedesktop_desktop_entry::DesktopEntry> = bytes_collection
.iter()
.filter_map(|(bytes, path)| {
freedesktop_desktop_entry::DesktopEntry::decode(bytes, path).ok()
})
.collect();

desktop_entries.sort_by_key(name);
desktop_entries.dedup_by_key(|desktop_entry| name(desktop_entry));

let terminal_command = desktop_entries.iter().find_map(terminal_command);

self.entries = desktop_entries
.iter()
.filter_map(|path| to_entry(path, terminal_command.clone()))
.collect();

self.entries.sort();
self.entries.dedup();

Expand Down