fix: discover XPC-backed application bundles
This commit is contained in:
@@ -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.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ public enum ApplicationCatalog {
|
|||||||
// Never expose helpers inside an application, even if its bundle is invalid.
|
// Never expose helpers inside an application, even if its bundle is invalid.
|
||||||
guard visitedApplications.insert(url).inserted,
|
guard visitedApplications.insert(url).inserted,
|
||||||
let bundle = Bundle(url: url),
|
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 executable = bundle.executableURL,
|
||||||
let executableValues = try? executable.resourceValues(forKeys: [.isRegularFileKey]),
|
let executableValues = try? executable.resourceValues(forKeys: [.isRegularFileKey]),
|
||||||
executableValues.isRegularFile == true,
|
executableValues.isRegularFile == true,
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -103,6 +103,20 @@ import Testing
|
|||||||
#expect(applications.map(\.url) == [alpha, display, fallback, parent].map { $0.resolvingSymlinksInPath() })
|
#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 {
|
@Test func discoveryCanonicalizesAliasesAndAvoidsDirectorySymlinkCycles() throws {
|
||||||
let root = try temporaryDirectory()
|
let root = try temporaryDirectory()
|
||||||
defer { try? FileManager.default.removeItem(at: root) }
|
defer { try? FileManager.default.removeItem(at: root) }
|
||||||
|
|||||||
Reference in New Issue
Block a user