feat: rank built-in commands alongside applications

This commit is contained in:
Valdemar Lorenzen
2026-10-15 11:23:50 +02:00
parent 31c651ecb1
commit e0f17c6339
7 changed files with 131 additions and 43 deletions
@@ -13,6 +13,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed)
]
private var visibleEntries: [Int] = []
private var isCommand = false
private var selectedEntry: Int? = 0
private var rowTopConstraints: [NSLayoutConstraint] = []
private var heightConstraint: NSLayoutConstraint?
@@ -129,6 +130,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
let pill = anchor.superview ?? anchor
_ = view
applicationLabel.stringValue = application.name
isCommand = application.command != nil
let favorite = entries[1]
let favoriteTitle = isFavorite ? "Remove Favorite" : "Favorite"
favorite.button.attributedTitle = NSAttributedString(string: favoriteTitle, attributes: [
@@ -201,7 +203,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
func controlTextDidChange(_ notification: Notification) {
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
visibleEntries = entries.indices.filter { query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil }
visibleEntries = entries.indices.filter {
(!isCommand || $0 == 0 || $0 == 1 || $0 == 3)
&& (query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil)
}
selectedEntry = visibleEntries.first
for index in entries.indices {
let position = visibleEntries.firstIndex(of: index)
+29 -27
View File
@@ -10,7 +10,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private let sectionLabel = NSTextField(labelWithString: "Applications")
private let openButton = NSButton(title: "Open Application", target: nil, action: nil)
private let history: LaunchHistory
private var applications: [Application] = []
private var applications: [Application] = Application.commands
private var results: [Application] = []
private var calculation: CalculatorResult?
private var isCurrencyCalculation = false
@@ -32,10 +32,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private var launcherViews: [NSView] = []
private var launcherFrameBeforeReview: NSRect?
private var translation: TranslationController?
private var showsTranslationCommand = true
private var translationCommandRow: Int? {
calculation == nil && showsTranslationCommand ? results.count : nil
}
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller()) {
self.history = history
@@ -74,8 +70,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
window.numberHintsChanged = { [weak self] in self?.updateNumberHints() }
window.launchNumber = { [weak self] index in
guard let self else { return }
if index == self.translationCommandRow { self.showTranslation() }
else if self.results.indices.contains(index) { self.openApplication(self.results[index]) }
if self.results.indices.contains(index) { self.openApplication(self.results[index]) }
}
window.dragEnded = { [weak self] in
guard let self else { return }
@@ -300,8 +295,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
actionsButton.keyEquivalentModifierMask = .command
actionsButton.target = self
actionsButton.action = #selector(showActions)
actionsButton.setAccessibilityLabel("Application actions")
actionsButton.toolTip = "Actions for the selected application (Command-K)"
actionsButton.setAccessibilityLabel("Entry actions")
actionsButton.toolTip = "Actions for the selected application or command (Command-K)"
actionsButton.isEnabled = false
openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select."
let returnKey = ShortcutIndicator("↵")
@@ -375,7 +370,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}.value
guard let self else { return }
guard !Task.isCancelled else { return }
self.applications = applications
self.applications = applications + Application.commands
self.updateLocationLabels()
self.discoveryTask = nil
self.updateResults(preservingSelection: true)
@@ -407,9 +402,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private func updateLocationLabels() {
var nameCounts: [String: Int] = [:]
nameCounts.reserveCapacity(applications.count)
for application in applications { nameCounts[application.name, default: 0] += 1 }
for application in applications where application.command == nil { nameCounts[application.name, default: 0] += 1 }
var groups: [String: [(url: URL, folder: URL)]] = [:]
for application in applications where (nameCounts[application.name] ?? 0) > 1 {
for application in applications where application.command == nil && (nameCounts[application.name] ?? 0) > 1 {
groups[application.name, default: []].append(
(application.url, application.url.deletingLastPathComponent())
)
@@ -458,9 +453,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
detail: "\(answer.detail) · \(cacheStatus)"
)
}
showsTranslationCommand = query.split(whereSeparator: \.isWhitespace).allSatisfy {
"Conjure Translate".localizedStandardContains(String($0))
}
if calculation != nil { (window as? LauncherPanel)?.resetNumberHints() }
if calculation != nil {
results = []
@@ -472,7 +464,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
sectionLabel.stringValue = isCurrencyCalculation ? "Daily currency rates"
: calculation == nil ? "Applications & Commands" : "Calculator"
tableView.setAccessibilityLabel(isCurrencyCalculation ? "Daily currency conversion result"
: calculation == nil ? "Applications" : "Calculator result")
: calculation == nil ? "Applications and commands" : "Calculator result")
tableView.reloadData()
if resultCount > 0 {
let row = selectedURL.flatMap { url in results.firstIndex { $0.id == url } } ?? 0
@@ -486,8 +478,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
emptyLabel.stringValue = "Finding applications…"
statusLabel.stringValue = "Finding applications…"
} else {
emptyLabel.stringValue = applications.isEmpty ? "No applications found" : "No matching applications"
statusLabel.stringValue = "\(results.count) applications" + (showsTranslationCommand ? " · 1 command" : "")
emptyLabel.stringValue = "No matching applications or commands"
let commandCount = results.lazy.filter { $0.command != nil }.count
statusLabel.stringValue = "\(results.count - commandCount) applications · \(commandCount) commands"
}
updateOpenButton()
(window as? LauncherPanel)?.sampleNumberHints()
@@ -519,7 +512,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
return true
}
private var resultCount: Int { calculation == nil ? results.count + (showsTranslationCommand ? 1 : 0) : 1 }
private var resultCount: Int { calculation == nil ? results.count : 1 }
private func moveSelection(by offset: Int) {
guard translation == nil else { return }
@@ -531,12 +524,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private func updateOpenButton() {
let canAct = !isLaunching && launcherFrameBeforeReview == nil && translation == nil && tableView.selectedRow >= 0
openButton.title = calculation == nil ? (tableView.selectedRow == translationCommandRow ? "Open Command" : "Open Application") : "Copy Answer"
openButton.title = calculation == nil
? (results.indices.contains(tableView.selectedRow) && results[tableView.selectedRow].command != nil
? "Open Command" : "Open Application") : "Copy Answer"
openButton.toolTip = calculation == nil
? "Open the selected entry (Return). Use ↑ and ↓ to select."
: "Copy the answer to the clipboard (Return)."
openButton.isEnabled = canAct && (calculation.map { $0.copyValue != nil }
?? (results.indices.contains(tableView.selectedRow) || tableView.selectedRow == translationCommandRow))
?? results.indices.contains(tableView.selectedRow))
actionsButton.isEnabled = canAct && calculation == nil && results.indices.contains(tableView.selectedRow)
}
@@ -574,11 +569,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
.withSymbolConfiguration(.init(pointSize: LauncherRowView.iconSize, weight: .regular))
cell.imageView?.contentTintColor = SunsetPalette.accent
cell.toolTip = calculation.detail
} else if row == translationCommandRow {
cell.textField?.stringValue = "Conjure Translate"
cell.textField?.setAccessibilityLabel(nil)
} else if results[row].command != nil {
let entry = results[row]
cell.textField?.stringValue = entry.name
cell.textField?.setAccessibilityLabel(history.isFavorite(entry) ? "\(entry.name), Favorite" : nil)
cell.locationLabel.isHidden = true
cell.kindLabel.stringValue = "Command"
cell.kindLabel.stringValue = history.isFavorite(entry) ? "★ Command" : "Command"
cell.imageView?.image = NSImage(systemSymbolName: "character.bubble", accessibilityDescription: nil)
cell.imageView?.contentTintColor = SunsetPalette.accent
cell.toolTip = "Translate English to Japanese"
@@ -723,7 +719,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
@objc private func openSelection() {
guard !isLaunching, translation == nil, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
if tableView.selectedRow == translationCommandRow { showTranslation(); return }
if let calculation {
guard tableView.selectedRow == 0, let value = calculation.copyValue else { return }
let pasteboard = NSPasteboard.general
@@ -749,7 +744,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}
private func openApplication(_ application: Application) {
guard !isLaunching else { return }
guard !isLaunching, translation == nil, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
if let command = application.command {
switch command {
case .translate: showTranslation()
}
history.recordLaunch(of: application)
return
}
(window as? LauncherPanel)?.resetNumberHints()
isLaunching = true
updateOpenButton()
+16 -3
View File
@@ -1,6 +1,11 @@
import Foundation
public struct Application: Identifiable, Sendable {
public enum Command: String, CaseIterable, Sendable {
case translate
}
public static let commands = Command.allCases.map { Application(command: $0) }
public let command: Command?
public var id: URL { url }
public let url: URL
public let name: String
@@ -12,8 +17,16 @@ public struct Application: Identifiable, Sendable {
self.init(canonicalURL: url.standardizedFileURL.resolvingSymlinksInPath(), name: name)
}
fileprivate init(canonicalURL: URL, name: String) {
url = canonicalURL.hasDirectoryPath
public init(command: Command) {
let name = switch command {
case .translate: "Conjure Translate"
}
self.init(canonicalURL: URL(string: "conjure-command://\(command.rawValue)")!, name: name, command: command)
}
fileprivate init(canonicalURL: URL, name: String, command: Command? = nil) {
self.command = command
url = command != nil || canonicalURL.hasDirectoryPath
? canonicalURL : URL(fileURLWithPath: canonicalURL.path, isDirectory: true)
self.name = name
searchableName = folded(name)
@@ -178,5 +191,5 @@ func alphabetical(_ lhs: Application, _ rhs: Application) -> Bool {
let order = lhs.name.localizedStandardCompare(rhs.name)
if order != .orderedSame { return order == .orderedAscending }
if lhs.name != rhs.name { return lhs.name < rhs.name }
return lhs.url.path < rhs.url.path
return lhs.url.absoluteString < rhs.url.absoluteString
}
+4 -2
View File
@@ -18,7 +18,8 @@ public final class LaunchHistory {
self.defaults = defaults
self.now = now
for (identifier, value) in defaults.dictionary(forKey: Self.storageKey) ?? [:] {
guard let url = URL(string: identifier), url.isFileURL,
guard let url = URL(string: identifier),
url.isFileURL || Application.commands.contains(where: { $0.id == url }),
let record = value as? [String: Any],
let count = record["count"] as? Int, count > 0,
let lastLaunch = record["lastLaunch"] as? Date,
@@ -26,7 +27,8 @@ public final class LaunchHistory {
usage[url] = Usage(count: count, lastLaunch: lastLaunch)
}
for identifier in defaults.stringArray(forKey: Self.favoritesStorageKey) ?? [] {
guard let url = URL(string: identifier), url.isFileURL else { continue }
guard let url = URL(string: identifier),
url.isFileURL || Application.commands.contains(where: { $0.id == url }) else { continue }
favorites.insert(url)
}
}