01

Intercepting keys — the keyboard hook

awase inserts itself "between" the keyboard driver and the application.

Windows provides a mechanism to receive keyboard input ahead of every application. It is called the low-level keyboard hook (WH_KEYBOARD_LL), and every time a physical key is pressed, awase is the first to be notified.

Physical keyboard │ key press ▼ 【 awase's hook 】 ← every key is received here first │ ├─ keys relevant to thumb shift → awase takes over processing │ └─ irrelevant keys → passed through unchanged │ ▼ Windows / Application

However, "interception" has a limitation: if processing takes too long, Windows forcibly unhooks the hook. awase copes with this limitation by finishing every decision as quickly as possible and handing heavy work off to a separate thread. If it is unhooked anyway, it re-registers itself automatically.

Difference from DLL injection: Some other thumb-shift tools use "DLL injection," embedding code directly into the application's process. This lets them reach deeper, but it is more likely to be flagged as a false positive by antivirus software. awase does not use this approach and operates using only standard Windows APIs.
02

Re-injecting every key — relay mode

awase swallows every key first, then re-sends it after processing.

A normal keyboard hook has two operating modes.

Filter mode Relay mode (awase's default)
Behavior Intercept only the relevant keys, pass the rest through Swallow every key first, then re-send it after processing
Coexistence with other hooks May conflict depending on ordering Because keys are re-injected, they reach other hooks too
Stability Prone to timing dependence Ordering is guaranteed by a FIFO queue

In relay mode, awase must always re-send any key it receives itself. To avoid an infinite loop in which "the key I sent is received by me again," re-injected keys are tagged with a marker so the hook passes them through.

03

How to detect a "simultaneous press" — state machines and timers

The heart of thumb shift is the part that decides "two keys were pressed simultaneously."

In NICOLA thumb shift, a character key and a thumb key that overlap within 100ms are judged a "chord." But a computer has no concept of "simultaneous"—one of them always arrives first. Furthermore, in fast typing the next character key can arrive before the previous thumb key is released.

Example: entering "が" (character key: A + right thumb key) Case 1 (character first): A↓ ─── A↑ right thumb↓ ─ right thumb↑ ↑ within 100ms here → "が" Case 2 (thumb first): right thumb↓ ─── right thumb↑ A↓ ─── A↑ ↑ within 100ms here → "が" Case 3 (three-key problem): か↓ ─────────── か↑ right thumb↓ ──────────── right thumb↑ ら↓ ─ ら↑ ↑ which one pairs up? → "が+ら" or "か+ら"?

This is solved by combining a state machine (FSM: Finite State Machine) with timers. Every time a key arrives, awase transitions to the next state based on "the current state" and "the kind of key," and at some point commits to "this is a chord" or "this is a single keystroke."

State machine sketch (simplified): Idle │ character key↓ ▼ Character key pending ─── timeout (100ms) → emit as a standalone character │ thumb key↓ (within 100ms) ▼ Both pressed ─── thumb key↑ → emit as a shifted character │ character key↑ ▼ Thumb key only pressed → wait for the next character key

In the "three-key problem" of Case 3, the state machine compares the time gaps d1 (character 1 → thumb) and d2 (thumb → character 2). If d1 < d2, character 1 is closer to the thumb, so it judges "character 1 + thumb is a chord"; if d1 ≥ d2, character 2 is closer to the thumb, so it judges "character 2 + thumb is a chord." But when d1 and d2 are close, timing alone cannot decide.

What confirm mode is: It is the strategy for "when to commit output." There are five kinds, such as waiting until the timeout (wait) or emitting right away and swapping the output if a shift key follows (speculative), so you can choose one to match your typing style.
04

Using Japanese statistics to prevent misjudgments — the n-gram model

When timing is close in the three-key problem, "is it natural as Japanese?" breaks the tie.

In fast typing, the "three-key problem" arises when a character key, a thumb key, and the next character key all overlap. d1 and d2 are nearly identical, and timing alone cannot decide whether "the earlier character pairs with the thumb (a shifted character)" or "the later character pairs with the thumb (a shifted character)."

awase's ngram_predictive mode resolves this ambiguity with Japanese statistics. When the timing is close, it checks the "sequence of hiragana" for each of the two interpretations against an n-gram corpus and chooses whichever makes more natural Japanese.

Example: when three keys overlap, the n-gram compares the occurrence probability of the bigram "preceding kana → candidate kana" for the two interpretations.

Interpretation A (か + right thumb = が) score: probability of the preceding character "ら" → "が" = "らが"
Interpretation B (emit か standalone) score: probability of the preceding character "よ" → "か" = "よか"

In a Japanese corpus, words containing "よか" (like "よかった" or "よかれ") are abundant, whereas a sequence of "らが" as in "らがった" is rare. So "よか" wins the score, and "よかった" is chosen.

Conversely, if the context changes so that "らが" scores higher, "らがった" is chosen. Rather than judging by timing figures alone, using the preceding character as context improves accuracy.

This corpus is statistical data learned from a large amount of Japanese text. It quantifies "how frequently a given two-hiragana combination appears" and is bundled with awase.

05

Tracking the invisible IME state — the shadow model

How does awase know "is the IME ON or OFF right now"?

To enable thumb-shift input, the IME must be in the "ON" state. But the Windows IME state cannot always be reliably obtained from an external program. With Google Japanese Input in particular, there is no public API to read the internal state accurately.

Managing three distinct "states" separately

Rather than a single IME state, awase manages three distinct "states."

┌─────────────────────────────────────────────────┐ │ desired (the user's intent) │ │ Ctrl+Henkan → want it ON │ │ Ctrl+Muhenkan → want it OFF │ └─────────────────────────┬───────────────────────┘ │ instruct ▼ ┌─────────────────────────────────────────────────┐ │ applied (the last result we applied) │ │ "sent the ON command, and got confirmation" │ └─────────────────────────┬───────────────────────┘ │ reconcile via observation ▼ ┌─────────────────────────────────────────────────┐ │ observed (the state actually observed from OS/IME)│ │ the reality obtained from polling/observation │ └─────────────────────────────────────────────────┘

The "intent" and the "reality" can diverge. For example, when switching applications the application itself may turn the IME OFF on its own. awase detects this divergence through periodic observation (polling), and if there is a divergence it corrects it automatically.

What idempotent control means

Many traditional thumb-shift tools used a "toggle the IME" approach: "if it's ON now, turn it OFF; if it's OFF, turn it ON." The weakness of this approach is that unless you know the current state accurately, you end up with the opposite of what you intended.

awase uses idempotent operations—"turn the IME ON" and "turn the IME OFF." These are operations whose result does not change no matter how many times you run them. Even if the state has diverged, running "turn it ON" always results in ON.

What idempotent means: using a light switch as an analogy, "each press flips between ON and OFF" is a toggle (the traditional approach). "Pressing this button always turns it ON" is idempotent (awase's approach). Because you do not need to know the state, even if a divergence occurs the result always matches your intent.

Centrally managing whether an observation "can be trusted" — ObservationAdmission

awase runs multiple "probes" asynchronously in the background. When a probe returns a result, is that observation really about "the window that is focused right now"?

For example, when switching apps with ALT+TAB, the OS briefly moves focus to the task switcher's window. If a probe launched during this tiny interval returns "IME OFF," the engine ends up stopped even after returning to the original app.

ALT+TAB timeline: LINE is focused │ press ALT+TAB → task switcher appears │ OS: focus = switcher window │ └─ ImmCrossProbe launches → returns "IME OFF" ← stale observation │ release ALT+TAB → back to LINE │ └─ probe result arrives → engine stops! (bug) ▼ awase's countermeasure: focus had changed → reject this observation

awase solves this with an "epoch." Every time focus changes, it increments an internal counter (FocusEpoch) by 1. A probe records the epoch number at start time, and when its result arrives it reconciles that against the current epoch number. If the numbers differ, it judges "focus changed in the meantime = stale" and discards the result.

Guaranteed by the type system: "Only an observation that passed epoch reconciliation can be written into the shadow model"—this is enforced by the compiler. When an observation passes epoch reconciliation, a token called AcceptedObservation is issued, and without this token the write_* functions cannot be called. When a probe is added in the future, forgetting the reconciliation results in a compile error.
06

Why the control method differs per application

The way the instruction "please turn the IME ON" is delivered differs by application.

There are broadly two ways Windows applications communicate with the IME: the old IMM32 mechanism and the newer TSF (Text Services Framework). On top of that, Google Japanese Input has its own internal structure. awase automatically determines the kind of application and tries a three-tier control method.

1

IMM32 cross-process control

Send a "please turn it ON" message directly to the application's IME window. This is the most reliable method, and on success you can immediately confirm that the state changed.

Target: common applications such as Notepad and Word

2

Google Japanese Input direct control (VK_IME_ON / VK_IME_OFF)

Send the Windows virtual keys VK_IME_ON (to hiragana mode) and VK_IME_OFF (to direct input). Because they are idempotent operations, they match your intent no matter how many times they are sent.

Target: every application where Google Japanese Input is running

3

KANJI key toggle (last resort)

Send the hankaku/zenkaku key (VK_KANJI) to switch the IME. Because it is a toggle operation, there is a risk of ending up with the opposite of what you intended unless the current state is known accurately. awase confirms the state via the shadow model before sending it.

Target: a fallback for when the above two methods cannot be used

If strategy 1 succeeds, strategies 2 and 3 are not attempted. If strategy 1 fails, strategy 2 is attempted. In this way, control works reliably regardless of the kind of application.

The special case of LINE / Qt apps: Qt-based apps such as LINE start their own IME state transition when they receive IME-related keys (such as hankaku/zenkaku). For LINE, awase adopts the policy of "never letting any physical IME key through" and operates the IME using only IMM32 cross-process control.
07

Why LINE / Qt apps are especially tricky

When LINE receives an IME key, it starts its own state transition.

Apps built with the Qt framework, such as LINE, start their own IME state transition when they receive IME-related keys like the hankaku/zenkaku key (VK_KANJI). LINE interprets the key awase sends to "turn the IME ON" with a different meaning, ending up in the opposite state from what was intended.

A normal app (such as Notepad): awase → sends VK_KANJI → the OS's IME receives it → IME ON LINE (a Qt app): awase → sends VK_KANJI → LINE intercepts it → LINE's own IME state transition begins → can end up the opposite of what was intended

To solve this problem, awase adopts the policy of never letting any physical IME key through for LINE. It removes the very opportunity for LINE to see an IME key and operates the IME using only IMM32 cross-process control (strategy 1).

Combined with ALT+TAB

For LINE, "not letting IME keys through" alone was not enough. When switching windows with ALT+TAB, the OS briefly moves focus to the task switcher's window. If the IME state is read at this instant, it is mistakenly judged "IME OFF," and the engine stops when returning to LINE.

awase solves this with the mechanism "increment a counter (epoch) every time focus changes, and reject observations with an old counter" (see ObservationAdmission in Section 5). The ALT+TAB problem in LINE is also resolved structurally by this epoch reconciliation.

08

Why the control method changes between Google Japanese Input and MS-IME

Even for the same "turn the IME ON," the command differs by IME software.

Windows lets you freely choose "which IME software to use." The representative ones are the Windows-standard MS-IME and the separately installed Google Japanese Input. Even for the same operation "please switch to hiragana mode," the command each one accepts differs.

Operation Google Japanese Input MS-IME
To hiragana mode VK_IME_ON (0x16) VK_DBE_HIRAGANA (0xF2)
To direct input (alphanumeric) VK_IME_OFF (0x1A) VK_DBE_ALPHANUMERIC (0xF0)
Are both idempotent? Yes (the result does not change no matter how many times it is sent) Yes (likewise)

awase automatically determines which IME is in use via the TSF (Text Services Framework) API. Specifically, it dynamically obtains the unique identifier (CLSID) for each IME and saves the IME kind it identified for the first time after startup to cache.toml. On the next startup it uses this cache, so there is no identification cost.

Why identify by CLSID: It used to determine the IME by process name—"is the Google Japanese Input process (GoogleIMEJaConverter.exe) running?" But process names can vary by environment. Obtaining the CLSID via TSF's EnumProfiles uses an official OS-level API, so it is reliable and stable.

When the IME is switched (for example, when you uninstall Google Japanese Input and revert to MS-IME), awase is notified via the WM_IME_KIND_CHANGED message. Upon receiving this message, awase switches its control method in real time. No restart is required.

09

Choosing how to deliver keys automatically — InjectionMode

Even for the same key, a different "delivery method" changes how the IME behaves.

When awase re-sends a key (relay mode), there are actually choices for "in what form to send it."

Mode Description Best-suited case
Unicode mode Send the key as a character code. Simple, but may bypass the IME Apps that do not need to go through the IME
TSF mode Send in a form equivalent to a physical key. The IME reliably receives it Google Japanese Input / TSF apps

In environments where Google Japanese Input is running, TSF mode is required. But rather than checking on every startup whether Google Japanese Input is running, awase uses a mechanism called automatic promotion.

Flow of InjectionMode automatic promotion: Right after startup: Unicode mode │ │ focus change → send keys in Unicode mode │ ├─ observe Google Japanese Input's WriteTransferCount │ ├─ increased (+350B or more) │ │ → judge "Google Japanese Input processed the key" │ │ → automatically promote to TSF mode │ │ → save to cache.toml (persists across restarts) │ │ │ └─ no increase → continue in Unicode mode

WriteTransferCount is the number of I/O write bytes of the Google Japanese Input process. When Google Japanese Input receives a key, writes occur for things like dictionary lookups. By monitoring this figure, you can confirm from the outside "whether Google Japanese Input actually processed the key."

Why the user needs no configuration: Thanks to automatic promotion, in environments where Google Japanese Input is installed, it automatically switches to TSF mode at the timing of the first focus change. In MS-IME environments or environments without Google Japanese Input, it keeps operating in Unicode mode. There is no need to choose the IME kind in the settings screen.
10

Why Chrome / Edge are especially difficult

Chrome uses multiple cooperating processes, so the usual method does not work.

Chrome and Edge adopt a "multi-process" architecture. The process responsible for display, the process responsible for networking, and the process responsible for input run separately.

The IMM32 method of "sending a message directly to the application's IME window" assumes that the sender and the receiver are in the same process. In Chrome this does not hold, and method 1 (IMM32 control) cannot be used.

Chrome's process layout (simplified): ┌──────────────────────────────────────┐ │ Browser process (frame / UI) │ │ ↕ cannot communicate via IMM32 │ │ Renderer process (page display) │← the IME communicates here │ ↕ IPC │ │ Network process ... │ └──────────────────────────────────────┘ awase cannot deliver directly to the IME window

So awase sends VK_IME_ON (hiragana mode) / VK_IME_OFF (direct input) to Google Japanese Input. Because they reach Google Japanese Input's internal process directly, they work regardless of Chrome's process layout.

Aligning timing with Google Japanese Input I/O monitoring

Chrome has yet another hard problem. It can take several hundred milliseconds until the IME is in a state where it can receive keys (the TSF composition context). If a character key arrives before this preparation is done, garbling occurs.

awase monitors Google Japanese Input's disk access (I/O) in the background. It treats the moment Google Japanese Input "goes quiet" as a "ready" sign, and sends characters from there.

Why I/O monitoring is effective: Google Japanese Input performs file reads and writes for things like dictionary lookups. Once the IME's composition context is established, internal processing settles down and the I/O goes quiet. This is a way to infer the "internal readiness state" from the outside without using invasive techniques like DLL injection.
11

The first character comes out garbled — the TSF cold-start problem

In TSF apps, "getting ready to accept input" takes time.

In TSF-native apps such as Windows Terminal, or in Chrome, a phenomenon can occur where you meant to type "こ" but "ko" comes out. This is called the "TSF cold-start problem."

Why it happens

A TSF app has an input buffer called the "composition context." Right after launching the app, or right after switching the IME ON, initializing this buffer takes several hundred milliseconds. If romaji keys ("k", "o") arrive before initialization is complete, the IME cannot convert them and outputs them as raw letters.

Timeline of a cold-start occurrence: 0ms switch the IME ON start typing "こ": K↓ K↑ 50ms O↓ O↑ 100ms (composition context initialization complete) ← keys that arrived before this are not converted Result: "ko" is output as raw letters

awase's solution

awase performs warmup processing "right after a confirm key (Enter or Space) is pressed" or "after a long period with no input." It is a "pre-warming" that activates the composition context ahead of time. Using the idle time before the next character arrives, it finishes preparation and prevents the cold-start before it happens.

Even if a cold-start occurs at an unpredictable time, awase detects the "ready" moment via Google Japanese Input I/O monitoring before sending characters, so garbling is unlikely to occur.

The sacrificial keystroke feature (reliable warmup for Chrome / Windows Terminal)

In TSF-native apps such as Chrome and Windows Terminal, there are cases where the pre-warming above cannot determine whether the composition context was reliably activated. To solve this, awase provides a sacrificial keystroke feature.

A sacrificial keystroke is the action of sending the A key as a "sacrificial pawn" together with a backspace, right before the character you actually want to type. Because A and BS are sent in the same batch, Chrome processes both before it draws the screen, so "あ" is never displayed even for an instant.

Flow of the sacrificial keystroke feature: [cold-start detected] │ ├─ send VK_A + BS in the same batch (sacrificial keystroke) │ ← nothing is displayed on screen │ ├─ observe Google Japanese Input's I/O write bytes (WriteTransferCount) │ ├─ +350B or more → confirm composition established → send the real romaji │ └─ timeout → force-reset Google Japanese Input with VK_IME_OFF→VK_IME_ON → retry │ └─ re-send the real romaji (no extra BS needed: the sacrificial keystroke already erased one character)

The sacrificial keystroke has two roles. First, forced warmup: the A key makes Google Japanese Input start a composition, so the context can be reliably activated. Second, state observation: when Google Japanese Input processes A, the I/O write bytes inside the process increase (to send the conversion-candidate data for "あ"). By measuring this increase with WriteTransferCount, you can directly confirm "whether the composition was really established." It is a content-based confirmation method that is not swayed by timing races.

A note about Windows Terminal: The VK_IME_ON (0x16) / VK_IME_OFF (0x1A) that awase sends to Google Japanese Input are standard Windows virtual key codes, but they do not exist on a physical keyboard. They also do not generate VT escape sequences, so no additional setup is needed in Windows Terminal.
12

Characters vanish after a long idle — the WezTerm long-idle problem

If WezTerm is left unused for a long time, the TSF context "cools down."

In WezTerm (a terminal written in Rust), the first character could vanish after a long period without input. For example, if you leave it for 10 minutes and then type "な," the phenomenon is that "な" does not appear and nothing happens.

Why it happens

WezTerm is a TSF-native app. In TSF, the "composition context" that communicates with the IME is internally reset if it is not used for a long time. By the time the next key arrives, the context has already been lost, and the first key is not delivered to the IME and vanishes.

Timeline of a WezTerm long-idle occurrence: 0 min typing with the IME ON ↓ left for 10–15 min 15 min TSF composition context is reset (internally) ↓ the user presses a key 15 min first "な" → not delivered to the IME, vanishes next "に" → the IME initializes the context → delivered normally Result: you meant to type "なにほ" but it becomes "にほ"

awase's solution — detect and re-send

A fixed-timeout approach of "warm up every N minutes" was also tried, but the timing at which WezTerm resets the context varies by environment, and fixing a threshold only moved the race condition to a different timing.

Rather than "preventing via a timeout," awase adopts the pattern of "detecting that it vanished and repairing it." It observes that the first key was not delivered to the IME and re-sends the same key once more.

Flow of LiteralDetect (detect and repair): a key arrives │ ├─ confirm whether the IME received it (observe TSF-mode write bytes) │ │ │ ├─ received → process as normal input │ │ │ └─ not received (literal-ization detected) │ │ │ ├─ erase the "raw character (e.g. k)" that WezTerm displayed, with BS │ └─ re-send the same key → this time the context is initialized, so it is delivered
"Detect and repair" over "prevent": If you try to prevent by extending a timeout, a race condition of "OK at this timing but fails at that timing" always remains. In WezTerm's case, because "you can tell that it failed," detecting and repairing eliminates the race condition fundamentally. awase calls this pattern LiteralDetect.
13

Working correctly beyond hiragana — katakana and JIS kana support

How to handle thumb shift when the IME is in a mode other than hiragana is complex.

The IME has multiple input modes beyond "hiragana," such as "katakana," "JIS kana," and "half-width katakana." Because awase's thumb-shift engine is designed on the premise of hiragana, an additional mechanism was needed to handle these modes appropriately.

The central principle "katakana = ObservedRomaji"

In katakana mode, romaji input remains active while the IME converts the output to katakana. awase treats katakana mode as equivalent to "romaji input in progress." This keeps the thumb-shift engine from being mistakenly deactivated.

Input mode awase's handling Engine state
Hiragana (romaji) Normal thumb-shift input Active
Katakana (full-width) Equivalent to romaji input in progress Active
Half-width katakana Equivalent to romaji input in progress Active
Alphanumeric (direct input) Thumb shift not needed Inactive
JIS kana Input that does not use romaji. The engine is kept on Active (passive standby)

ObservedEisu — automatic detection of alphanumeric direct input

The IME can switch to "alphanumeric input mode" while remaining ON. This state causes a mismatch of "shadow = ON" but "reality = alphanumeric direct input."

awase manages this as an independent mode called ObservedEisu (observed alphanumeric). When ObservedEisu is detected, it automatically switches to IME OFF (direct input) within 500ms and deactivates the engine as well. The user no longer has to notice and deal with it manually.

Why alphanumeric mode must not be left as is: If alphanumeric mode continues while the shadow model thinks "IME ON," an inconsistency persists in which awase tries to convert keys expecting hiragana input while letters are actually output. By treating ObservedEisu as an explicit mode, awase can safely auto-recover, treating "this state as anticipated."
14

Automatic recovery after sleep or unlock

Right after a PC resumes from sleep, the IME state cannot be read correctly.

When you put a PC to sleep and resume it, the Windows IME subsystem also temporarily enters an "initializing" state. If awase tries to read the IME state at this instant, it can get back the mistaken value is_japanese_ime = false (no Japanese IME).

Timeline after resuming from sleep: Before sleep: Google Japanese Input running, IME ON │ └─ sleep │ resume ├─ OS: re-initializing the IME subsystem │ ↓ if awase reads during this │ is_japanese_ime = false (wrong) │ → engine stops (judged "no Japanese IME") │ └─ initialization complete (several hundred ms later) → the normal value returns, but the engine has already stopped

Solution via grace protection

awase sets a "grace period" right after resuming from sleep. During this period, it ignores downgrades to is_japanese_ime = false. On the other hand, it always accepts updates to is_japanese_ime = true (normal).

This is an asymmetric design of "hold bad news for a fixed period, accept good news immediately." It discards the mistaken false right after resuming from sleep, and the engine recovers the moment the correct true arrives after initialization completes.

Using shadow grace and epoch for different purposes: For "stale observations after focus changed," FocusEpoch is used (Section 5). For "temporary wrong values after resuming from sleep," a grace period is used. The former is a problem of "the observation is old"; the latter is a problem of "the observation itself is temporarily untrustworthy." Each is handled with a different approach.