A small Windows utility that shows your current keyboard language — and, for CJK layouts, the current input mode — right next to the text caret.
You always know what you are about to type before you type it, without looking away at the taskbar.
- Korean:
KO-가/KO-A - Japanese:
JA-あ/JA-ア/JA-ア/JA-A - Chinese:
ZH-中/ZH-A - Everything else: the language code —
DE,EN,FR, …
The tray icon shows the current language too, so the state is visible even when you are not in a text field.
English
I often work in both German and Korean on Windows, so I tried a number of existing tools for seeing which input language is currently active. Many of them are good, but some did not behave the way I wanted — so I had an AI build one instead.
The code is 100 % AI-written. The model used was primarily Opus, through Claude Code.
한국어
윈도우 환경에서 독일어와 한국어를 같이 사용하는 경우가 많았습니다. 그래서 현재 언어 상태가 무엇인지 확인하는 프로그램을 이것저것 사용해봤는데요, 좋은 프로그램들이 많았지만 원하는대로 동작하지 않는 경우도 있었습니다. 그래서 AI를 시켜서 만들어보았습니다.
코드는 100% AI로 작성하였습니다. 사용된 모델은 주로 Opus 이고, Claude Code를 사용했음을 알립니다.
- One small executable. No installer, no runtime, no DLLs beyond what ships with Windows. Copy it anywhere and run it.
- Nearly free at rest. 0.0–0.4 % of one core, and only while a caret is actually present.
- Never steals focus and never blocks clicks.
- Works across app frameworks — Win32, Chromium/Electron, UWP/WinUI — through a fallback chain of caret-location strategies.
- Per-monitor DPI aware, so it lands correctly on mixed-scaling multi-monitor setups.
- Live configuration. Save
config.iniand the change applies immediately.
Windows 10 1809 or later, x64. Developed and tested on Windows 11.
Download LanguageIndicator.exe and run it. There is no installer.
It runs in the background with a tray icon. Right-click that icon for:
| Menu item | What it does |
|---|---|
| Show indicator | Toggles the overlay (double-clicking the icon does the same) |
| Open config file | Opens config.ini in Notepad |
| Run at Windows startup | Registers under HKCU — no administrator rights needed |
| Exit |
Settings live in config.ini next to the executable. It is created on first run, and
saving it applies the changes immediately — no restart.
If the executable's own directory is not writable (when installed under Program Files, for
example), %APPDATA%\LanguageIndicator\ is used instead. LanguageIndicator.exe --config
prints the path actually in use on its first line.
[display]
mode = always ; always | onchange (show briefly after a change, then hide)
duration_ms = 1500
[appearance]
font_family = Segoe UI
font_size = 12.5
font_weight = 600 ; 100-900
text_color = FFFFFF ; RRGGBB
text_opacity = 95 ; 0-100
background_color = 0078D4
background_opacity = 95
corner_radius = 5
padding_x = 7
padding_y = 3
min_width = 22
[position]
anchor = below ; right | left | above | below
offset_x = 0
offset_y = 6
selection_anchor = end ; where to sit when a click selects all text: end | start
[animation]
smooth = true ; glide on short moves, jump on long ones
duration_ms = 90
snap_distance = 120 ; moves longer than this skip the animation
[behavior]
poll_interval_ms = 100
use_uia = true ; see "Known limitations"Sizes are given at 100 % scaling and are scaled to your monitor automatically, so there is nothing to adjust for a high-DPI display.
Options added by a newer version are appended to an existing config.ini automatically,
leaving your edits untouched.
A value outside its accepted range, or one that is not a number where a number is expected, makes the program report the problem and decline to start:
LanguageIndicator did not start, because config.ini contains values that cannot be used:
[appearance] padding_x = 99999
expected a number between 0 and 200
[position] anchor = sideways
expected one of: right, left, above, below
Silently substituting a default would hide the typo: the setting would simply appear not to work, with nothing to explain why.
Editing the file while the program is running is treated differently — you are probably mid-edit, so it keeps the settings it already has, shows a tray notification naming the offending key, and carries on. Fix the value and the next save applies normally.
To check a file without starting the program:
LanguageIndicator.exe --config| Setting | Accepted |
|---|---|
mode |
always, onchange |
duration_ms |
100 – 60000 |
font_size |
5 – 96 |
font_weight |
100 – 900 |
text_color, background_color |
six hex digits (FFFFFF or #FFFFFF) |
text_opacity, background_opacity |
0 – 100 |
corner_radius |
0 – 64 |
padding_x, padding_y |
0 – 200 |
min_width |
0 – 500 |
anchor |
right, left, above, below |
offset_x, offset_y |
−500 – 500 |
selection_anchor |
end, start |
smooth, use_uia |
true, false |
[animation] duration_ms |
10 – 2000 |
snap_distance |
0 – 10000 |
poll_interval_ms |
16 – 2000 |
| Command | Purpose |
|---|---|
--config |
Config file path and the values in effect |
--languages |
Installed keyboard layouts and the label each produces |
--probe |
Live view of language, IME state and caret coordinates, per application |
--selftest |
Verifies the build, environment and settings, then exits (exit code 0 on success) |
Exit codes: 0 success, 1 selftest failure or an instance already running, 2 the
settings file was rejected.
The binary is a GUI-subsystem application, so PowerShell does not wait for it: the prompt comes back before the diagnostic output is printed, which makes it look as though the program is still running. It is not — it has already exited. Pipe it if you want the shell to wait:
.\LanguageIndicator.exe --config | Out-Host--probe is the tool to reach for when the badge misbehaves in a particular application: it
shows which strategy located the caret, and — when coordinates were found but discarded —
why they were rejected.
Visual Studio 2022 with the Desktop development with C++ workload. There are no external dependencies.
The simplest route, from any terminal:
powershell -ExecutionPolicy Bypass -File tools\build.ps1That script locates Visual Studio, puts its bundled CMake on PATH for the current process,
and builds. -Configuration Debug and -Clean are available.
A standard Visual Studio installation does not put cmake on the system PATH — it
lives inside the VS directory — so a plain terminal will not find it. To run CMake yourself,
either use the script above or start from Developer PowerShell for VS 2022 in the Start
menu.
Two generators are configured. Both use the same MSVC toolchain with the same flags and produce equivalent binaries; they differ only in what drives the build.
| Preset | Generator | Output | |
|---|---|---|---|
vs2022 |
Visual Studio 17 2022 | build\vs2022\<config>\ |
Multi-config, and produces a .sln you can open and debug in the IDE. CMake finds Visual Studio on its own, so it works from any shell. |
ninja-release, ninja-debug |
Ninja | build\ninja-<config>\ |
Much faster, but single-config, no .sln, and requires a Developer PowerShell because Ninja invokes cl.exe directly and needs the compiler environment. |
# Visual Studio generator
cmake --preset vs2022
cmake --build --preset vs2022-release
# Ninja — from Developer PowerShell for VS 2022
cmake --preset ninja-release
cmake --build --preset ninja-releaseNinja builds noticeably faster, which is worth having while iterating.
The result is LanguageIndicator.exe, linked against the static CRT.
Three questions have to be answered continuously.
What is the input language? The keyboard layout of the foreground thread
(GetKeyboardLayout), mapped to an ISO 639 code. Layouts are per-thread, so the foreground
thread must be asked specifically.
What is the IME conversion mode? WM_IME_CONTROL / IMC_GETCONVERSIONMODE, sent with
SendMessageTimeout so an unresponsive application cannot hang the indicator. The message
goes to the focused control, not the top-level window — the conversion mode belongs to the
input context, and the input context belongs to whatever actually receives input. Readings
are validated against the language (Korean cannot have the katakana bit set) and transient
failures are held over, because applications do return garbage during transitions.
Where is the caret? A fallback chain, because no single API covers everything:
GetGUIThreadInfo— synchronous and cheap; classic Win32 controls.- MSAA
OBJID_CARET. - UI Automation
TextPattern.
Steps 2 and 3 are cross-process COM calls that block if the target application stops pumping messages, so they run on a dedicated STA worker thread; the main thread only ever reads finished results. Every candidate rectangle is validated — a caret is tall and narrow, and some providers return the geometry of an off-screen element instead.
Updates are driven by SetWinEventHook (foreground, focus, text-selection changes) rather
than by polling, with a low-frequency timer only for the IME conversion mode, which has no
event to subscribe to.
The overlay is a layered, click-through, never-activated window rendered with Direct2D and
DirectWrite, composited via UpdateLayeredWindow.
- Elevated windows. An unelevated process cannot read the caret or IME state of a window running as administrator (Windows UIPI). The last known state is kept instead.
- Console windows. Windows Terminal and friends sometimes return implausible conversion modes; the validator rejects them and only the language code is shown.
- Secure desktops. Nothing is displayed over UAC prompts or the lock screen — by design.
use_uia = falsedisables selection detection, so clicking a field that selects all its text places the badge wherever that application keeps its caret (Chrome keeps it at the start). Applications that MSAA cannot serve show nothing at all. Leave it on unless you are isolating a problem.- Unsigned binaries. The executable is not code-signed, so SmartScreen will warn on first run. Security software may also treat it as suspicious: reading other processes' input state through global hooks and the accessibility APIs is, by behaviour alone, hard to tell apart from software you would not want. Nothing here records or transmits keystrokes — the source is short enough to check.
docs/PLAN.md is the working record kept while building this: the technology choice and why, the risky parts, and a round-by-round log of what each application actually did when measured. Most of the non-obvious code here exists because of something in that file.
MIT — see LICENSE.
