feat(applications): match macOS apps by their localized display name - #590
Conversation
macOS localizes bundle display names: Photos.app presents as "Fotos" on a German system, Notes.app as "Notizen", System Settings.app as "Systemeinstellungen". Asyar indexed the bundle file name instead, so on any non-English system those apps could not be found under the name the user sees — twelve stock system apps on a German install alone. The scan now resolves the name through NSFileManager, which is the only reliable source: the localized string does not live in Info.plist (CFBundleDisplayName reads "Photos" even on a localized system) and the lproj resources are not reliably parseable. Both spellings stay searchable. SearchableItem::search_names() returns the display name plus the bundle file name when they differ, and the skim pre-filter scores all of them, keeping the best hit. The file name is derived from `path`, so it needs no extra storage and no schema change. Only `name` is ever rendered, so results still read "Fotos". Ranking follows: the non-display name is added to the classifier's keywords, otherwise an app matched via its file name would classify as "no name hit" and rank below genuine misses. App ids are deliberately unaffected. build_app_id() embeds the name, and the id keys usage stats, user-assigned aliases and item shortcuts — so it keeps receiving the file name via the new bundle_file_name() helper. Existing ids stay byte-identical and no data is lost on upgrade. A test pins this down. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…-stem search names
|
Hey @kai-osthoff, really nice work on this one 🎉 The localized-name problem is a genuinely annoying one to get right, and I like the design: keeping bundle_file_name() (identity, used for the app_ id) strictly separate from display_name() (presentation, what the user sees) is exactly the right call — it means usage stats, aliases, and shortcuts survive a language change untouched, which is easy to get wrong here. The search_names() addition so both the localized name and the on-disk name stay searchable is a thoughtful touch too — that's the kind of detail that's easy to skip and would've quietly broken search for anyone used to typing the English app name. Test coverage was solid, and everything (cargo test, clippy --all-targets, fmt) passed clean. Two small things worth flagging, both handled: Merge conflict with #589 — main moved forward after this branch was cut, and #589 touched the same file (both added a test at the same anchor line). Nothing wrong on your end, just timing. It's resolved now — all tests from both PRs pass together. Merging now. |
Problem
macOS localizes bundle display names. On a German system
Photos.apppresents as "Fotos",Notes.appas "Notizen",System Settings.appas "Systemeinstellungen". Asyar indexes the bundle file name, so those apps are unreachable under the name the user actually sees.On one German install this affects twelve stock system apps — Fotos, Notizen, Erinnerungen, Kalender, Nachrichten, Karten, Musik, Wetter, Rechner, Vorschau, Kontakte, Systemeinstellungen — plus any localized third-party app. For non-English users this makes application search substantially less useful.
The name has to come from the OS.
CFBundleDisplayNameinInfo.plistreads "Photos" even on a localized system, and thelprojresources aren't reliably parseable (Photos.app/Contents/Resources/de.lprojcarries no readableInfoPlist.stringsat all).NSFileManager.displayName(atPath:)is the supported route;objc2-foundationis already a dependency, so this adds none.Both names stay searchable
Replacing
nameoutright would trade one bug for another — users who know apps by their English names would stop finding them, and macOS itself keeps both (kMDItemDisplayName=Fotos.app,kMDItemAlternateNames=Photos.app).SearchableItem::search_names()returns the display name plus the bundle file name when the two differ, and the skim pre-filter scores all of them and keeps the best hit. This has to happen in the pre-filter:merged_searchusessearch()as its gate, so a name only reachable viakeywordswould be filtered out before ranking ever sees it. The classifier'skeywordsare extended too, otherwise a file-name match would classify as "no name hit" and rank below genuine misses.The file name is derived from
path, which is already stored — no new field, no schema change, no regenerated TS bindings, no migration. Onlynameis rendered, so results still read "Fotos".App ids are deliberately unaffected
build_app_id()embeds the name, and the resulting id keys usage stats, user-assigned aliases (item_aliases.object_id) and item shortcuts. Letting a localized display name reach it would have changed the id of every localized app on the first scan after upgrade and silently dropped all three.So the id keeps receiving the file name, via a new
bundle_file_name()helper that is deliberately separate fromdisplay_name(). Existing ids stay byte-identical. A test pins this down so the two don't get merged back together later.This also de-duplicates the
file_stem()logic that was copy-pasted acrosssync_application_indexandlist_applications.Cost
NSFileManagerresolution measured over a real install: 246 apps in 64 ms (261 µs/app), on the index sync that already walks the filesystem and extracts icons — both far more expensive. No cache added: it would raise an invalidation question (language change, app update, rename) for a cost that doesn't currently warrant one. Happy to add one if you'd rather.Testing
cargo test --lib→ 3176 passed, 9 ignored.cargo clippyclean,cargo fmt --checkclean.One pre-existing failure, unrelated to this change:
file_index::deep::mdfind::tests::probe_succeeds_on_a_real_macfails when the test process can't reach Spotlight. Verified by stashing this branch's changes and re-running it — it fails identically on a clean tree.Note on locale coverage: no test can assert "Fotos" without failing on an English CI runner. The search-side tests therefore run against synthetic items and are locale-independent, while the
NSFileManagertest only asserts what must hold in every locale (non-empty, extension stripped). Confirming the German behaviour end-to-end needs a localized machine.