USキーボードを使っています
社用PCも私物のPCも、MacBookのUSキーボード配列のものを利用しています。
USキーボードには良いところ、悪いところありますが、
日本語を日常使いしている身にとって、致命的なのは「かな変換」、「英数変換」キーが無いことだと思います。
最近のMacOSでは、fnキーを単体で押したり、 control + spaceで入力ソースを切り替えることができるようになっています。
しかし、最も使いやすい形は右コマンドが「かな変換」、左コマンドが「英数変換」のようにJISキーボードに近い操作感で変換ができることです。
キーバインド変換用のサードパーティアプリ
上記のようにコマンドキーに変換を割り当てるためのサードパーティアプリが存在します。
https://karabiner-elements.pqrs.org/
karabiner-elementsはかなり高機能なアプリケーションで、コマンドキーに限らず他のキーも自由度高くカスタマイズすることができます。
karabiner-elements設定がおかしくなった
意図したわけではないのですが、自分の何等かの操作によって、コマンドキーとオプションキーが入れ替わるといった事象がありました(現在は解決済み、原因は後述)。
この事象は外部キーボードに限り発現していました。
アプリをダウングレードしても、OSのキーボード設定をいじっても解決しなかったため、karabiner-elementsの代用アプリケーションを自作しようという発想になりました。
技術構成
OSのキーボードAPIを操作する -> Objective-C
CLIを作る -> Go
ということで、GoからObjective-Cを呼ぶ構成で作成します。
作ったもの
こちらのコマンドでダウンロードできます。
$ go install github.com/shuyaeer/joker@latest
cmd/main.goはこんな感じです。
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa -framework ApplicationServices -framework Carbon
#include <Cocoa/Cocoa.h>
#include <ApplicationServices/ApplicationServices.h>
#include <Carbon/Carbon.h>
#include <stdlib.h>
#include <stdbool.h>
#include <dispatch/dispatch.h>
// キーコードの定義
#define LEFT_COMMAND_KEYCODE 55
#define RIGHT_COMMAND_KEYCODE 54
// 入力ソース切り替え関数
void switchToEnglish() {
TISInputSourceRef source = TISCopyInputSourceForLanguage(CFSTR("en-US"));
if (source) {
TISSelectInputSource(source);
CFRelease(source);
}
}
void switchToJapanese() {
TISInputSourceRef source = TISCopyInputSourceForLanguage(CFSTR("ja-JP"));
if (source) {
TISSelectInputSource(source);
CFRelease(source);
}
}
// グローバル変数でCommandキーの状態を管理
static bool leftCommandPressed = false;
static bool rightCommandPressed = false;
// タイマーのディスパッチキュー
static dispatch_queue_t timerQueue;
// タイマーの期間(ナノ秒単位)
#define TIMER_DURATION 50 * NSEC_PER_MSEC // 100ミリ秒
// タイマー設定関数
void setupTimers() {
timerQueue = dispatch_queue_create("com.example.keytimer", DISPATCH_QUEUE_SERIAL);
}
// 左Commandキーのタイマー開始関数
void startLeftCommandTimer() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, TIMER_DURATION), timerQueue, ^{
if (leftCommandPressed) {
switchToEnglish();
leftCommandPressed = false;
}
});
}
// 右Commandキーのタイマー開始関数
void startRightCommandTimer() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, TIMER_DURATION), timerQueue, ^{
if (rightCommandPressed) {
switchToJapanese();
rightCommandPressed = false;
}
});
}
// キーイベントコールバック関数
static CGEventRef keyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
if (type == kCGEventFlagsChanged) {
CGEventFlags flags = CGEventGetFlags(event);
CGKeyCode keycode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
// 左Commandキーの押下/離脱
if (keycode == LEFT_COMMAND_KEYCODE) {
if (!(flags & kCGEventFlagMaskCommand)) {
leftCommandPressed = true;
startLeftCommandTimer();
} else {
leftCommandPressed = false;
}
}
// 右Commandキーの押下/離脱
if (keycode == RIGHT_COMMAND_KEYCODE) {
if (!(flags & kCGEventFlagMaskCommand)) {
rightCommandPressed = true;
startRightCommandTimer();
} else {
rightCommandPressed = false;
}
}
} else if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
CGKeyCode keycode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
// Commandキー以外のキーが押下された場合、Commandキーの単独押下フラグをリセット
if (keycode != LEFT_COMMAND_KEYCODE && keycode != RIGHT_COMMAND_KEYCODE) {
leftCommandPressed = false;
rightCommandPressed = false;
}
}
return event;
}
// イベントタップの設定と開始
void startKeyTap() {
setupTimers();
// イベントマスクの設定
CGEventMask mask = CGEventMaskBit(kCGEventFlagsChanged) | CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp);
// イベントタップの作成
CFMachPortRef tap = CGEventTapCreate(kCGSessionEventTap,
kCGHeadInsertEventTap,
0,
mask,
keyCallback,
NULL);
if (!tap) {
NSLog(@"Failed to create event tap");
exit(1);
}
// ループソースの作成
CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
CGEventTapEnable(tap, true);
// ループの開始
CFRunLoopRun();
}
*/
import "C"
import (
"fmt"
)
func main() {
fmt.Println("Starting key tap...")
go C.startKeyTap()
select {}
}
ほぼObjective-Cの記述になっています。
GoからObjective-Cを呼んでいる処理は以下です。組み込みにC
というライブラリがあり、利用しています。
import "C"
import (
"fmt"
)
func main() {
fmt.Println("Starting key tap...")
go C.startKeyTap()
select {}
}
実行
$ joker
Starting key tap...
Current Input Source: ABC (en)
2024-12-24 17:41:23.406 joker[54764:10070093] Keycode: 54 # 右コマンドを押した
2024-12-24 17:41:23.565 joker[54764:10070093] Keycode: 54 # 右コマンドを離した
Current Input Source: Hiragana (Google) (ja)
オプション等はないです。シンプルにコンソールにjoker
と入力することで、キーボードの入力監視を開始します。
ソースがCurrent Input Source: ABC (en)
からCurrent Input Source: Hiragana (Google) (ja)
に切り替わっていることが確認できます。
感想
GoからObjective-Cを呼び出すことで、MacOSのキーボードAPIを扱うことができるというのは感動でした。
込み入ったものを作るのには向かない構成ではありますが、他にもメニューバーやカーソル等もGoから制御できると、作れるもの可能性が一気に広がった感覚があります。
後から気がついたのですが、Goのみで作成されたライブラリでも、キーボードの操作はできるようです。
https://github.com/gvalkov/golang-evdev
おまけ
このCLIを作るきっかけとなった、karabiner-elementsにて外付けキーボードのバインドがおかしくなるという事象についてですが、現在は解決しています。
~/.config/karabiner/karabiner.json
に設定ファイルがあり、外部キーボードに関する記述を消すことで、解消されました。
"devices": [ "name": "Default profile",
{
"identifiers": {
"is_keyboard": true,
"product_id": 591,
"vendor_id": 1452
},
"treat_as_built_in_keyboard": true
},
{
"identifiers": {
"is_keyboard": true,
"is_pointing_device": true,
"product_id": 591,
"vendor_id": 1452
},
"treat_as_built_in_keyboard": true
}
],
OSとkarabiner-elementsのバージョンを同時期に更新したのですが、それが原因だったのかなぁと思っております。