feat: rank built-in commands alongside applications
This commit is contained in:
@@ -69,13 +69,13 @@ Applications are discovered in `/Applications`, `/System/Applications`, `/System
|
||||
|
||||
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 for applications and built-in commands alike. 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 launch count, most recent launch, and alphabetically. Neither favorites nor usage promote a weaker text match above a stronger one or change which entries match. Hover over an application to see its path. Application 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.
|
||||
|
||||
With an empty search, favorites appear first, separated from the remaining applications by a 12-point gap when both groups are present. Typed searches have no group spacing. Within both favorite and non-favorite groups, applications are ordered by successful launch count, then most recent launch, then alphabetically. Favorites and usage are stored locally by application bundle identity and survive restarts; copies with the same name remain independent. Only launches through Conjure count; failed launches do not. Applications without history remain alphabetical within their group.
|
||||
With an empty search, favorites appear first, separated from the remaining entries by a 12-point gap when both groups are present. Typed searches have no group spacing. Within both favorite and non-favorite groups, applications and commands are ordered together by launch count, then most recent launch, then alphabetically. Favorites and usage are stored locally by application bundle identity or stable built-in command identity and survive restarts; an application and a command with the same name remain independent. Successful application launches and opening a command through Conjure count; submitting translations inside a command does not. Entries without history remain alphabetical within their group.
|
||||
|
||||
Background discovery preserves the selected application even if its row changes. If that application disappears, the first remaining result is selected. Changing the query or reopening the launcher selects the first result.
|
||||
Background discovery preserves the selected entry even if its row changes. If that entry disappears, the first remaining result is selected. Changing the query or reopening the launcher selects the first result.
|
||||
|
||||
Applications with identical display names show a muted containing-folder hint beside the name. Copies sharing the same folder show their full application bundle paths instead. Home paths abbreviate to `~`, and long hints truncate in the middle. Unique names have no hint; locations are recalculated whenever discovery refreshes.
|
||||
|
||||
@@ -83,6 +83,8 @@ Applications with identical display names show a muted containing-folder hint be
|
||||
|
||||
Search for **Conjure Translate**, labeled **Command**, and open it with Return, double-click, or **Open Command**. The launcher fades into an English → Japanese translation tool; the existing search bar becomes the English input and retains the launcher's placeholder styling. Escape returns to the previous search.
|
||||
|
||||
The command participates in the same text matching, usage ranking, and favorites as applications (`ct` matches its initials). Return, double-click, **Open Command**, numbered shortcuts, and **Actions → Open** all record an opening. Its Actions menu offers **Open**, **Favorite / Remove Favorite**, and **Reset ranking**, but not Finder, Quit, or Uninstall. Reset ranking clears usage without removing its favorite status.
|
||||
|
||||
Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropdown. MyMemory is the initial default, and the selection is saved locally. Switching providers cancels pending work and clears the previous translation; there is no automatic fallback. Press Return from the English input or click **Translate** in the bottom-right launcher-style action pill, beside its Return key badge, to send text to the selected provider.
|
||||
|
||||
[MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
- [x] 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)
|
||||
- [ ] improve on the conjure translation command
|
||||
@@ -0,0 +1,70 @@
|
||||
import AppKit
|
||||
import ConjureKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor func commandsShareUsageFavoritesAndTextRankingWithApplications() {
|
||||
let suite = "ConjureCommandRanking.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
var now = Date(timeIntervalSinceReferenceDate: 12_000)
|
||||
let history = LaunchHistory(defaults: defaults, now: { now })
|
||||
let command = Application(command: .translate)
|
||||
let app = Application(url: URL(fileURLWithPath: "/Applications/Conjure Test.app"), name: "Conjure Test")
|
||||
let sameNameApp = Application(url: URL(fileURLWithPath: "/Applications/Conjure Translate.app"), name: command.name)
|
||||
let entries = [app, sameNameApp, command]
|
||||
history.recordLaunch(of: app)
|
||||
now += 1
|
||||
history.recordLaunch(of: command)
|
||||
#expect(history.suggestions(in: entries).first?.id == command.id)
|
||||
#expect(history.results(for: "conjure", in: entries).first?.id == command.id)
|
||||
#expect(history.results(for: "ct", in: entries).first?.id == command.id)
|
||||
history.recordLaunch(of: app)
|
||||
#expect(history.suggestions(in: entries).first?.id == app.id)
|
||||
#expect(history.results(for: "Conjure Translate", in: entries).first?.id == command.id)
|
||||
history.toggleFavorite(command)
|
||||
#expect(history.suggestions(in: entries).first?.id == command.id)
|
||||
#expect(!history.isFavorite(sameNameApp))
|
||||
let reloaded = LaunchHistory(defaults: defaults, now: { now })
|
||||
#expect(reloaded.isFavorite(command))
|
||||
#expect(reloaded.suggestions(in: entries).first?.id == command.id)
|
||||
reloaded.toggleFavorite(command)
|
||||
#expect(reloaded.results(for: "conjure", in: entries).first?.id == app.id)
|
||||
#expect(reloaded.results(for: "Conjure Translate", in: entries).first?.id == command.id)
|
||||
reloaded.recordLaunch(of: sameNameApp)
|
||||
reloaded.resetRanking(of: command)
|
||||
let afterReset = LaunchHistory(defaults: defaults, now: { now })
|
||||
#expect(afterReset.suggestions(in: entries).first?.id == app.id)
|
||||
#expect(afterReset.results(for: "Conjure Translate", in: entries).first?.id == sameNameApp.id)
|
||||
}
|
||||
|
||||
@Test @MainActor func openingCommandByReturnAndNumberShortcutRecordsUsage() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "ConjureCommandOpening.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let now = Date(timeIntervalSinceReferenceDate: 13_000)
|
||||
let history = LaunchHistory(defaults: defaults, now: { now })
|
||||
let command = Application(command: .translate)
|
||||
let competitor = Application(url: URL(fileURLWithPath: "/Applications/Alpha.app"), name: "Alpha")
|
||||
history.recordLaunch(of: competitor)
|
||||
let launcher = LauncherWindowController(history: history)
|
||||
let content = try #require(launcher.window?.contentView)
|
||||
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
||||
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first)
|
||||
search.stringValue = "ct"
|
||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||
let table = try #require(descendants(content).compactMap { $0 as? NSTableView }.first)
|
||||
#expect(table.numberOfRows == 1)
|
||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
||||
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
||||
#expect(history.suggestions(in: [command, competitor]).first?.id == competitor.id)
|
||||
let panel = try #require(launcher.window as? LauncherPanel)
|
||||
panel.launchNumber?(0)
|
||||
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
||||
let reloaded = LaunchHistory(defaults: defaults, now: { now })
|
||||
#expect(reloaded.suggestions(in: [command, competitor]).first?.id == command.id)
|
||||
}
|
||||
Reference in New Issue
Block a user