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")