fix(desktop): XDG desktop entry precedence
Resolve duplicate desktop IDs before applying desktop entry visibility rules This allows user desktop entries to override matching system entries and ensures Hidden, NoDisplay, and NotShowIn overrides do not reveal lower-priority entries Signed-off-by: name-is-hai <90458368+name-is-hai@users.noreply.github.com>
This commit is contained in:
parent
5187dd6d1d
commit
18731e8392
1 changed files with 228 additions and 38 deletions
266
src/desktop.rs
266
src/desktop.rs
|
|
@ -77,9 +77,12 @@ impl DesktopEntryCache {
|
|||
}
|
||||
|
||||
pub fn refresh(&mut self) {
|
||||
self.entries = fde::Iter::new(fde::default_paths())
|
||||
.filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok())
|
||||
.collect();
|
||||
self.entries = desktop_entries_with_precedence(
|
||||
fde::Iter::new(fde::default_paths())
|
||||
.filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok()),
|
||||
)
|
||||
.filter(|entry| !entry.hidden())
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entry: fde::DesktopEntry) {
|
||||
|
|
@ -604,28 +607,36 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec<String> {
|
|||
ordered
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn desktop_entries_with_precedence<'a>(
|
||||
entries: impl Iterator<Item = fde::DesktopEntry> + 'a,
|
||||
) -> impl Iterator<Item = fde::DesktopEntry> + 'a {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
entries.filter(move |entry| seen.insert(entry.id().to_owned()))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn load_applications<'a>(
|
||||
locales: &'a [String],
|
||||
include_no_display: bool,
|
||||
only_show_in: Option<&'a str>,
|
||||
) -> impl Iterator<Item = DesktopEntryData> + 'a {
|
||||
fde::Iter::new(fde::default_paths())
|
||||
.filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok())
|
||||
.filter(move |de| {
|
||||
(include_no_display || !de.no_display())
|
||||
&& only_show_in.zip(de.only_show_in()).is_none_or(
|
||||
|(xdg_current_desktop, only_show_in)| {
|
||||
only_show_in.contains(&xdg_current_desktop)
|
||||
},
|
||||
)
|
||||
&& only_show_in.zip(de.not_show_in()).is_none_or(
|
||||
|(xdg_current_desktop, not_show_in)| {
|
||||
!not_show_in.contains(&xdg_current_desktop)
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(move |de| DesktopEntryData::from_desktop_entry(locales, de))
|
||||
desktop_entries_with_precedence(
|
||||
fde::Iter::new(fde::default_paths())
|
||||
.filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok()),
|
||||
)
|
||||
.filter(move |de| {
|
||||
!de.hidden()
|
||||
&& (include_no_display || !de.no_display())
|
||||
&& only_show_in.zip(de.only_show_in()).is_none_or(
|
||||
|(xdg_current_desktop, only_show_in)| only_show_in.contains(&xdg_current_desktop),
|
||||
)
|
||||
&& only_show_in.zip(de.not_show_in()).is_none_or(
|
||||
|(xdg_current_desktop, not_show_in)| !not_show_in.contains(&xdg_current_desktop),
|
||||
)
|
||||
})
|
||||
.map(move |de| DesktopEntryData::from_desktop_entry(locales, de))
|
||||
}
|
||||
|
||||
// Create an iterator which filters desktop entries by app IDs.
|
||||
|
|
@ -642,40 +653,51 @@ pub fn load_applications_for_app_ids<'a>(
|
|||
let app_ids = std::rc::Rc::new(std::cell::RefCell::new(app_ids));
|
||||
let app_ids_ = app_ids.clone();
|
||||
|
||||
let applications = iter
|
||||
let applications = desktop_entries_with_precedence(iter)
|
||||
.filter(move |de| {
|
||||
// Match and consume the requested ID before applying visibility
|
||||
// filters. This prevents a Hidden or NoDisplay override from being
|
||||
// recreated by fill_missing_ones.
|
||||
let position = {
|
||||
let requested = app_ids.borrow();
|
||||
|
||||
requested
|
||||
.iter()
|
||||
.position(|id| de.matches_id(fde::unicase::Ascii::new(*id)))
|
||||
.or_else(|| {
|
||||
requested
|
||||
.iter()
|
||||
.position(|id| de.matches_name(fde::unicase::Ascii::new(*id)))
|
||||
})
|
||||
};
|
||||
|
||||
let Some(position) = position else {
|
||||
return false;
|
||||
};
|
||||
|
||||
app_ids.borrow_mut().remove(position);
|
||||
|
||||
if de.hidden() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !include_no_display && de.no_display() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if only_show_in.zip(de.only_show_in()).is_some_and(
|
||||
|(xdg_current_desktop, only_show_in)| !only_show_in.contains(&xdg_current_desktop),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if only_show_in.zip(de.not_show_in()).is_some_and(
|
||||
|(xdg_current_desktop, not_show_in)| not_show_in.contains(&xdg_current_desktop),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search by ID first
|
||||
app_ids
|
||||
.borrow()
|
||||
.iter()
|
||||
.position(|id| de.matches_id(fde::unicase::Ascii::new(*id)))
|
||||
// Then fall back to search by name
|
||||
.or_else(|| {
|
||||
app_ids
|
||||
.borrow()
|
||||
.iter()
|
||||
.position(|id| de.matches_name(fde::unicase::Ascii::new(*id)))
|
||||
})
|
||||
// Remove the app ID if found
|
||||
.map(|i| {
|
||||
app_ids.borrow_mut().remove(i);
|
||||
true
|
||||
})
|
||||
.unwrap_or_default()
|
||||
true
|
||||
})
|
||||
.map(move |de| DesktopEntryData::from_desktop_entry(locales, de));
|
||||
|
||||
|
|
@ -911,6 +933,174 @@ mod tests {
|
|||
entry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_entry_precedence_prefers_first_entry() {
|
||||
let locales = vec!["en_US.UTF-8".to_string()];
|
||||
|
||||
let user_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=User Application\n\
|
||||
Exec=user-application\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let system_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=System Application\n\
|
||||
Exec=system-application\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let entries = desktop_entries_with_precedence(vec![user_entry, system_entry].into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].exec(), Some("user-application"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_override_masks_system_entry_without_fallback() {
|
||||
let locales = vec!["en_US.UTF-8".to_string()];
|
||||
|
||||
let user_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=Example Application\n\
|
||||
Hidden=true\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let system_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=Example Application\n\
|
||||
Exec=system-application\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let applications = load_applications_for_app_ids(
|
||||
vec![user_entry, system_entry].into_iter(),
|
||||
&locales,
|
||||
vec!["com.example.App"],
|
||||
true,
|
||||
false,
|
||||
Some("COSMIC"),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(applications.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_display_override_masks_system_entry() {
|
||||
let locales = vec!["en_US.UTF-8".to_string()];
|
||||
|
||||
let user_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=User Application\n\
|
||||
Exec=user-application\n\
|
||||
NoDisplay=true\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let system_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=System Application\n\
|
||||
Exec=system-application\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let hidden_from_menu = load_applications_for_app_ids(
|
||||
vec![user_entry.clone(), system_entry.clone()].into_iter(),
|
||||
&locales,
|
||||
vec!["com.example.App"],
|
||||
true,
|
||||
false,
|
||||
Some("COSMIC"),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(hidden_from_menu.is_empty());
|
||||
|
||||
let included_when_requested = load_applications_for_app_ids(
|
||||
vec![user_entry, system_entry].into_iter(),
|
||||
&locales,
|
||||
vec!["com.example.App"],
|
||||
true,
|
||||
true,
|
||||
Some("COSMIC"),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(included_when_requested.len(), 1);
|
||||
assert_eq!(
|
||||
included_when_requested[0].exec.as_deref(),
|
||||
Some("user-application")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_show_in_override_masks_system_entry_for_cosmic() {
|
||||
let locales = vec!["en_US.UTF-8".to_string()];
|
||||
|
||||
let user_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=User Application\n\
|
||||
Exec=user-application\n\
|
||||
NotShowIn=COSMIC;\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let system_entry = load_entry(
|
||||
"com.example.App.desktop",
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name=System Application\n\
|
||||
Exec=system-application\n",
|
||||
&locales,
|
||||
);
|
||||
|
||||
let hidden_in_cosmic = load_applications_for_app_ids(
|
||||
vec![user_entry.clone(), system_entry.clone()].into_iter(),
|
||||
&locales,
|
||||
vec!["com.example.App"],
|
||||
true,
|
||||
false,
|
||||
Some("COSMIC"),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(hidden_in_cosmic.is_empty());
|
||||
|
||||
let visible_in_gnome = load_applications_for_app_ids(
|
||||
vec![user_entry, system_entry].into_iter(),
|
||||
&locales,
|
||||
vec!["com.example.App"],
|
||||
true,
|
||||
false,
|
||||
Some("GNOME"),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(visible_in_gnome.len(), 1);
|
||||
assert_eq!(
|
||||
visible_in_gnome[0].exec.as_deref(),
|
||||
Some("user-application")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_generation_covers_common_variants() {
|
||||
let ctx = DesktopLookupContext::new("com.example.App.desktop")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue