feat: open providers with Command-P and dismiss pickers with Escape

This commit is contained in:
2026-10-18 00:34:15 +02:00
parent 86632f1d86
commit f12ef7e7a1
6 changed files with 42 additions and 10 deletions
+6 -4
View File
@@ -54,7 +54,9 @@ The launcher, emoji picker, notes, and translation tool open immediately without
| Open a result or copy a calculated answer | Return or double-click |
| Open actions for the selected entry | Command-K |
| Open one of the first nine results | Hold Command until numbers appear, then press 1–9 |
| Dismiss the launcher, including the current Command and Actions | Escape or Command-W |
| Close the open action/provider picker, otherwise dismiss the launcher | Escape |
| Dismiss the launcher, including the current Command and pickers | Command-W |
| Open the translation provider picker | Command-P (in Translate) |
| Go back without dismissing | Back chevron |
| Open Settings | Command-comma |
| Quit | Command-Q |
@@ -114,7 +116,7 @@ If macOS keeps prompting despite an enabled Accessibility entry, quit Conjure, r
Open **Actions** with Command-K for **Copy and Paste** (default), **Copy Emoji** (copy only, no Accessibility permission needed), **Favorite**, or **Remove Favorite**. Favorites are saved locally and grouped ahead of other emoji, with a small extra spacing break when browsing without a search. There are no star badges.
With Actions open, the Back chevron closes Actions first; another Back returns to Applications and restores your search. Escape or Command-W dismisses the entire launcher, including the current Command and its Actions. Switching destinations also removes the previous Command's Actions completely. Copy and Copy and Paste dismiss the launcher without an intermediate Back step.
With Actions open, Escape or the Back chevron closes Actions first. Another Escape dismisses the launcher; another Back returns to Applications and restores your search. Command-W always dismisses the entire launcher, including the current Command and its Actions. Switching destinations also removes the previous Command's Actions completely. Copy and Copy and Paste dismiss the launcher without an intermediate Back step.
The picker works offline with 1,870 emoji from [gemoji v4.1.0](https://github.com/github/gemoji/tree/v4.1.0). Its [MIT license](Sources/ConjureKit/Resources/LICENSE-gemoji.txt) is included in the application.
@@ -132,9 +134,9 @@ Notes autosave locally as RTFD packages, including embedded images, under `~/Lib
Search for **Conjure Translate**, identified by its centered 🌏 icon, and open it. Choose a provider in the pill-shaped header selector, enter text in the search bar, then press Return or click **Translate**. Typing alone never sends text. When the translation is available, the action changes to **Copy**: press Return or click it to copy the full translation and close Conjure. Command-C still copies selected output. Editing the input restores **Translate**.
The provider selector expands into a searchable action-style popup from its top edge downwards, hiding the indicator until the popup closes. A checkmark marks the current provider. Use Up/Down and Return or click a provider to select it; Back or clicking outside closes the popup. Changing providers clears the previous result without submitting a request.
Press Command-P or click the provider selector to expand a searchable action-style popup from its top edge downwards, hiding the indicator until the popup closes. A checkmark marks the current provider. Use Up/Down and Return or click a provider to select it; Escape, Back, or clicking outside closes the popup. Changing providers clears the previous result without submitting a request.
The footer shows the current language pair (initially **English → Japanese**). Click it or open **Actions** with Command-K to change the source language, change the target language, or swap them. Language choices are saved locally and apply to both providers. Changing languages clears the previous result without submitting another request. The input hint and output accessibility label follow the selected pair. Back closes Actions first, then returns to the launcher and restores your search; Escape or Command-W dismisses the entire window.
The footer shows the current language pair (initially **English → Japanese**). Click it or open **Actions** with Command-K to change the source language, change the target language, or swap them. Language choices are saved locally and apply to both providers. Changing languages clears the previous result without submitting another request. The input hint and output accessibility label follow the selected pair. Escape closes an open action/provider picker first and dismisses the window only when no picker is open. Back closes the picker first, then returns to the launcher and restores your search. Command-W always dismisses the entire window.
Editing the input, changing languages or providers, saving or forgetting the key in Settings, or leaving Translate cancels the active request; cancelled results are not shown. After a request fails, submit again to retry.
@@ -308,6 +308,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
func handle(_ selector: Selector) -> Bool {
switch selector {
case #selector(NSResponder.cancelOperation(_:)):
guard isShown else { return false }
close()
case #selector(NSResponder.insertNewline(_:)):
if let selectedEntry { chooseAction(at: selectedEntry) }
case #selector(NSResponder.moveUp(_:)): moveSelection(by: -1)
+11 -4
View File
@@ -97,6 +97,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}
window.usesTextNavigation = { [weak self] in self?.notes != nil }
window.dismiss = { [weak self] in self?.dismiss() }
window.escape = { [weak self] in self?.handleEscape() }
window.canUseNumberShortcuts = { [weak self] in
guard let self else { return false }
return !self.isLaunching && self.calculation == nil && self.resultCount > 0
@@ -115,8 +116,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
actions.closed = { [weak self] in
guard let self else { return }
self.openButton.keyEquivalent = "\r"
if NSApp.isActive { self.window?.makeFirstResponder(self.searchField) }
else { self.dismiss(restoringFocus: false) }
self.window?.makeFirstResponder(self.searchField)
}
currency.changed = { [weak self] in self?.updateResults(preservingSelection: true) }
restorePosition()
@@ -494,9 +494,15 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
updateResults()
}
private func handleEscape() {
let selector = #selector(NSResponder.cancelOperation(_:))
if actions.handle(selector) || translation?.handle(selector) == true || emojiPicker?.handle(selector) == true { return }
dismiss()
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.cancelOperation(_:)) {
dismiss()
handleEscape()
return true
}
if let emojiPicker { return emojiPicker.handle(commandSelector) }
@@ -1163,6 +1169,7 @@ final class LauncherPanel: NSPanel {
override var canBecomeMain: Bool { true }
var navigateSelection: ((Int) -> Void)?
var dismiss: (() -> Void)?
var escape: (() -> Void)?
var dragEnded: (() -> Void)?
var canUseNumberShortcuts: (() -> Bool)?
var numberHintsChanged: (() -> Void)?
@@ -1293,7 +1300,7 @@ final class LauncherPanel: NSPanel {
}
override func cancelOperation(_ sender: Any?) {
dismiss?()
if let escape { escape() } else { dismiss?() }
}
override func performClose(_ sender: Any?) {
@@ -68,6 +68,9 @@ final class TranslationController: NSViewController {
providerButton.setAccessibilityLabel("Translation provider")
providerButton.target = self
providerButton.action = #selector(showProviders)
providerButton.keyEquivalent = "p"
providerButton.keyEquivalentModifierMask = .command
providerButton.toolTip = "Choose translation provider (Command-P)"
providerButton.isBordered = false
providerButton.font = .systemFont(ofSize: 12, weight: .medium)
providerButton.contentTintColor = SunsetPalette.text
+1
View File
@@ -124,6 +124,7 @@ public final class GlobalShortcuts {
case "z": return "Undo"
case "m": return "Minimize"
case "k": return "entry actions"
case "p": return "translation provider selection"
case "return", "enter": return "Open, Copy, Translate, or confirm uninstall"
case "1"..."9" where key.utf8.count == 1: return "numbered application launching"
default: break
@@ -3,8 +3,8 @@ import ConjureKit
import Testing
@testable import Conjure
@Test(arguments: ["launcher", "launcher-actions", "emoji", "emoji-actions", "translate-input", "translate-output", "notes-editor"])
@MainActor func escapeDismissesEntireLauncher(_ destination: String) throws {
@Test(arguments: ["launcher", "launcher-actions", "emoji", "emoji-actions", "translate-input", "translate-output", "translate-actions", "translate-providers", "notes-editor"])
@MainActor func escapeClosesPickerBeforeDismissingLauncher(_ destination: String) throws {
_ = NSApplication.shared
let suite = "ConjureEscape.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
@@ -42,6 +42,17 @@ import Testing
$0.title == "Actions" && $0.target is EmojiPickerController
})
(button.target as? EmojiPickerController)?.showActions()
} else if destination == "translate-actions" {
let button = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
$0.title == "Actions" && $0.target is TranslationController
})
button.performClick(nil)
} else if destination == "translate-providers" {
let shortcut = try #require(NSEvent.keyEvent(
with: .keyDown, location: .zero, modifierFlags: .command, timestamp: 1,
windowNumber: window.windowNumber, context: nil, characters: "p",
charactersIgnoringModifiers: "p", isARepeat: false, keyCode: 35))
#expect(window.performKeyEquivalent(with: shortcut))
} else if destination == "translate-output" {
let output = try #require(descendants(content).compactMap { $0 as? NSTextView }.first {
$0.accessibilityLabel() == "Japanese translation"
@@ -57,6 +68,11 @@ import Testing
windowNumber: window.windowNumber, context: nil, characters: "\u{1b}",
charactersIgnoringModifiers: "\u{1b}", isARepeat: false, keyCode: 53))
window.sendEvent(escape)
if destination.hasSuffix("-actions") || destination == "translate-providers" {
#expect(window.isVisible)
#expect(window.firstResponder === search.currentEditor())
window.sendEvent(escape)
}
#expect(!window.isVisible)
#expect(!descendants(content).contains { $0 is NSCollectionView })
}