「なぜ awase は難しいアプリでも動くのか」を、コードなしで技術的に説明します。
awase はキーボードドライバとアプリケーションの「間」に割り込んでいます。
Windows にはキーボードの入力を全アプリの手前で受け取れる仕組みがあります。 ローレベルキーボードフック(WH_KEYBOARD_LL)と呼ばれるもので、 物理キーが押されるたびに awase が最初に通知を受け取れます。
ただし「横取り」には制限があります。処理に手間取ると Windows がフックを強制解除してしまうのです。 awase はすべての判断をできるだけ素早く終わらせ、重い処理は別スレッドに渡すことで この制限に対応しています。また解除された場合も自動で再登録します。
awase は一度すべてのキーを飲み込み、処理後に送り直します。
通常のキーボードフックには2つの動作モードがあります。
| フィルターモード | リレーモード(awase のデフォルト) | |
|---|---|---|
| 動作 | 関係するキーだけ横取り、他は素通し | すべてのキーをいったん飲み込み、処理後に送り直す |
| 他フックとの共存 | 順番によっては競合 | 再注入なので他のフックにもキーが届く |
| 安定性 | タイミングに依存しやすい | FIFO キューで順番が保証される |
リレーモードでは、awase はキーを受け取ったら必ず自分で送り直す必要があります。 このとき「自分が送ったキーを自分でまた受け取る」という無限ループが起きないよう、 再注入したキーには目印(マーカー)を付けてフックで素通りさせています。
03親指シフトの核心は「2つのキーが同時に押された」と判断する部分です。
NICOLA 親指シフトでは、文字キーと親指キーが100ms以内に重なったとき「同時打鍵」と判定します。 しかしコンピュータには「同時」という概念がなく、必ずどちらかが先に来ます。 しかも高速タイピングでは、前の親指キーを離す前に次の文字キーが来ることもあります。
これを解決するのが 状態機械(FSM: Finite State Machine) と タイマー の組み合わせです。 awase はキーが来るたびに「現在の状態」と「キーの種類」から次の状態へ遷移し、 どこかのタイミングで「これは同時打鍵だ」または「これは単独打鍵だ」と確定させます。
ケース3の「3打鍵問題」では、状態機械が d1(文字1→親指)と d2(親指→文字2)の時間差を比べます。 d1 < d2 なら文字1と親指が近いので「文字1+親指を同時打鍵」、 d1 ≥ d2 なら文字2と親指が近いので「文字2+親指を同時打鍵」と判断します。 しかし d1 と d2 が拮抗しているとき、タイミングだけでは決めきれません。
wait)、
とりあえず出力してシフトキーが来たら差し替える(speculative)など5種類あり、
タイピングスタイルに合わせて選べます。
3打鍵問題でタイミングが拮抗しているとき、「日本語として自然か」で決着をつけます。
高速タイピングでは、文字キー・親指キー・次の文字キーが3つ同時に重なる「3打鍵問題」が起きます。 d1 と d2 がほぼ同じで、タイミングだけでは「前の文字と親指が組む(シフト文字)」か 「後の文字と親指が組む(シフト文字)」か判断できないケースです。
awase の ngram_predictive モードはこの曖昧さを日本語統計で解消します。
タイミングが拮抗しているとき、2通りの解釈それぞれの「ひらがなの連なり」を
n-gram コーパスに照らして、より自然な日本語になるほうを選びます。
このコーパスは大量の日本語テキストから学習した統計データです。 「ひらがな2文字の組み合わせがどれだけ頻繁に現れるか」を数値化したもので、 awase に同梱されています。
05「今 IME は ON なのか OFF なのか」を awase はどうやって知るのか。
親指シフト入力を有効にするには「IME が ON の状態」が必要です。 しかし Windows の IME 状態は、外部のプログラムから確実に取得できない場合があります。 特に Google 日本語入力では、内部状態を正確に読み取る公開 API がありません。
awase はひとつの IME 状態ではなく、3つの異なる「状態」を管理しています。
「意図」と「実態」がずれることがあります。たとえばアプリ切り替え時に アプリ側が独自に IME を OFF にすることがあります。 awase はこのずれを定期的な観測(ポーリング)で検出し、 ずれていれば自動的に修正します。
従来の親指シフトツールの多くは「IME をトグルする」という方式を使っていました。 「今 ON なら OFF に、OFF なら ON に」という操作です。 この方式の弱点は、現在の状態を正確に知らないと意図と逆になることです。
awase は「IME を ON にする」「IME を OFF にする」という 冪等(べきとう)な操作を使います。 何回実行しても結果が変わらない操作です。 もし状態がずれていても「ON にする」を実行すれば必ず ON になります。
awase はバックグラウンドで複数の「プローブ」を非同期に走らせています。 プローブが結果を返してきたとき、その観測は本当に「今フォーカスしているウィンドウ」のものでしょうか?
たとえば ALT+TAB でアプリを切り替えるとき、OS は一瞬だけタスクスイッチャーのウィンドウにフォーカスを移します。 このわずかな時間に起動したプローブが「IME OFF」と返してくると、 元のアプリに戻った後もエンジンが停止してしまいます。
awase はこれを「エポック(epoch)」で解決しています。 フォーカスが変わるたびに内部カウンタ(FocusEpoch)を 1 増やします。 プローブはスタート時のエポック番号を記録しておき、 結果が届いたときに現在のエポック番号と照合します。 番号が違えば「その間にフォーカスが変わった = stale」と判断して結果を棄てます。
AcceptedObservation というトークンが発行され、
このトークンなしでは write_* 関数を呼べません。
将来プローブを追加した際も、照合を忘れると コンパイルエラーになります。
「IME を ON にしてください」という指示の届け方が、アプリによって異なります。
Windows のアプリが IME と通信する方法は大きく2種類あります。 古い IMM32 という仕組みと、新しい TSF(Text Services Framework) です。 さらに Google 日本語入力は独自の内部構造を持ちます。 awase はアプリの種類を自動判定し、3段階の制御方式を試みます。
アプリの IME ウィンドウに直接「ON にしてください」というメッセージを送る。 最も確実な方法で、成功すれば即座に状態が変わったことを確認できます。
対象: メモ帳・ワードなど一般的なアプリ
Windows の仮想キー VK_IME_ON(ひらがなモードへ)と VK_IME_OFF(直接入力へ)を送る。 冪等な操作なので、何度送っても意図通りになります。
対象: Google 日本語入力が起動しているすべてのアプリ
半角/全角キー(VK_KANJI)を送って IME を切り替える。 トグル操作なので現在の状態を正確に把握していないと意図と逆になる危険があります。 awase は shadow モデルで状態を確認してから送信します。
対象: 上記2方式が使えない場合のフォールバック
戦略1が成功すれば戦略2・3は試みません。戦略1が失敗したら戦略2を試みます。 こうしてアプリの種類に関係なく確実に制御できます。
LINE は IME キーを受け取ると独自の状態遷移を始めてしまいます。
LINE などの Qt フレームワーク製アプリは、 半角/全角キー(VK_KANJI)などの IME 関連キーを受け取ると アプリ自身が独自の IME 状態遷移を開始します。 awase が「IME ON にする」と送ったキーを LINE が別の意味で解釈し、 意図と逆の状態になってしまうのです。
この問題を解決するために、awase は LINE に対して 物理的な IME キーを一切通さないという方針を取っています。 LINE が IME キーを見る機会そのものをなくし、 IMM32 クロスプロセス制御(戦略1)だけで IME を操作します。
LINE は「IME キーを通さない」だけでは十分ではありませんでした。 ALT+TAB でウィンドウを切り替えるとき、 OS はタスクスイッチャーのウィンドウに一瞬フォーカスを移します。 この瞬間に IME 状態を読み取ると誤って「IME OFF」と判定され、 LINE に戻ったときにエンジンが停止してしまいます。
awase は「フォーカスが変わるたびにカウンタ(エポック)を増やし、 古いカウンタの観測は棄却する」という仕組みでこれを解決しています (セクション5の ObservationAdmission 参照)。 LINE での ALT+TAB 問題も、この epoch 照合によって構造的に解消されています。
08同じ「IME ON にする」でも、IME ソフトごとにコマンドが異なります。
Windows には「どの IME ソフトを使うか」を自由に選べる仕組みがあります。 代表的なのは Windows 標準の MS-IME と、 別途インストールする Google 日本語入力です。 同じ「ひらがなモードにしてください」という操作でも、 受け付けるコマンドが違います。
| 操作 | Google 日本語入力 | MS-IME |
|---|---|---|
| ひらがなモードへ | VK_IME_ON (0x16) |
VK_DBE_HIRAGANA (0xF2) |
| 直接入力(英数)へ | VK_IME_OFF (0x1A) |
VK_DBE_ALPHANUMERIC (0xF0) |
| どちらも冪等か | はい(何度送っても結果が変わらない) | はい(同様) |
awase はどちらの IME が使われているかを、
TSF(Text Services Framework)の API で自動判別します。
具体的には IME ごとに固有の識別子(CLSID)を動的に取得し、
起動後初めて識別した IME 種別を cache.toml に保存します。
次回起動時はこのキャッシュを使うため、判別コストがかかりません。
EnumProfiles で CLSID を取得する方式は
OS レベルの公式 API を使うため、確実で安定しています。
IME が切り替わったとき(たとえば Google 日本語入力をアンインストールして
MS-IME に戻したとき)は、WM_IME_KIND_CHANGED メッセージで
awase に通知が来ます。awase はこのメッセージを受け取ると
制御方式をリアルタイムに切り替えます。再起動は不要です。
同じキーでも「届け方」が違うと IME の動作が変わります。
awase がキーを送り返すとき(リレーモード)、 実は「どんな形式で送るか」にも選択肢があります。
| モード | 説明 | 向いているケース |
|---|---|---|
| Unicode モード | 文字コードとしてキーを送る。シンプルだが IME を経由しないことがある | IME 経由が不要なアプリ |
| TSF モード | 物理キーと同等の形式で送る。IME が確実に受け取れる | Google 日本語入力 / TSF アプリ |
Google 日本語入力 が動いている環境では TSF モードが必要です。 しかし awase は起動時に Google 日本語入力 が動いているかを毎回確認するのではなく、 自動昇格という仕組みを使います。
WriteTransferCount は Google 日本語入力 プロセスの
I/O 書き込みバイト数です。Google 日本語入力 がキーを受け取ると
辞書参照などのために書き込みが発生します。
この数値を監視することで「Google 日本語入力 が実際にキーを処理したか」を
外部から確認できます。
Chrome は複数のプロセスが協調するため、通常の方法が通じません。
Chrome や Edge は「マルチプロセス」アーキテクチャを採用しています。 表示を担当するプロセス、ネットワークを担当するプロセス、入力を担当するプロセスが 分離して動作します。
IMM32 の「アプリの IME ウィンドウに直接メッセージを送る」方法は、 送り先と受け取り先が同じプロセスにいることを前提としています。 Chrome ではこれが当てはまらず、方法1(IMM32 制御)が使えません。
そこで awase は Google 日本語入力へ VK_IME_ON(ひらがなモード)/ VK_IME_OFF(直接入力)を送信します。 Google 日本語入力の内部プロセスに直接届くため、Chrome のプロセス構成に関係なく動作します。
さらに Chrome には別の難問があります。 IME がキーを受け取れる状態(TSF composition context)になるまでに、 数百ミリ秒かかることがあるのです。この準備ができる前に文字キーが届くと、 文字化けが起きます。
awase は Google 日本語入力のディスクアクセス(I/O)をバックグラウンドで監視しています。 Google 日本語入力が「静止」したタイミングを「準備完了」のサインとみなし、 そこから文字を送信します。
TSF アプリでは「入力を受け付ける準備」に時間がかかります。
Windows Terminal などの TSF ネイティブアプリや Chrome で、 「こ」と打ったつもりが「ko」と出る現象が起きることがあります。 これは「TSF cold-start 問題」と呼ばれます。
TSF アプリは「composition context」という入力バッファを持っています。 アプリを起動した直後、または IME ON に切り替えた直後は、 このバッファの初期化に数百ミリ秒かかります。 初期化が完了する前にローマ字キー(「k」「o」)が届くと、 IME がそれを変換できず英字のまま出力してしまいます。
awase は「確定キー(Enter やスペース)が押された直後」や 「長時間入力がなかった後」に warmup 処理を行います。 composition context をあらかじめ活性化しておく「予熱」です。 次の文字が来るまでの空き時間を使って準備を済ませ、cold-start を未然に防ぎます。
それでも予測できないタイミングで cold-start が起きた場合も、 awase は Google 日本語入力 I/O 監視で「準備できた」タイミングを検知してから文字を送るため、 化けが起きにくくなっています。
Chrome や Windows Terminal などの TSF ネイティブアプリでは、上記の予熱だけでは composition context が確実に活性化されたか判断できないケースがあります。 awase はこれを解決するために捨て打ち機能を備えています。
捨て打ちとは、本来打ちたい文字の直前に「捨て駒」として A キーを バックスペースとセットで送り込む動作です。 A と BS を同一バッチで送るため、 Chrome は画面を描画する前に両方を処理し、「あ」が一瞬でも表示されることはありません。
捨て打ちには2つの役割があります。
第一に強制暖機:A キーで Google 日本語入力 が composition を開始するため、
context を確実に活性化できます。
第二に状態観測:Google 日本語入力 が A を処理すると
プロセス内部の I/O 書き込みバイト数が増加します(「あ」の変換候補データ送信のため)。
この増加量を WriteTransferCount で計測することで、
「composition が本当に確立されたか」を直接確認できます。
タイミングの競合に左右されない、コンテンツベースの確認方法です。
WezTerm は長時間使わないでいると TSF の文脈が「冷え」てしまいます。
WezTerm(Rust 製ターミナル)では、しばらく入力しないでいた後に 最初の1文字が消えてしまうことがありました。 たとえば10分放置してから「な」と打つと、「な」が出ずに何も起きない、という現象です。
WezTerm は TSF ネイティブアプリです。 TSF では IME と通信する「composition context」が 長時間使われないと内部的にリセットされます。 次にキーが来たときにはすでに context が失われており、 最初のキーは IME に届かずに消えてしまいます。
固定のタイムアウトで「何分おきに warmup する」という方法も試みましたが、 WezTerm が context をリセットするタイミングは環境によって異なり、 閾値を固定しても競合条件が別のタイミングに移るだけでした。
awase は「タイムアウトで予防する」のではなく、 「消えたことを検出して修復する」パターンを採用しています。 最初のキーが IME に届かなかったことを観測し、同じキーをもう一度送り直します。
LiteralDetect と呼んでいます。
IME がひらがな以外のモードのとき、親指シフトをどう扱うかは複雑です。
IME には「ひらがな」だけでなく「カタカナ」「JIS かな」「半角カタカナ」など 複数の入力モードがあります。 awase の親指シフトエンジンはひらがな前提で設計されているため、 これらのモードを適切に扱う追加の仕組みが必要でした。
カタカナモードでは、ローマ字入力が有効なまま IME が出力をカタカナに変換します。 awase はカタカナモードを「ローマ字入力中」と同等に扱います。 これにより親指シフトエンジンが誤って非活性化されません。
| 入力モード | awase の扱い | エンジン状態 |
|---|---|---|
| ひらがな(ローマ字) | 通常の親指シフト入力 | Active |
| カタカナ(全角) | ローマ字入力中と同等 | Active |
| 半角カタカナ | ローマ字入力中と同等 | Active |
| 英数(直接入力) | 親指シフト不要 | Inactive |
| JIS かな | ローマ字を使わない入力。エンジンは維持 | Active(旁受け) |
IME が ON のまま「英数入力モード」に切り替わることがあります。 この状態は「shadow = ON」「実態 = 英数直接入力」という 食い違いを引き起こします。
awase はこれを ObservedEisu(英数観測)として独立したモードで管理します。
ObservedEisu を検出すると、500ms 以内に自動的に IME OFF(直接入力)に切り替え、
エンジンも非活性化します。
ユーザーが気づいてから手動で対処しなくてよくなります。
ObservedEisu を明示的なモードとして扱うことで、
「この状態は想定済み」として安全に自動回復できます。
PC がスリープから復帰した直後は、IME の状態を正しく読み取れません。
PC をスリープさせて復帰させると、Windows の IME サブシステムも
一時的に「初期化中」の状態になります。
この瞬間に awase が IME 状態を読もうとすると、
is_japanese_ime = false(日本語 IME なし)という
誤った値が返ってくることがあります。
awase はスリープ復帰直後に「grace 期間」を設定します。
この期間中は is_japanese_ime = false への
ダウングレードを無視します。
一方で is_japanese_ime = true(正常)への更新は
常時受け付けます。
これは「悪いニュースは一定時間保留、良いニュースはすぐ受け付ける」という非対称な設計です。
スリープ復帰直後の誤った false を捨て、
初期化完了後の正しい true が届いた時点でエンジンが回復します。