From 796e8a820c71b984652d265a5baa20fd6bc3a457 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Thu, 15 Oct 2026 10:04:08 +0200 Subject: [PATCH] fix: discover XPC-backed application bundles --- README.md | 2 ++ Sources/ConjureKit/Applications.swift | 3 ++- TODO.md | 7 +++++++ Tests/ConjureKitTests/ApplicationsTests.swift | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d5d924..cf457fc 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ Conjure recognizes the native [login-launch Apple event](https://developer.apple Applications are discovered in `/Applications`, `/System/Applications`, `/System/Library/CoreServices/Applications`, and `~/Applications`, including nested folders. Discovery runs off the UI thread and refreshes whenever the launcher is presented. Hidden items, invalid bundles, and helpers inside application packages are excluded; symlinked applications are deduplicated by their canonical paths. +Discovery supports both standard (`APPL`) and XPC-backed (`XPC!`) `.app` bundles, including Apple Passwords, provided they contain a valid executable. + Search is case- and accent-insensitive. Ranking is exact name, full-name prefix, word prefix, ordered word initials, substring, then ordered-subsequence fuzzy matches. Fuzzy matches favor smaller gaps and earlier starts. Equally good text matches put favorites first, then order by successful launch count, most recent launch, and alphabetically. Neither favorites nor usage promote a weaker text match above a stronger one or change which applications match. Hover over a result to see its path. Launch failures appear in a native alert without dismissing the launcher. Words are runs of letters or numbers separated by spaces, punctuation, or symbols. For example, `settings` matches the second word of System Settings, `gc` matches Google Chrome, and `ss` matches System Settings. Initials must occur in order but can skip words: both `vsc` and `vc` match Visual Studio Code. diff --git a/Sources/ConjureKit/Applications.swift b/Sources/ConjureKit/Applications.swift index 3b20467..35ab42b 100644 --- a/Sources/ConjureKit/Applications.swift +++ b/Sources/ConjureKit/Applications.swift @@ -68,7 +68,8 @@ public enum ApplicationCatalog { // Never expose helpers inside an application, even if its bundle is invalid. guard visitedApplications.insert(url).inserted, let bundle = Bundle(url: url), - bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as? String == "APPL", + let packageType = bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as? String, + packageType == "APPL" || packageType == "XPC!", let executable = bundle.executableURL, let executableValues = try? executable.resourceValues(forKeys: [.isRegularFileKey]), executableValues.isRegularFile == true, diff --git a/TODO.md b/TODO.md index e69de29..c492019 100644 --- a/TODO.md +++ b/TODO.md @@ -0,0 +1,7 @@ +- [ ] add a translation "command" + - it should appear in the list as a "Command" instead of an "Application" (entry type/kind) + - it should be named/titled "Conjure Translate" + - applying the open action on the command will cause the launcher to morph into a translation tool (ui transition like the uninstall confirmation view) + - the search bar becomes the translate input + - first pass should translate from english to japanese + - it should use gpt-6-luna as the translation unit (requires the user to insert an api key) \ No newline at end of file diff --git a/Tests/ConjureKitTests/ApplicationsTests.swift b/Tests/ConjureKitTests/ApplicationsTests.swift index 3a358e1..a7b68ab 100644 --- a/Tests/ConjureKitTests/ApplicationsTests.swift +++ b/Tests/ConjureKitTests/ApplicationsTests.swift @@ -103,6 +103,20 @@ import Testing #expect(applications.map(\.url) == [alpha, display, fallback, parent].map { $0.resolvingSymlinksInPath() }) } +@Test func discoveryFindsSearchableXPCAppsButNotTheirHelpersOrInvalidBundles() throws { + let root = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let passwords = try appBundle(in: root, filename: "Passwords", name: "Passwords", packageType: "XPC!") + _ = try appBundle(in: passwords.appendingPathComponent("Contents/Helpers"), + filename: "Helper", name: "Helper", packageType: "XPC!") + _ = try appBundle(in: root, filename: "NoExecutable", name: "NoExecutable", + executable: false, packageType: "XPC!") + + let applications = ApplicationCatalog.discover(in: [root]) + #expect(applications.map(\.url) == [passwords.resolvingSymlinksInPath()]) + #expect(ApplicationSearch.results(for: "passwords", in: applications).map(\.name) == ["Passwords"]) +} + @Test func discoveryCanonicalizesAliasesAndAvoidsDirectorySymlinkCycles() throws { let root = try temporaryDirectory() defer { try? FileManager.default.removeItem(at: root) }