From 18731e839265eeb534924b4a83e69a0e5043f32b Mon Sep 17 00:00:00 2001 From: name-is-hai <90458368+name-is-hai@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:12:46 +0700 Subject: [PATCH] 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> --- src/desktop.rs | 266 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 228 insertions(+), 38 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index cfa7a668..67002ca5 100644 --- a/src/desktop.rs +++ b/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 { ordered } +#[cfg(not(windows))] +fn desktop_entries_with_precedence<'a>( + entries: impl Iterator + 'a, +) -> impl Iterator + '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 + '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::>(); + + 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::>(); + + 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::>(); + + 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::>(); + + 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::>(); + + 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::>(); + + 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")