⌨️

自作キーボードのキーマップ最適化のためにキー入力分析基盤を作ってみた(前編)

2024/12/14に公開

この記事は、tacoms Advent Calendar 2024の13日目です!
他メンバーのAdvent Calendarはこちらからご覧ください!👇
https://qiita.com/advent-calendar/2024/tacoms

はじめに

私たちの仕事において1日で最も触れるものといえばやはりキーボードでしょう。
大事な仕事道具ゆえにこだわっている方も多いかと思います。

弊社にもこだわりのキーボードをお持ちのエンジニアが多数在籍しています!💪
https://zenn.dev/tacoms/articles/20241203-desk-tour-of-tacoms-vol-1

そしてこだわった末に、自作キーボードに行き着いた(沼にハマった)方も多いのではないでしょうか。

自作キーボードの魅力は、好みの色のキーキャップや、好みの打鍵感のキースイッチにカスタマイズできる点が挙げられるでしょう。
さらにキーの物理的な配置の設計から行うことで、左右を分割・独立させたり、トラックボールを搭載したりと、どこまでも理想のキーボードを追求してカスタマイズすることができます。

私が利用しているキーボードは左右分割型でトラックボールを搭載した Keyball44 というものです。


実際に利用しているキーボード。左右お好みの方向にトラックボールを搭載可能で、トラックボールケースは射出成形という個人が発注するレベルではないこだわりようです。さらには、、(いっぱい喋れてしまうのでまたの機会に。。!)

画像を見ていただくとわかるとおり、標準的なキーボードと比べてキー数が少なく、44キーしかありません。[1] [2]
このようなキー数の少ないキーボードでは、レイヤー(特定のキーを押しているときだキーの役割を変更する fn キーのようなもの)やタップダンス(通常の打鍵と長押しの打鍵で入力値を変える機能)などを駆使してキーマップを工夫する必要があります。

使い始めて1年ほど経ちますが、未だにキーマップにしっくりきていません!😭
特に記号と装飾キーの配置が難しいです。。
当初はUS配列をベースに感覚で設定したのですが、まだまだ改善の余地があるようです。

そこで今回は、データに基づいた最適化のために、実際の入力データを収集・可視化するシステムの構築に挑戦してみました。

この記事ではその前編として、入力データを監視・出力するキーロガーの実装を中心にご紹介します。

できたもの


キーロガーを実装してキー入力のログを取り、Logstash 経由で Elasticsearch に取り込んで Kibana で可視化しています。
リアルタイムで蓄積され Kibana に反映されるようにしました。[3]

どうやって最適なキーマップを決めるか

キーマップというと記号や装飾キーの配置など細かな部分を想像すると思います。
しかし、アルファベットの配置まで考えればそれはキー配列を考えることと同じであり、キー配列の最適化には先行研究がたくさんあります。

以下の記事では QWERTY 配列を含む様々なキー配列のベンチマークを比較しています。[4]
ローマ字入力に最適なキー配列を考える(比較編)

また、ベンチマークから最適なキーマップを考案する際にはこちらの記事が大変参考になります。[5]
ローマ字入力に最適なキー配列を考える(制作編)

これらの研究は貴重な参考材料ですが、私の使っている Keyball はいかんせんキーが少ないため、数字、記号、装飾キーなどは異なる配置を検討する必要があります。
また、多くの先行研究は小説など文章を主としてベンチマークを取っていますが、私が課題を感じているのは主に記号と装飾キー、制御キーの配置です。

そこで今回は自分に最適化されたキーマップを考えるために、自分が日々入力したキーを蓄積し分析できる基盤を作ってみたいと思います。

なお macOS Sequoia 15.0.1 でのみ動作確認しています。

構成

今回はキーロガーで出力されたログを ELK スタックで可視化するようにしました。

  • Keylogger
    • Go 言語で keylogger コマンドとして実装。起動するとキー入力イベントのログを標準出力に出力し続ける。
    • 別途 macos の LaunchAgents に登録してログイン時に起動しバックグラウンドで動作させる。出力はログファイルにリダイレクトさせる。
  • Elasticsearch
    • 言わずと知れた分散型の検索・分析エンジン。
  • Logstash
    • データの収集・変換・送信を行うツール。
    • 今回はログファイルを Elasticsearch に送信する役割。
  • Kibana
    • データの可視化・分析を行うツール。
    • 今回は Elasticsearch に接続し収集したログを元に可視化を行う。

Keylogger の実装

まずは肝心のキーロガーを実装していきます。
名前はシンプルに keylogger とします。

以下のような構成になりました。

.
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── internal
│   ├── core
│   ├── event
│   └── keymap
├── log
│   ├── error.log
│   └── key.log
├── main.go
├── Makefile
└── scripts
    └── com.github.momiom.keylogger.plist

ざっくり以下のように分かれています。

  • CLI 部分
  • キー入力の監視・出力を行うキーロガー
  • ログイン時にバックグラウンドで起動させるための設定ファイル
  • ビルド、インストールを行う Makefile

CLI の実装

CLI 部分の実装には Cobra を利用します。
Cobra は Hugo, Kubernetes (Kubectl), GitHub CLI などで採用されていコマンドラインツール作成ライブラリです。[6]
サブコマンドを複数持つ Git コマンドのような CLI がサクッと作れるのでおすすめです!

すごく簡単な実装なので詳細は割愛いたします🙏
基本的な使い方は以下の記事等が大変参考になります!

ただし今回は色々と調整していたらただ起動するだけのコマンドになったため、あまり活用できていません😅

キーロガーの実装

メインとなるキー入力の監視は robotn/gohook を利用します。[7]
gohook は以下のように hook を開始するだけで手軽にキー入力の他、マウスのイベントも取得することができます。
今回は試していませんが、コアは C 言語で書かれており、Windows, Linux にも対応しているとのことです。

package main

import (
	"fmt"
	hook "github.com/robotn/gohook"
)

func main() {
	evChan := hook.Start()
	for ev := range evChan {
		fmt.Printf("event: %+v\n", ev)
	}
}
❯ go run main.go 
event: 2024-12-12 09:10:14.887442 +0900 JST m=+0.102612543 - Event: {Kind: HookEnabled}
event: 2024-12-12 09:10:19.330584 +0900 JST m=+4.545751834 - Event: {Kind: KeyHold, Rawcode: 0, Keychar: 65535}
event: 2024-12-12 09:10:19.33066 +0900 JST m=+4.545828584 - Event: {Kind: KeyDown, Rawcode: 0, Keychar: 97}
event: 2024-12-12 09:10:19.381739 +0900 JST m=+4.596907126 - Event: {Kind: KeyUp, Rawcode: 0, Keychar: 65535}
event: 2024-12-12 09:10:20.602029 +0900 JST m=+5.817196334 - Event: {Kind: KeyHold, Rawcode: 11, Keychar: 65535}
event: 2024-12-12 09:10:20.602049 +0900 JST m=+5.817216126 - Event: {Kind: KeyDown, Rawcode: 11, Keychar: 98}
event: 2024-12-12 09:10:20.704267 +0900 JST m=+5.919434001 - Event: {Kind: KeyUp, Rawcode: 11, Keychar: 65535}
event: 2024-12-12 09:10:23.965205 +0900 JST m=+9.180369834 - Event: {Kind: MouseHold, Button: 1, X: 887, Y: 1004, Clicks: 1}
event: 2024-12-12 09:10:23.965213 +0900 JST m=+9.180378334 - Event: {Kind: MouseDown, Button: 1, X: 887, Y: 1004, Clicks: 1}
event: 2024-12-12 09:10:23.965215 +0900 JST m=+9.180380084 - Event: {Kind: MouseUp, Button: 1, X: 887, Y: 1004, Clicks: 1}

今回はマウスイベントなどは不要なので、独自の Event 構造体を作成し必要なイベントのみ格納します。

type Event struct {
	Type      string    `json:"type"`
	Keycode   uint16    `json:"keycode"`
	Rawcode   uint16    `json:"rawcode"`
	Keychar   rune      `json:"keychar"`
	Timestamp time.Time `json:"timestamp"`
}

for ev := range evChan {
    e := event.Event{
        Type:      event.ConvertKindToString(ev.Kind),
        Keycode:   ev.Keycode,
        Rawcode:   ev.Rawcode,
        Keychar:   ev.Keychar,
        Timestamp: ev.When,
    }
    fmt.Printf("event: %+v\n", ev)
}

また、このままではキー情報が Keycoode や Rawcode で出力されていてわかりにくいため、実際に画面に印字された文字に変換します。
gohook にはこれを実現する RawcodetoKeychar という便利な関数がありますが、アルファベット・数字は変換できるものの、装飾キーや制御キー(Enter, Space など)が上手く変換できませんでした。(ライブラリコードを見ると定義はされているようですが。。)

そこで力技ですが、全ての装飾キー・制御キーを打鍵して Rawcode を出力し、その結果から Rawcode をキー名に変換する RawcodeToKeyname 関数を作成します。

このような単純かつ面倒な作業こそ AI さんにうってつけなので、Cursor の Sonoet3.5 さんに依頼してやってもらいます。

以下はキーボードのキーを option, command-left, eisu, space, kana, command-right, fn, arrow-left, arrow-up, arrow-right, arrow-down, shift-left, shift-right, control, enter, tab, backspace, delete, esc の順に打鍵した際のログ出力です。
ログ出力に含まれる Rawcode と上記の打鍵を参考に、 Rawcode を打鍵されたキーに変換する関数 `RawcodeToKeyname` を作成してください。

# ログ出力
{実際のログ出力}

結果以下のような関数ができました!さすがです👍

func RawcodeToKeyName(rawcode uint16) string {
	switch rawcode {
	case 58:
		return "option"
	case 55:
		return "command-left"
	case 102:
		return "eisu"
// 中略
	default:
		return "unknown"
	}
}

ついでにアルファベットや数字の面倒もみてもらいました。これで全ての入力を印字に変換できるようになりました!

func GetKeyName(rawcode uint16) string {
	// まず独自のマッピングを試す
	keyName := RawcodeToKeyName(rawcode)
	if keyName != "unknown" {
		return keyName
	}
	// 独自のマッピングで見つからなかった場合は hook.RawcodetoKeychar を使用
	return hook.RawcodetoKeychar(rawcode)
}

ログイン時にバックグラウンド起動させる

今回は macos が対象なので LaunchAgents を利用します。
以下に大変詳しい解説があるのでご参照ください🙇‍♂️
https://dev.classmethod.jp/articles/mac-launch-agents/

今回作成した plist は以下です。標準出力・エラー出力をログファイルに書き出すように設定しました。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.github.momiom.keylogger</string>
        <key>ProgramArguments</key>
        <array>
            <string>/Users/xxx/dev/github.com/momiom/keylogger-visualization/keylogger/bin/keylogger</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <true/>
        <key>StandardOutPath</key>
        <string>/Users/xxx/dev/github.com/momiom/keylogger-visualization/keylogger/log/key.log</string>
        <key>StandardErrorPath</key>
        <string>/Users/xxx/dev/github.com/momiom/keylogger-visualization/keylogger/log/error.log</string>
    </dict>
</plist>

このファイルを ~/Library/LaunchAgents に配置すると、アプリケーションのインストールでよく見るログイン項目の追加の通知が来ます。
これでログイン時にバックグラウンドで起動するようになりました!

ここでは確認のため手動で起動してみます。
go build で実行ファイルを作成し、plist を ~/Library/LaunchAgents に配置後、以下のコマンドを実行します。

> launchctl load ~/Library/LaunchAgents/com.github.momiom.keylogger.plist

初回の起動時は macos のアクセシビリティの権限がないためエラーになってしまいます。

システム設定 > プライバシーとセキュリティ > アクセシビリティ で keylogger の実行ファイルを追加します。
その後、一度以下のコマンドで停止し、再度 load することで起動できます。

> launchctl unload ~/Library/LaunchAgents/com.github.momiom.keylogger.plist
> launchctl load ~/Library/LaunchAgents/com.github.momiom.keylogger.plist

指定したディレクトリにログファイルが作成され、ログが書き込まれていれば成功です!
止める際は以下のコマンドを実行します。

ビルド・インストール

最後にビルドとインストールを手軽にするために Makefile を作成します。

test コマンドでは Go 言語のテストと合わせて plist のチェックもしています。(今回はテスト書いてないですが。。)
また install コマンドで LaunchAgents の plist を配置するようにしています。
その他手動で起動・停止するために start, stop のコマンドも定義しておきました。

.PHONY: build test clean install

GO      = go
GOOS    = darwin

build:
	env GOOS=$(GOOS) $(GO) build -ldflags="-s -w" -o bin/keylogger

test:
	env GOOS=$(GOOS) $(GO) test -v ./...
	plutil -lint ./scripts/com.github.momiom.keylogger.plist

clean:
	rm -rf bin/keylogger

install: build
	cp ./scripts/com.github.momiom.keylogger.plist ~/Library/LaunchAgents/
	launchctl unload ~/Library/LaunchAgents/com.github.momiom.keylogger.plist 2> /dev/null
	launchctl load ~/Library/LaunchAgents/com.github.momiom.keylogger.plist

start:
	launchctl unload ~/Library/LaunchAgents/com.github.momiom.keylogger.plist 2> /dev/null
	launchctl load ~/Library/LaunchAgents/com.github.momiom.keylogger.plist

stop:
	launchctl unload ~/Library/LaunchAgents/com.github.momiom.keylogger.plist

動作確認

以下を実行することでビルド、インストールができます。

> make
> make install

作成されたログファイルを tail -f してキー打鍵のたびにログが出ていればキーロガーの実装は完了です!!

後編に続く。。!

執筆遅延が 記事が長くなってしまったので ELK スタックの構築と可視化は 自作キーボードのキーマップ最適化のためにキー入力分析基盤を作ってみた(後編)」でお届けします!

後編では以下のトピックを予定しています!お楽しみに😃

  • ELK スタックの構築
  • Kibana の Canvas で可視化
脚注
  1. 一般的なテンキー付きキーボードなら108キー程度です。 ↩︎

  2. 39キーの Keyball39, 61キーの Keybal61 もあります。 ↩︎

  3. 実はキーボードの部分はキーが足りなかったり [{ が別々にカウントされていたり、そもそも Keyball のキー配置でなかったりと改善の余地があります。。後編までにがんばって更新します💪💪 ↩︎

  4. 一般的にキー配列といえば QWERTY 配列ですが、実は運指としてはとても効率が悪い事がわかっています。 ↩︎

  5. 考案された「大西配列」はローマ字入力において指の移動距離などが最小水準になっています。効率を求める方はぜひ挑戦してみては。 ↩︎

  6. Cabra の作者は Hugo にも関わっていて、つよつよエンジニアな方です。 ↩︎

  7. 色々と調べているうちに同作者の go-vgo/robotgo という Go 言語で GUI オートメーションが作れるライブラリを見つけました。 OpenCV で画面上の要素をパターンマッチングしてクリックなどできるようで、おもしろそう。 ↩︎

tacomsテックブログ

Discussion