feat: add native macOS application launcher

This commit is contained in:
2026-10-11 18:16:01 +02:00
commit 0d033667ff
10 changed files with 780 additions and 0 deletions
@@ -0,0 +1,127 @@
import ConjureKit
import Foundation
import Testing
@Test func searchRanksExactPrefixSubstringAndFuzzyMatches() {
let names = ["Caramel", "My cl Tool", "Cloud", "Cold", "CL", "Unrelated"]
let applications = names.map(application)
#expect(ApplicationSearch.results(for: "cl", in: applications).map(\.name) == [
"CL", "Cloud", "My cl Tool", "Cold", "Caramel",
])
#expect(ApplicationSearch.results(for: "zq", in: applications).isEmpty)
}
@Test func searchFoldsAccentsAndTrimsWhitespace() {
let applications = ["Decafé", "Caféteria", "Café", "Camera"].map(application)
#expect(ApplicationSearch.results(for: " \n CAFÉ \t ", in: applications).map(\.name) == [
"Café", "Caféteria", "Decafé",
])
}
@Test func fuzzyMatchesPreferCompactSpansThenEarlierStartsAndAlphabeticalTies() {
let applications = ["AxxxxB", "xAyB", "AzB", "AyB", "B before A"].map(application)
#expect(ApplicationSearch.results(for: "ab", in: applications).map(\.name) == [
"AyB", "AzB", "xAyB", "AxxxxB",
])
}
@Test func emptySearchSortsAlphabeticallyAndBreaksDuplicateNameTiesByURL() {
let first = Application(url: URL(fileURLWithPath: "/First/Tools.app"), name: "Tools")
let second = Application(url: URL(fileURLWithPath: "/Second/Tools.app"), name: "Tools")
let applications = [second, first, application("Alpha")]
for query in ["", " \n\t "] {
#expect(ApplicationSearch.results(for: query, in: applications).map(\.url) == [
applications[2].url, first.url, second.url,
])
}
#expect(ApplicationSearch.results(for: "tools", in: applications).map(\.url) == [first.url, second.url])
}
@Test func discoveryFindsNestedAppsButNotHiddenInvalidOrBundledHelpers() throws {
let root = try temporaryDirectory()
defer { try? FileManager.default.removeItem(at: root) }
let nested = root.appendingPathComponent("Utilities", isDirectory: true)
let alpha = try appBundle(in: nested, filename: "Alpha", name: "Alpha")
let parent = try appBundle(in: root, filename: "Parent", name: "Parent")
_ = try appBundle(in: parent.appendingPathComponent("Contents/Helpers"), filename: "Helper", name: "Helper")
_ = try appBundle(in: root, filename: ".Hidden", name: "Hidden")
_ = try appBundle(in: root, filename: "NoExecutable", name: "NoExecutable", executable: false)
_ = try appBundle(in: root, filename: "WrongType", name: "WrongType", packageType: "BNDL")
let fallback = try appBundle(in: root, filename: "Fallback", name: nil)
let display = try appBundle(in: root, filename: "Filename", name: "Internal", displayName: "Unlocalized")
let localization = display.appendingPathComponent("Contents/Resources/en.lproj", isDirectory: true)
try FileManager.default.createDirectory(at: localization, withIntermediateDirectories: true)
let localizedInfo = try PropertyListSerialization.data(
fromPropertyList: ["CFBundleDisplayName": "Display"], format: .xml, options: 0
)
try localizedInfo.write(to: localization.appendingPathComponent("InfoPlist.strings"))
let invalid = root.appendingPathComponent("Invalid.app", isDirectory: true)
_ = try appBundle(in: invalid, filename: "OtherHelper", name: "OtherHelper")
let applications = ApplicationCatalog.discover(in: [root, root.appendingPathComponent("Missing")])
#expect(applications.map(\.name) == ["Alpha", "Display", "Fallback", "Parent"])
#expect(applications.map(\.url) == [alpha, display, fallback, parent].map { $0.resolvingSymlinksInPath() })
}
@Test func discoveryCanonicalizesAliasesAndAvoidsDirectorySymlinkCycles() throws {
let root = try temporaryDirectory()
defer { try? FileManager.default.removeItem(at: root) }
let nested = root.appendingPathComponent("Nested", isDirectory: true)
let app = try appBundle(in: nested, filename: "Editor", name: "Editor")
let alias = root.appendingPathComponent("Alias.app")
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: app)
try FileManager.default.createSymbolicLink(at: nested.appendingPathComponent("Loop"), withDestinationURL: root)
let linkedDirectory = root.appendingPathComponent("LinkedDirectory")
try FileManager.default.createSymbolicLink(at: linkedDirectory, withDestinationURL: nested)
let applications = ApplicationCatalog.discover(in: [root, linkedDirectory, root])
#expect(applications.count == 1)
#expect(applications.first?.id == app.resolvingSymlinksInPath())
#expect(Application(url: alias, name: "Alias").id == applications.first?.id)
}
private func application(_ name: String) -> Application {
Application(url: URL(fileURLWithPath: "/Applications/\(name).app"), name: name)
}
private func temporaryDirectory() throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url
}
@discardableResult
private func appBundle(
in directory: URL,
filename: String,
name: String?,
displayName: String? = nil,
executable: Bool = true,
packageType: String = "APPL"
) throws -> URL {
let url = directory.appendingPathComponent("\(filename).app", isDirectory: true)
let contents = url.appendingPathComponent("Contents", isDirectory: true)
let macOS = contents.appendingPathComponent("MacOS", isDirectory: true)
try FileManager.default.createDirectory(at: macOS, withIntermediateDirectories: true)
var info: [String: Any] = [
"CFBundlePackageType": packageType,
"CFBundleExecutable": "Launcher",
"CFBundleDevelopmentRegion": "en",
"CFBundleIdentifier": "test.conjure.\(UUID().uuidString)",
]
info["CFBundleName"] = name
info["CFBundleDisplayName"] = displayName
let data = try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0)
try data.write(to: contents.appendingPathComponent("Info.plist"))
if executable {
let binary = macOS.appendingPathComponent("Launcher")
try Data("#!/bin/sh\nexit 0\n".utf8).write(to: binary)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
}
return url
}