Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5395031
feat: Add custom prompt shortcuts and keyboard shortcut configuration
nyanko3141592 Jan 5, 2026
cba5a90
feat: Add double tap prompt configuration for Eisu and Kana keys
nyanko3141592 Jan 5, 2026
c7de270
feat: Improve layout and messaging in Custom Prompt Shortcuts Editor
nyanko3141592 Jan 5, 2026
dbc082c
feat: Refactor Custom Prompt Shortcuts Editor layout and styling
nyanko3141592 Jan 5, 2026
0dfa86d
feat: Enhance custom prompt shortcuts functionality and UI integration
nyanko3141592 Jan 8, 2026
a17702a
feat: Implement double-tap prompt retrieval and configuration for Eis…
nyanko3141592 Jan 8, 2026
4a9de06
feat: Move Magic Conversion settings from ConfigWindow to PromptInput…
nyanko3141592 Jan 10, 2026
b8c3d5a
fix: Fix swiftlint conditional_returns_on_newline violations
nyanko3141592 Jan 10, 2026
52290e2
fix: Remove trailing newline in CustomCodableConfigItem.swift
nyanko3141592 Jan 10, 2026
c583cd6
Merge remote-tracking branch 'origin/main' into feat/custom-prompt-sh…
nyanko3141592 Jan 10, 2026
4a7d563
fix: Restore Magic Conversion settings to ConfigWindow
nyanko3141592 Jan 10, 2026
79812ba
fix: Remove debug logging code and file headers
nyanko3141592 Jan 13, 2026
8d8b4d2
Merge remote-tracking branch 'origin/main' into feat/custom-prompt-sh…
nyanko3141592 Jan 14, 2026
81bd30b
refactor: Remove redundant comments from KeyboardShortcutRecorder
nyanko3141592 Jan 14, 2026
2c7607c
Merge remote-tracking branch 'origin/main' into feat/custom-prompt-sh…
nyanko3141592 Jan 15, 2026
37144ca
fix: Remove debug code and add backward compatibility decoder
nyanko3141592 Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions azooKeyMac/Configs/KeyboardShortcut.swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルなんだけど、Apple Platform非依存のロジックに関してはCore/側で管理していきたいので、NSEvent部分以外を上手くCore/の方に書いてほしい。

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Cocoa

/// キーボードショートカットを表す構造体
public struct KeyboardShortcut: Codable, Equatable, Hashable, Sendable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyboardShotrcut側にeisuDoubleTapkanaDoubleTapを入れる案はない?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

入れてもいいんだけど将来的になんか困りそうじゃない?そうでもないか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

うーん?具体的なシナリオってある?

public var key: String
public var modifiers: EventModifierFlags

public init(key: String, modifiers: EventModifierFlags) {
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
}
}

/// NSEvent.ModifierFlagsをCodable/Sendableにするためのラッパー
public struct EventModifierFlags: Codable, Equatable, Hashable, Sendable {
private var rawValue: UInt

public init(rawValue: UInt) {
self.rawValue = rawValue
}

public init(from nsModifiers: NSEvent.ModifierFlags) {
self.rawValue = nsModifiers.rawValue
}

public var nsModifierFlags: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: rawValue)
}

public static let control = EventModifierFlags(from: .control)
public static let option = EventModifierFlags(from: .option)
public static let shift = EventModifierFlags(from: .shift)
public static let command = EventModifierFlags(from: .command)

public func contains(_ other: EventModifierFlags) -> Bool {
(rawValue & other.rawValue) == other.rawValue
}
}
74 changes: 74 additions & 0 deletions azooKeyMac/Configs/KeyboardShortcutConfigItem.swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルも同じ理由で、実装可能な部分はなるべくCore/側に置いてほしい。今はほとんどのConfigはCore/側に移動してる。

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@_spi(Core) import Core
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@_spi(Core)って何……?

import Foundation

protocol KeyboardShortcutConfigItem: ConfigItem<KeyboardShortcut> {
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"
}
}

protocol StringConfigItemWithDefault: ConfigItem<String> {
static var `default`: String { get }
}

extension StringConfigItemWithDefault {
public var value: String {
get {
let stored = UserDefaults.standard.string(forKey: Self.key) ?? ""
return stored.isEmpty ? Self.default : stored
}
nonmutating set {
UserDefaults.standard.set(newValue, forKey: Self.key)
}
}
}

extension Config {
/// 英数キーダブルタップのプロンプト
public struct EisuDoubleTapPrompt: StringConfigItemWithDefault {
public init() {}

public static let `default`: String = "english"
public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.eisu_double_tap_prompt"
}

/// かなキーダブルタップのプロンプト
public struct KanaDoubleTapPrompt: StringConfigItemWithDefault {
public init() {}

public static let `default`: String = "japanese"
public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.kana_double_tap_prompt"
}
}
85 changes: 81 additions & 4 deletions azooKeyMac/InputController/azooKeyMacInputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,64 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s
return isDouble
}

// MARK: - ダブルタッププロンプト取得
private func getDoubleTapPrompt(isEisu: Bool) -> String? {
// Check pinned prompts first
if let data = UserDefaults.standard.data(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory"),
let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) {
if let matched = history.first(where: { $0.isPinned && (isEisu ? $0.isEisuDoubleTap : $0.isKanaDoubleTap) }) {
return matched.prompt
}
}

// Fallback to config
if isEisu {
let prompt = Config.EisuDoubleTapPrompt().value
return prompt.isEmpty ? nil : prompt
} else {
let prompt = Config.KanaDoubleTapPrompt().value
return prompt.isEmpty ? nil : prompt
}
}

// MARK: - カスタムプロンプトショートカット検出
private func checkCustomPromptShortcut(event: NSEvent) -> String? {
guard let characters = event.charactersIgnoringModifiers,
!characters.isEmpty else {
return nil
}

let key = characters.lowercased()

// 必要な修飾キーのみをマスクして取得
let relevantModifiers: NSEvent.ModifierFlags = [.control, .option, .shift, .command]
let eventModifiers = event.modifierFlags.intersection(relevantModifiers)

// 修飾キーがない場合は早期リターン(通常の入力)
if eventModifiers.isEmpty {
return nil
}

// Check pinned prompts with shortcuts
guard let data = UserDefaults.standard.data(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory"),
let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) else {
return nil
}

let pinnedWithShortcuts = history.filter { $0.isPinned && $0.shortcut != nil }
if let matched = pinnedWithShortcuts.first(where: { item in
guard let itemShortcut = item.shortcut else {
return false
}
let shortcutModifiers = itemShortcut.modifiers.nsModifierFlags.intersection(relevantModifiers)
return itemShortcut.key == key && eventModifiers == shortcutModifiers
}) {
return matched.prompt
}

return nil
}

override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
let applicationDirectoryURL = if #available(macOS 13, *) {
URL.applicationSupportDirectory
Expand Down Expand Up @@ -229,6 +287,19 @@ 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
}
}
}
}

let userAction = UserAction.getUserAction(eventCore: event.keyEventCore, inputLanguage: inputLanguage)

// 英数キー(keyCode 102)の処理
Expand All @@ -238,8 +309,11 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s
if isDoubleTap {
let selectedRange = client.selectedRange()
if selectedRange.length > 0 {
if self.triggerAiTranslation(initialPrompt: "english") {
return true
// Check pinned prompts for Eisu double-tap
if let prompt = getDoubleTapPrompt(isEisu: true) {
if self.triggerAiTranslation(initialPrompt: prompt) {
return true
}
}
}
if !self.segmentsManager.isEmpty {
Expand All @@ -256,8 +330,11 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s
if isDoubleTap {
let selectedRange = client.selectedRange()
if selectedRange.length > 0 {
if self.triggerAiTranslation(initialPrompt: "japanese") {
return true
// Check pinned prompts for Kana double-tap
if let prompt = getDoubleTapPrompt(isEisu: false) {
if self.triggerAiTranslation(initialPrompt: prompt) {
return true
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions azooKeyMac/InputController/azooKeyMacInputControllerHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ extension azooKeyMacInputController {
self.appMenu.autoenablesItems = true
self.liveConversionToggleMenuItem = NSMenuItem(title: "ライブ変換", action: #selector(self.toggleLiveConversion(_:)), keyEquivalent: "")
self.appMenu.addItem(self.liveConversionToggleMenuItem)
self.transformSelectedTextMenuItem = NSMenuItem(title: TransformMenuTitle.normal, action: #selector(self.performTransformSelectedText(_:)), keyEquivalent: "s")
self.transformSelectedTextMenuItem.keyEquivalentModifierMask = [.control]

// ショートカット設定を読み込み
let shortcut = Config.TransformShortcut().value
self.transformSelectedTextMenuItem = NSMenuItem(
title: TransformMenuTitle.normal,
action: #selector(self.performTransformSelectedText(_:)),
keyEquivalent: shortcut.key
)
self.transformSelectedTextMenuItem.keyEquivalentModifierMask = shortcut.modifiers.nsModifierFlags
self.transformSelectedTextMenuItem.target = self
self.appMenu.addItem(self.transformSelectedTextMenuItem)
self.appMenu.addItem(NSMenuItem.separator())
Expand Down
Loading