1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoAdvent Calendar 2024

Day 19

【Go】karabiner-elementの代用アプリを自作してみた

Posted at

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のバージョンを同時期に更新したのですが、それが原因だったのかなぁと思っております。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?