diff --git a/Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift b/Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift new file mode 100644 index 00000000..27bd76d9 --- /dev/null +++ b/Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift @@ -0,0 +1,77 @@ +import Foundation + +/// キーボードショートカットを表す構造体 +public struct KeyboardShortcut: Codable, Equatable, Hashable, Sendable { + public var key: String + public var modifiers: KeyEventCore.ModifierFlag + + public init(key: String, modifiers: KeyEventCore.ModifierFlag) { + self.key = key + self.modifiers = modifiers + } + + /// デフォルトのショートカット(Control+S) + public static let defaultTransformShortcut = KeyboardShortcut( + key: "s", + modifiers: .control + ) + + /// 表示用の文字列(例: "⌃S") + public var displayString: String { + var result = "" + + if modifiers.contains(.control) { + result += "⌃" + } + if modifiers.contains(.option) { + result += "⌥" + } + if modifiers.contains(.shift) { + result += "⇧" + } + if modifiers.contains(.command) { + result += "⌘" + } + + result += key.uppercased() + return result + } +} + +protocol KeyboardShortcutConfigItem: ConfigItem { + static var `default`: KeyboardShortcut { get } +} + +extension KeyboardShortcutConfigItem { + public var value: KeyboardShortcut { + get { + guard let data = UserDefaults.standard.data(forKey: Self.key) else { + return Self.default + } + do { + let decoded = try JSONDecoder().decode(KeyboardShortcut.self, from: data) + return decoded + } catch { + return Self.default + } + } + nonmutating set { + do { + let encoded = try JSONEncoder().encode(newValue) + UserDefaults.standard.set(encoded, forKey: Self.key) + } catch { + // エンコード失敗時は何もしない + } + } + } +} + +extension Config { + /// いい感じ変換のキーボードショートカット + public struct TransformShortcut: KeyboardShortcutConfigItem { + public init() {} + + public static let `default`: KeyboardShortcut = .defaultTransformShortcut + public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.transform_shortcut" + } +} diff --git a/Core/Sources/Core/Configs/StringConfigItem.swift b/Core/Sources/Core/Configs/StringConfigItem.swift index 363e8f9e..1c235aff 100644 --- a/Core/Sources/Core/Configs/StringConfigItem.swift +++ b/Core/Sources/Core/Configs/StringConfigItem.swift @@ -56,7 +56,7 @@ extension Config { } /// プロンプト履歴(JSON形式で保存) - struct PromptHistory: StringConfigItem { - static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory" + public struct PromptHistory: StringConfigItem { + public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory" } } diff --git a/Core/Sources/Core/InputUtils/KeyEventCore.swift b/Core/Sources/Core/InputUtils/KeyEventCore.swift index 8f01d710..bfdc3930 100644 --- a/Core/Sources/Core/InputUtils/KeyEventCore.swift +++ b/Core/Sources/Core/InputUtils/KeyEventCore.swift @@ -1,5 +1,5 @@ public struct KeyEventCore: Sendable, Equatable { - public struct ModifierFlag: OptionSet, Sendable, Hashable { + public struct ModifierFlag: OptionSet, Codable, Sendable, Hashable { public let rawValue: Int public init(rawValue: Int) { diff --git a/azooKeyMac/InputController/NSEvent.swift b/azooKeyMac/InputController/NSEvent.swift index 584bf39d..83430a22 100644 --- a/azooKeyMac/InputController/NSEvent.swift +++ b/azooKeyMac/InputController/NSEvent.swift @@ -3,24 +3,48 @@ import Core extension NSEvent { var keyEventCore: KeyEventCore { - var modifierFlags: KeyEventCore.ModifierFlag = [] - if self.modifierFlags.contains(.shift) { - modifierFlags.insert(.shift) - } - if self.modifierFlags.contains(.control) { - modifierFlags.insert(.control) - } - if self.modifierFlags.contains(.command) { - modifierFlags.insert(.command) - } - if self.modifierFlags.contains(.option) { - modifierFlags.insert(.option) - } - return KeyEventCore( - modifierFlags: modifierFlags, + KeyEventCore( + modifierFlags: .init(from: self.modifierFlags), characters: self.characters, charactersIgnoringModifiers: self.charactersIgnoringModifiers, keyCode: self.keyCode ) } } + +/// NSEvent.ModifierFlagsとKeyEventCore.ModifierFlagの相互変換 +extension KeyEventCore.ModifierFlag { + public init(from nsModifiers: NSEvent.ModifierFlags) { + var flags: KeyEventCore.ModifierFlag = [] + if nsModifiers.contains(.control) { + flags.insert(.control) + } + if nsModifiers.contains(.option) { + flags.insert(.option) + } + if nsModifiers.contains(.shift) { + flags.insert(.shift) + } + if nsModifiers.contains(.command) { + flags.insert(.command) + } + self = flags + } + + public var nsModifierFlags: NSEvent.ModifierFlags { + var flags: NSEvent.ModifierFlags = [] + if contains(.control) { + flags.insert(.control) + } + if contains(.option) { + flags.insert(.option) + } + if contains(.shift) { + flags.insert(.shift) + } + if contains(.command) { + flags.insert(.command) + } + return flags + } +} diff --git a/azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift b/azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift index c2a2dd69..75b540d7 100644 --- a/azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift +++ b/azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift @@ -163,13 +163,15 @@ extension azooKeyMacInputController { }, completion: { [weak self] prompt in self?.segmentsManager.appendDebugMessage("showPromptInputWindow: Window closed with prompt: \(prompt ?? "nil")") + self?.isPromptWindowVisible = false // Restore focus on cancel (prompt == nil) here so every closing path including window-level Esc ends up restoring focus. if prompt == nil, let app = currentApp { app.activate(options: []) self?.segmentsManager.appendDebugMessage("showPromptInputWindow: Restored focus to original app on cancel") } - self?.isPromptWindowVisible = false + // 設定変更に備えてキャッシュを更新 + self?.reloadPinnedPromptsCache() } ) } diff --git a/azooKeyMac/InputController/azooKeyMacInputController.swift b/azooKeyMac/InputController/azooKeyMacInputController.swift index 81fa6ada..63a2f18b 100644 --- a/azooKeyMac/InputController/azooKeyMacInputController.swift +++ b/azooKeyMac/InputController/azooKeyMacInputController.swift @@ -36,6 +36,9 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s private static let doubleTapInterval: TimeInterval = 0.5 private static let candidateWindowInitialSize = CGSize(width: 400, height: 1000) + // ピン留めプロンプトのキャッシュ(パフォーマンス向上のため) + private var pinnedPromptsCache: [PromptHistoryItem] = [] + private static func makeCandidateWindow(contentViewController: NSViewController, inputClient: IMKTextInput?) -> NSWindow { let window = NSWindow(contentViewController: contentViewController) window.styleMask = [.borderless] @@ -58,6 +61,44 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s return isDouble } + /// ピン留めプロンプトのキャッシュを更新 + func reloadPinnedPromptsCache() { + guard let data = UserDefaults.standard.data(forKey: Config.PromptHistory.key), + let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) else { + self.pinnedPromptsCache = [] + return + } + self.pinnedPromptsCache = history.filter { $0.isPinned } + } + + // MARK: - カスタムプロンプトショートカット検出 + private func checkCustomPromptShortcut(event: NSEvent) -> String? { + guard let characters = event.charactersIgnoringModifiers, + !characters.isEmpty else { + return nil + } + + let key = characters.lowercased() + let eventModifiers = KeyEventCore.ModifierFlag(from: event.modifierFlags) + + // 修飾キーがない場合は早期リターン(通常の入力) + if eventModifiers.isEmpty { + return nil + } + + // キャッシュからショートカット付きのピン留めプロンプトを検索 + if let matched = self.pinnedPromptsCache.first(where: { item in + guard let itemShortcut = item.shortcut else { + return false + } + return itemShortcut.key == key && itemShortcut.modifiers == eventModifiers + }) { + return matched.prompt + } + + return nil + } + override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { let applicationDirectoryURL = if #available(macOS 13, *) { URL.applicationSupportDirectory @@ -124,6 +165,8 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s CustomInputTableStore.registerIfExists() self.updateLiveConversionToggleMenuItem(newValue: self.liveConversionEnabled) self.updateTransformSelectedTextMenuItemEnabledState() + // ピン留めプロンプトのキャッシュを更新 + self.reloadPinnedPromptsCache() self.segmentsManager.activate() if let client = sender as? IMKTextInput { @@ -229,6 +272,21 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s return false } + // カスタムプロンプトショートカットのチェック + if let matchedPrompt = checkCustomPromptShortcut(event: event) { + let aiBackendEnabled = Config.AIBackendPreference().value != .off + if aiBackendEnabled && !self.isPromptWindowVisible { + let selectedRange = client.selectedRange() + if selectedRange.length > 0 { + if self.triggerAiTranslation(initialPrompt: matchedPrompt) { + return true + } + } + } + // ショートカットがマッチした場合はイベントを消費して他のハンドラに渡さない + return true + } + let userAction = UserAction.getUserAction(eventCore: event.keyEventCore, inputLanguage: inputLanguage) // 英数キー(keyCode 102)の処理 diff --git a/azooKeyMac/Windows/KeyboardShortcutRecorder.swift b/azooKeyMac/Windows/KeyboardShortcutRecorder.swift new file mode 100644 index 00000000..3040a66c --- /dev/null +++ b/azooKeyMac/Windows/KeyboardShortcutRecorder.swift @@ -0,0 +1,164 @@ +import AppKit +import Core +import SwiftUI + +/// キーボードショートカットを記録するためのビュー +struct KeyboardShortcutRecorder: NSViewRepresentable { + @Binding var shortcut: Core.KeyboardShortcut + var placeholder: String = "ショートカットを入力..." + + func makeNSView(context: Context) -> ShortcutRecorderView { + let view = ShortcutRecorderView() + view.shortcut = shortcut + view.placeholder = placeholder + view.onShortcutChanged = { newShortcut in + shortcut = newShortcut + } + return view + } + + func updateNSView(_ nsView: ShortcutRecorderView, context: Context) { + if nsView.shortcut != shortcut { + nsView.shortcut = shortcut + } + } +} + +/// NSViewベースのショートカットレコーダー +class ShortcutRecorderView: NSView { + var shortcut: Core.KeyboardShortcut = .defaultTransformShortcut { + didSet { + needsDisplay = true + } + } + var placeholder: String = "ショートカットを入力..." + var onShortcutChanged: ((Core.KeyboardShortcut) -> Void)? + + private var isRecording = false { + didSet { + needsDisplay = true + } + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + wantsLayer = true + layer?.cornerRadius = 4 + layer?.borderWidth = 1 + layer?.borderColor = NSColor.separatorColor.cgColor + } + + override var acceptsFirstResponder: Bool { + true + } + + override func becomeFirstResponder() -> Bool { + isRecording = true + return super.becomeFirstResponder() + } + + override func resignFirstResponder() -> Bool { + isRecording = false + return super.resignFirstResponder() + } + + override func mouseDown(with event: NSEvent) { + window?.makeFirstResponder(self) + } + + override func keyDown(with event: NSEvent) { + guard isRecording else { + super.keyDown(with: event) + return + } + + // keyCode 53: Escape - 記録をキャンセル + if event.keyCode == 53 { + window?.makeFirstResponder(nil) + return + } + + // keyCode 51: Delete (Backspace), keyCode 117: Forward Delete - デフォルトにリセット + if event.keyCode == 51 || event.keyCode == 117 { + shortcut = .defaultTransformShortcut + onShortcutChanged?(shortcut) + window?.makeFirstResponder(nil) + return + } + + guard let characters = event.charactersIgnoringModifiers, + !characters.isEmpty else { + return + } + + let key = characters.lowercased() + let modifiers = KeyEventCore.ModifierFlag(from: event.modifierFlags) + + guard modifiers.contains(.control) || + modifiers.contains(.option) || + modifiers.contains(.shift) || + modifiers.contains(.command) else { + return + } + + let newShortcut = Core.KeyboardShortcut(key: key, modifiers: modifiers) + shortcut = newShortcut + onShortcutChanged?(newShortcut) + window?.makeFirstResponder(nil) + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let backgroundColor: NSColor = isRecording ? .controlAccentColor.withAlphaComponent(0.1) : .controlBackgroundColor + backgroundColor.setFill() + bounds.fill() + + let text: String + let textColor: NSColor + + if isRecording { + text = placeholder + textColor = .secondaryLabelColor + } else { + text = shortcut.displayString + textColor = .labelColor + } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 13), + .foregroundColor: textColor + ] + + let attributedString = NSAttributedString(string: text, attributes: attributes) + let textSize = attributedString.size() + let textRect = NSRect( + x: (bounds.width - textSize.width) / 2, + y: (bounds.height - textSize.height) / 2, + width: textSize.width, + height: textSize.height + ) + + attributedString.draw(in: textRect) + + if isRecording { + NSGraphicsContext.saveGraphicsState() + NSFocusRingPlacement.only.set() + bounds.fill() + NSGraphicsContext.restoreGraphicsState() + } + } + + override var intrinsicContentSize: NSSize { + NSSize(width: 120, height: 28) + } +} diff --git a/azooKeyMac/Windows/PromptInput/PromptHistoryItem.swift b/azooKeyMac/Windows/PromptInput/PromptHistoryItem.swift index 9ca3a6b6..452f536d 100644 --- a/azooKeyMac/Windows/PromptInput/PromptHistoryItem.swift +++ b/azooKeyMac/Windows/PromptInput/PromptHistoryItem.swift @@ -1,14 +1,29 @@ +import Core import Foundation // Structure for prompt history item with pinned status -struct PromptHistoryItem: Sendable, Codable { +struct PromptHistoryItem: Sendable, Codable, Identifiable { + var id: UUID let prompt: String - var isPinned: Bool = false - var lastUsed: Date = Date() + var isPinned: Bool + var lastUsed: Date + var shortcut: KeyboardShortcut? - init(prompt: String, isPinned: Bool = false) { + init(prompt: String, isPinned: Bool = false, shortcut: KeyboardShortcut? = nil) { + self.id = UUID() self.prompt = prompt self.isPinned = isPinned self.lastUsed = Date() + self.shortcut = shortcut + } + + // 後方互換性のためのカスタムデコーダー + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() + self.prompt = try container.decode(String.self, forKey: .prompt) + self.isPinned = try container.decodeIfPresent(Bool.self, forKey: .isPinned) ?? false + self.lastUsed = try container.decodeIfPresent(Date.self, forKey: .lastUsed) ?? Date() + self.shortcut = try container.decodeIfPresent(KeyboardShortcut.self, forKey: .shortcut) } } diff --git a/azooKeyMac/Windows/PromptInput/PromptInputView.swift b/azooKeyMac/Windows/PromptInput/PromptInputView.swift index d8659726..75b640c6 100644 --- a/azooKeyMac/Windows/PromptInput/PromptInputView.swift +++ b/azooKeyMac/Windows/PromptInput/PromptInputView.swift @@ -11,6 +11,7 @@ struct PromptInputView: View { @State private var hoveredHistoryIndex: Int? @State private var isNavigatingHistory: Bool = false @State private var includeContext: Bool = Config.IncludeContextInAITransform().value + @State private var editingShortcutFor: PromptHistoryItem? @FocusState private var isTextFieldFocused: Bool let initialPrompt: String? @@ -171,6 +172,25 @@ struct PromptInputView: View { .buttonStyle(PlainButtonStyle()) .help(item.isPinned ? "Unpin" : "Pin") + // Shortcut button (only for pinned items) + if item.isPinned { + Button { + editingShortcutFor = item + } label: { + if let shortcut = item.shortcut { + Text(shortcut.displayString) + .font(.system(size: 8, weight: .medium)) + .foregroundColor(.accentColor) + } else { + Image(systemName: "command") + .font(.system(size: 9)) + .foregroundColor(.secondary.opacity(0.4)) + } + } + .buttonStyle(PlainButtonStyle()) + .help(item.shortcut == nil ? "Set shortcut" : "Edit shortcut") + } + // Prompt text Text(item.prompt) .font(.system(size: 11)) @@ -360,6 +380,19 @@ struct PromptInputView: View { hoveredHistoryIndex = nil isTextFieldFocused = true } + .sheet(item: $editingShortcutFor) { item in + ShortcutEditorSheet( + item: item, + allItems: promptHistory, + onSave: { updatedItem in + updateShortcut(for: updatedItem) + editingShortcutFor = nil + }, + onCancel: { + editingShortcutFor = nil + } + ) + } } private func requestPreview() { @@ -411,7 +444,7 @@ struct PromptInputView: View { private func loadPromptHistory() { // Try to load as Data first (new format) - if let data = UserDefaults.standard.data(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory") { + if let data = UserDefaults.standard.data(forKey: Config.PromptHistory.key) { if let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) { promptHistory = history return @@ -424,7 +457,7 @@ struct PromptInputView: View { } // Fallback to string format (legacy) - let historyString = UserDefaults.standard.string(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory") ?? "" + let historyString = UserDefaults.standard.string(forKey: Config.PromptHistory.key) ?? "" if !historyString.isEmpty, let data = historyString.data(using: .utf8) { if let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) { @@ -439,10 +472,12 @@ struct PromptInputView: View { // Add default pinned prompts if history is empty if promptHistory.isEmpty { - let defaultPinnedPrompts = ["elaborate", "rewrite", "formal", "english"] - promptHistory = defaultPinnedPrompts.map { prompt in - PromptHistoryItem(prompt: prompt, isPinned: true) - } + promptHistory = [ + PromptHistoryItem(prompt: "elaborate", isPinned: true), + PromptHistoryItem(prompt: "rewrite", isPinned: true), + PromptHistoryItem(prompt: "formal", isPinned: true), + PromptHistoryItem(prompt: "english", isPinned: true) + ] savePinnedHistory() } } @@ -455,12 +490,29 @@ struct PromptInputView: View { } private func togglePin(for item: PromptHistoryItem) { - if let index = promptHistory.firstIndex(where: { $0.prompt == item.prompt }) { + if let index = promptHistory.firstIndex(where: { $0.id == item.id }) { promptHistory[index].isPinned.toggle() savePinnedHistory() } } + private func updateShortcut(for item: PromptHistoryItem) { + if let index = promptHistory.firstIndex(where: { $0.id == item.id }) { + // Clear conflicting keyboard shortcuts from other items + if let newShortcut = item.shortcut { + for i in promptHistory.indices where i != index { + if promptHistory[i].shortcut == newShortcut { + promptHistory[i].shortcut = nil + } + } + } + + // Update the item + promptHistory[index].shortcut = item.shortcut + savePinnedHistory() + } + } + private func savePromptToHistory(_ prompt: String) { // Check if prompt already exists and preserve its pinned status if let existingIndex = promptHistory.firstIndex(where: { $0.prompt == prompt }) { @@ -468,8 +520,9 @@ struct PromptInputView: View { let existingItem = promptHistory[existingIndex] promptHistory.remove(at: existingIndex) - // Create updated item with new lastUsed time but preserve pinned status - let updatedItem = PromptHistoryItem(prompt: prompt, isPinned: existingItem.isPinned) + // Create updated item with new lastUsed time but preserve pinned status and shortcut + var updatedItem = PromptHistoryItem(prompt: prompt, isPinned: existingItem.isPinned, shortcut: existingItem.shortcut) + updatedItem.id = existingItem.id if existingItem.isPinned { // For pinned items, just update in place (sorting will handle position) @@ -495,7 +548,7 @@ struct PromptInputView: View { private func savePinnedHistory() { if let data = try? JSONEncoder().encode(promptHistory) { - UserDefaults.standard.set(data, forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory") + UserDefaults.standard.set(data, forKey: Config.PromptHistory.key) } } @@ -567,3 +620,107 @@ struct PromptInputView: View { ) .frame(width: 380) } + +// MARK: - Shortcut Editor Sheet +struct ShortcutEditorSheet: View { + @State private var item: PromptHistoryItem + @State private var shortcut: Core.KeyboardShortcut + @State private var hasShortcut: Bool + let allItems: [PromptHistoryItem] + let onSave: (PromptHistoryItem) -> Void + let onCancel: () -> Void + + // Reserved system shortcuts + private var reservedShortcuts: [Core.KeyboardShortcut] { + [ + Config.TransformShortcut().value // いい感じ変換のショートカット + ] + } + + private var conflictingPrompt: String? { + guard hasShortcut else { + return nil + } + return allItems.first(where: { otherItem in + otherItem.id != item.id && + otherItem.shortcut == shortcut + })?.prompt + } + + private var isSystemShortcut: Bool { + guard hasShortcut else { + return false + } + return reservedShortcuts.contains(shortcut) + } + + init(item: PromptHistoryItem, allItems: [PromptHistoryItem], onSave: @escaping (PromptHistoryItem) -> Void, onCancel: @escaping () -> Void) { + self._item = State(initialValue: item) + self._shortcut = State(initialValue: item.shortcut ?? Core.KeyboardShortcut(key: "a", modifiers: .control)) + self._hasShortcut = State(initialValue: item.shortcut != nil) + self.allItems = allItems + self.onSave = onSave + self.onCancel = onCancel + } + + var body: some View { + VStack(spacing: 16) { + Text("Set Shortcut for \"\(item.prompt)\"") + .font(.headline) + + VStack(spacing: 12) { + // Keyboard shortcut + VStack(alignment: .leading, spacing: 4) { + HStack { + Toggle("Keyboard Shortcut", isOn: $hasShortcut) + .toggleStyle(.checkbox) + .font(.caption) + .foregroundColor(.secondary) + } + + if hasShortcut { + KeyboardShortcutRecorder(shortcut: $shortcut) + .frame(height: 40) + + // Conflict warnings + if isSystemShortcut { + Text("This shortcut is reserved for a system function") + .font(.system(size: 9)) + .foregroundColor(.red) + } else if let conflicting = conflictingPrompt { + Text("Already used by \"\(conflicting)\" (will be replaced)") + .font(.system(size: 9)) + .foregroundColor(.orange) + } + } + } + } + + HStack { + Button("Cancel") { + onCancel() + } + .keyboardShortcut(.escape) + + Spacer() + + if hasShortcut { + Button("Remove") { + var updatedItem = item + updatedItem.shortcut = nil + onSave(updatedItem) + } + } + + Button("Save") { + var updatedItem = item + updatedItem.shortcut = hasShortcut ? shortcut : nil + onSave(updatedItem) + } + .keyboardShortcut(.return) + } + } + .padding(20) + .frame(width: 360) + } +} diff --git a/azooKeyMac/Windows/PromptInput/PromptInputWindow.swift b/azooKeyMac/Windows/PromptInput/PromptInputWindow.swift index 0bba9795..861777e1 100644 --- a/azooKeyMac/Windows/PromptInput/PromptInputWindow.swift +++ b/azooKeyMac/Windows/PromptInput/PromptInputWindow.swift @@ -16,6 +16,7 @@ final class PromptInputWindow: NSWindow { private var applyCallback: ((String) -> Void)? private var isTextFieldCurrentlyFocused: Bool = false private var initialPrompt: String? + private var previousApp: NSRunningApplication? init() { super.init( @@ -91,6 +92,12 @@ final class PromptInputWindow: NSWindow { self.applyCallback = onApply self.completion = completion self.initialPrompt = initialPrompt + let frontmostApp = NSWorkspace.shared.frontmostApplication + if let frontmostApp, frontmostApp.processIdentifier != NSRunningApplication.current.processIdentifier { + self.previousApp = frontmostApp + } else { + self.previousApp = nil + } // Reset the window display state resetWindowState() @@ -166,6 +173,13 @@ final class PromptInputWindow: NSWindow { completion(nil) } + // Restore focus to the previous application + if let previousApp = self.previousApp { + DispatchQueue.main.async { + previousApp.activate(options: []) + } + } + super.close() self.completion = nil self.previewCallback = nil