この文書は
TODO6.mdを起点とした設計検討と、その後の実装済み仕様の記録である。実装済みの内容を「現在の仕様」、未実装の案を「将来」として区別する。
MacTcode の入力操作を JSON Lines(JSONL)形式で記録し、ログからキーボードの打鍵履歴を可視化するアニメーションを生成する。
対象は次の二つである。
- MacTcode IME に組み込むキーロガー
- JSONL を読み、アニメーションを出力する Ruby 製 visualizer
キーボードの打鍵表示、未確定キー列に対する次打鍵候補、最近確定した文字列の表示を行う。
- メニューから記録を開始・停止する。
- ロガーの有効/無効状態は IME プロセスのメモリ内だけで保持する。
- IME プロセスの再起動後、ロガーは停止状態とする。設定ファイルへ状態を保存・復元しない。
- 設定リロード時は、記録中であってもロガーを停止する。
- 停止、設定リロード、アプリ終了時には、保留中の出力をフラッシュしてファイルを閉じる。
- ログ書き込みに失敗しても通常の IME 入力は継続し、ロガーだけを停止する。
- 記録中であることはメニューのチェック状態などで判別可能にする。
- 設定ディレクトリ配下の
logs/サブディレクトリへ保存する。 - セッションごとに開始時刻を含む新規ファイルを作る。
- 例:
keystrokes-20260919-123456.jsonl - 同じ秒に複数のセッションを開始した場合は、既存ログを上書きせず
keystrokes-20260919-123456-1.jsonlのように連番を付ける。 - 初版では自動削除を行わない。将来、保存期間・件数・合計容量による削除設定を追加できるようにする。
固定ファイル名の上書きではなくセッション単位の新規作成とする。これにより、記録を失わず、複数セッションの比較や動画生成対象の選択が可能になる。
excludedAppsに該当するアプリケーション上での入力は記録しない。- excluded アプリケーションのアプリ名、ウィンドウタイトル、キー入力、確定文字列をログへ書き込まない。
- ログには入力内容が含まれるため、記録中であること、保存場所、ログを共有する際の注意をドキュメントに明記する。
1行を1イベントとする JSONL を使う。先頭にはセッションのメタデータを置き、以後に時系列イベントを記録する。
各イベントは少なくとも次を持つ。
| 項目 | 説明 |
|---|---|
schemaVersion |
ログ形式のバージョン。初版は 1。 |
sequence |
セッション内で単調増加する連番。同一ミリ秒のイベント順を保証する。 |
timestamp |
人が確認できる壁時計時刻(ISO 8601、ミリ秒・タイムゾーン付き)。 |
elapsedMilliseconds |
セッション開始からの経過ミリ秒。再生時刻の基準。 |
type |
イベント種別。 |
現在のセッション開始イベントにはアプリケーション版を記録する。visualizer は再生に Swift の変換ロジックや辞書を再実装しないことを原則とする。
初版で次のイベントを記録する。
| イベント | 用途 |
|---|---|
keyInput |
生のキー入力。キーボードの打鍵表示に使用する。 |
modeChanged |
T-Code、全角、半角カナ、行、部首合成、交ぜ書きなどのモード遷移を表す。 |
pendingChanged |
未確定キー列と、次打鍵候補の変化を表す。 |
textCommitted |
文字列の挿入・確定を表す。 |
textDeleted |
後方削除など、文字列の削除を表す。 |
textReplaced |
既存文字列を別の文字列へ置換したことを表す。 |
passthrough |
直前の keyInput を IME が処理せず入力先へ渡したことを表す。 |
pendingKakuteiCancel |
PendingKakutei を取り消したことを表し、Cancel キーを強調する。 |
sessionStarted / sessionStopped |
記録セッションの開始・終了を表す。 |
パススルーイベントも記録対象とする。現在は reason(directMode、modeForward、unhandled など)で、IME が入力を処理しなかった理由を記録する。直前の keyInput と組み合わせて再生するため、visualizer は passthrough 自体にキー情報を持たせない。
pendingChanged は次の項目を持つ。
| 項目 | 説明 |
|---|---|
keys |
pending 中のキーの文字列表現。 |
count |
pending のキー数。 |
nextChars |
次の基本キーに対応する候補文字列の配列。 |
基本キーはファンクションキー以外の40個のキーを指す。nextChars は基本キーと同じ順序の40要素配列であり、各要素は次の基本キーを押したときに確定する文字列である。出力が未定義、アクション、または次のキーマップへ進む場合は空文字列とする。
{
"type": "pendingChanged",
"keys": "f",
"count": 1,
"nextChars": ["", "あ", "", "…"]
}pending が空の場合は nextChars: [] を記録する。これは基本キー上の候補表示を消す指示である。
keyInput は物理的・論理的な打鍵表示のため、textCommitted、textDeleted、textReplaced は最近入力した文字列を再生するために使用する。キー列から Ruby 側で IME の変換を再実装して確定文字列を推測してはならない。
新しい文字列を確定・挿入した場合に用いる。
{
"type": "textCommitted",
"text": "あ",
"source": "tcode"
}Backspace などで確定済み文字列を削除した場合に用いる。削除対象の文字列を取得できる経路では text に記録する。取得できない経路では text を省略でき、その場合 visualizer は表示用バッファの末尾1文字を削除する。
{
"type": "textDeleted",
"text": "あ",
"source": "backspace"
}変換確定、変換確定の取消し、その他の置換を表す。削除イベントと確定イベントの連続へ分解せず、必ず1イベントで表す。
{
"type": "textReplaced",
"replacedText": "かんじ",
"text": "漢字",
"source": "mazegaki"
}PendingKakutei の取消しは逆向きの置換として記録する。
{
"type": "textReplaced",
"replacedText": "漢字",
"text": "かんじ",
"source": "pendingKakuteiCancel"
}PendingKakuteiMode は、すべての確定文字列を通過する共通状態ではない。
- 交ぜ書きは MRU 学習が有効な場合に
PendingKakuteiを作る。 - 部首合成は自動学習が有効な場合に
PendingKakuteiを作る。 - 通常の T-Code 入力、学習が無効な交ぜ書き・部首合成、全角、半角カナ、行、ダイレクトモード、自己挿入などは
PendingKakuteiを作らず、直接テキストを操作しうる。
PendingKakutei がある変換では、保持している変換前の yomiString と変換後の kakuteiString を用いて textReplaced を正確に作れる。また取消し時も同じ情報で逆方向の textReplaced を作れる。
ただし、テキスト変更ログ全体を PendingKakutei のみで記録してはならない。実装では次の二層に分ける。
Client/ContextClientなど、実際に挿入・削除・置換を要求する境界で、汎用的なテキスト変更を記録する。- 部首合成、交ぜ書き、
PendingKakuteiは、sourceや置換前後の文字列を補足して変換の意味を記録する。
これにより、通常入力を取りこぼさず、変換操作についても意味のある再生ができる。
- Ruby スクリプトとして実装する。
- ログ読み込み・状態再生、フレーム描画、エンコードを分離する。
- 各フレームは SVG として描画し、エンコーダ固有の API へ描画ロジックを密結合させない。
- タイトルには
MacTcode 打鍵ログと、sessionStarted.timestampから得る録画開始時刻をYYYY-mm-dd HH:MM:SS形式で表示する。 - モード表示は Swift のクラス名をそのまま表示せず、名前空間と末尾の
Modeを除いた小文字の名前を表示する。例:MacTcode.ZenkakuModeはzenkaku。
--typeはgifまたはmp4を受け付け、既定値はmp4。- GIF の生成には ImageMagick の
magickコマンドを使用する。 - MP4 は一時 GIF を経由して
ffmpeg -y -i <gif> <mp4>で生成する。 - MP4 出力時に
ffmpegがなければ、起動直後に Homebrew のインストール例を表示して終了する。 --outputに拡張子がなければ、--typeに応じて.gifまたは.mp4を付加する。- MP4 用の中間 GIF は一時ディレクトリにのみ作成し、出力先には残さない。
- SVG フレームはそれぞれ静止 GIF へ変換する。この変換は8ワーカーで並列に実行し、
Converting frames: n/totalの進捗を表示する。 - 個別フレームの変換失敗はすべて収集し、1件でも失敗した場合は動画 GIF の合成・MP4 変換・最終出力の更新を行わない。
- すべての静止 GIF の変換成功後、順番に並べて1回の
magickコマンドで動画 GIF へ合成する。
| 形式 | 位置付け |
|---|---|
| GIF | --type gif で出力する共有・確認用形式。 |
| MP4 | 既定の出力形式。長時間ログに適する。 |
| WebP | 未実装の将来候補。 |
- フレーム時刻は
0, 1000 / fps, 2 * 1000 / fps, ...ミリ秒とする。 - 各フレームでは、その時刻以下のイベントを
sequence順にすべて適用する。 - 同一フレームに複数の打鍵がある場合も、各キーの強調終了時刻を個別に管理する。
- 動画内の時刻表示は経過時間を秒単位で表示する。ミリ秒はログ内だけに保持する。
打鍵後にキーを赤く表示する期間は、ログ形式ではなく visualizer のコマンドラインオプションで決める。
ruby scripts/visualize_keystrokes.rb INPUT.jsonl --fps 30 --key-highlight-frames 9--fpsの初期値は30。--key-highlight-framesの初期値は9(30 FPS で 0.3 秒)。- 強調時間は
key-highlight-frames / fps秒となる。
- 4行・40個の基本キーと、Sym、Space、Delete、Enter、Cancel のファンクションキーを描画する。Escape キーは描画しない。
- 基本キーのキートップには通常のキーラベルを印字しない。ファンクションキーにはラベルを表示する。
[,],-,=,'はレイアウトへ含めない。- 基本キーの位置は
KEYCODES定数で定義した macOS virtual key code と対応付ける。ハイライト位置は入力文字列ではなくkeyInput.keyCodeから決めるため、Dvorak などの入力配列でも物理位置が正しい。 - 打鍵キーは赤く強調する。
- 経過時間、モード、pending を表示する。
pendingChanged.keysの末尾がバックスラッシュの場合は Sym キーを強調する。 - 最近入力した文字列はキーボード上方に表示し、Ruby の
each_char単位で末尾40文字までを維持する。
pendingChanged.nextCharsが40要素なら、各基本キーに対応する文字列を薄い色で描画する。nextCharsが空配列、存在しない、または40要素でない場合は、通常の候補文字をすべて消す。- 1打鍵目のキーが候補文字を持つ場合、そのキーが赤く強調されている間も候補文字は薄い色のまま表示する。
- 2打鍵目では、押したキーに表示済みの候補文字をハイライト期間中保持する。直後に空の
nextCharsが届いても消さない。 - printable な passthrough の場合は、基本キーのハイライト期間中に直前の
keyInput.textを表示する。 - ハイライト中に保持する文字は赤い背景上で見やすくするため、白・太字・通常候補より大きい文字で表示する。
pendingKakuteiCancelでは Cancel キーを強調する。
visualizer は textCommitted、textDeleted、textReplaced を順に適用して表示用のテキストバッファを維持する。
- 表示は最大40文字とする。
- 現在の visualizer では Ruby の
each_char単位で保持・切り詰める。 - カーソル移動、範囲選択、任意位置への編集は初版の対象外とする。
- 変換中の文字列ではなく、確定したテキストだけを表示対象とする。
keyInput.modifiersの配列順序・将来追加する修飾キー名- passthrough の
reasonの列挙値を公開仕様として固定すること - セッション開始イベントに設定識別情報や辞書版を追加するか
- WebP 出力の追加方法
これらは、本書のイベント種別とライフサイクルの確定事項を変更せずに決定する。