A code-free, technical explanation of why awase works even in difficult applications.
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.
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.
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.
03The 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.
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."
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.
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.
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.
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.
05How 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.
Rather than a single IME state, awase manages three distinct "states."
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.
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.
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.
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.
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.
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.
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
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
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.
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.
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).
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.
08Even 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.
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.
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.
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."
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.
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.
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.
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."
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.
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.
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.
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.
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.
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.
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.
LiteralDetect.
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.
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) |
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.
ObservedEisu as an explicit mode, awase can safely auto-recover, treating
"this state as anticipated."
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).
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.