Skip to content

Commit

Permalink
🐛 implement full desktop spec (#120)
Browse files Browse the repository at this point in the history
This PR implements a bunch of fixes around the applications plugin. This
aligns the applications plugin closer with the [freedesktop desktop
entry
specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html).
Furhtermore it fixes #110.

This PR includes:
- Hiding desktop entries based on the following logic:
   - Type has to be "Application"
   - Hidden has to be false or unset
   - NoDisplay has to be false or unset
- the XDG_CURRENT_DESKTOP has to be in OnlyShowIn and not in NotShowIn
- Terminal desktop entries are now opened in the first terminal emulator
that is found on the system (fixes things like btop not being able to
launch)
  • Loading branch information
friedow authored Apr 9, 2024
1 parent d69a208 commit 94d275b
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 47 deletions.
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.

129 changes: 84 additions & 45 deletions client/src/plugin/applications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,19 @@ 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 title = name(desktop_entry);

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 +28,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 +54,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 +87,35 @@ 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
}

// filter entries where Exec == false
if let Some(exec) = desktop_entry.exec() {
if exec.to_ascii_lowercase() == "false" {
return false;
}
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);
}

true
fn name(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> String {
let locale = std::env::var("LANG").unwrap_or(String::from("en_US"));
desktop_entry
.name(Some(&locale))
.unwrap_or_default()
.to_string()
}

impl Plugin for ApplicationsPlugin {
Expand Down Expand Up @@ -120,19 +146,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 paths: Vec<std::path::PathBuf> =
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.collect();

let 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

0 comments on commit 94d275b

Please sign in to comment.