DEV Community

Cover image for Auto-correcting wrong-layout typing on Wayland is nearly impossible. We did it anyway
Vyacheslav Strelnikov
Vyacheslav Strelnikov

Posted on • Originally published at poltertype.com

Auto-correcting wrong-layout typing on Wayland is nearly impossible. We did it anyway

Originally published on the PolterType blog.

If you type in more than one keyboard layout, you know the moment: you
look up at the screen and the last word reads Espa;a. The keyboard
was still in US English, and Spanish ñ lives on that key. So you
delete the word, switch the layout, and type it again — for the
thirtieth time today. The further apart the alphabets — Greek, Hebrew,
Cyrillic — the worse the mess.

Utilities that fix this automatically are an old category. Windows has
had them since the 2000s (Punto Switcher and its descendants), X11 had
xneur: watch what is being typed, notice that it is gibberish in the
current layout but a real word in the other one, switch the layout and
retype the word. Then Linux desktops moved to Wayland, and the category
quietly died there — not because nobody cared, but because Wayland is
deliberately designed to make this class of application impossible.

PolterType is an
open-source (MIT) tray app, pure Rust, that does it anyway — on
Windows, macOS and Linux. On Linux that includes Wayland, which was
the hard part, and which is what this post is about.

Three doors, all locked

To fix a word typed in the wrong layout you need three capabilities:

  1. hear keystrokes globally, in every application;
  2. inject keystrokes, to erase the wrong word and retype it;
  3. draw a small suggestion next to the text being edited.

X11 hands all three to any client that can open the display: raw
XInput2 events on the root window, XTest for injection,
override-redirect windows for the popup. Which is exactly why X11 is
also a keylogger's paradise — and exactly why Wayland's designers
refused to carry those capabilities over. There is no protocol for
global key events. There is no protocol for placing a window at screen
coordinates. Synthetic input exists only behind portals that most
compositors didn't implement for years.

From a security standpoint, all of this is correct. From the
standpoint of a layout corrector, all three doors are locked. Here is
the key we found for each one.

Door 1: hearing keystrokes — go under the display server, not over it

If the compositor won't relay input, read it where the compositor
itself does: evdev. PolterType's listener reads
/dev/input/event* directly, below the display server, so it works
identically on every compositor — Hyprland, Sway, KDE, GNOME, whatever
comes next.

The honest cost: that needs membership in the input group plus a
udev rule, which is one sudo between installing the app and it
working (scripts/setup-linux.sh does both; the Setup pane inside the
app probes your machine and tells you which half is missing, including
the classic trap where usermod -aG input can't touch a login session
that already exists). On X11 the same app needs no permission at all —
that contrast is Wayland's security model working as intended, and we
state it rather than hide it.

Reading evdev means you get keycodes, not characters. PolterType
carries its own layout mappings — TOML files generated from
xkeyboard-config rather than transcribed from keyboard pictures — for
the fifteen layouts it currently bundles, and only the layouts your OS
actually has enabled are loaded.

There was one alternative we refused only after measuring it: AT-SPI,
the accessibility bus, has a keystroke-listener API on paper.
Registering it returns false on wlroots compositors and delivers
nothing even with keys injected at the kernel level, because
at-spi2-registryd has no keyboard of its own — on Wayland it relays
what the compositor hands it, and only Mutter does. Where it would
work (X11), the existing listener already needs no permissions. So:
no AT-SPI listener, and that is now a decision with measurements
behind it, not an open plan item.

Door 2: typing it back

Corrections go out through a uinput virtual keyboard: erase the
word, switch the layout, retype it. The same setup script covers
/dev/uinput, so it is one permission story, not two.

Since 0.10.0 there is also a fallback for sessions where uinput
cannot be opened: the RemoteDesktop portal, the standard,
permissioned way to ask a compositor to synthesise input. It is tried
only when uinput is closed, so nobody who ran the setup script ever
sees a consent dialog. Full disclosure: that path is written from the
specification and has not yet run on a real GNOME or KDE session —
it is labelled that way in the code, and if it misbehaves we will
assume PolterType is wrong before the compositor.

And here is the part nobody warns you about: switching the layout
has no universal Wayland API either. Every desktop owns layouts its
own way, so PolterType probes a chain of backends in priority order —
hyprctl switchxkblayout, KDE's org.kde.keyboard D-Bus interface,
GNOME-family gsettings input sources, IBus, Fcitx5, and X11 XKB
group locking as the bare-window-manager fallback. If none of them
answers, the app refuses to start rather than sit in the tray
detecting mistakes it cannot fix.

Door 3: a tooltip next to your caret

When PolterType is not sure enough to auto-correct, it shows a small
suggestion you can accept with a chord. On Wayland that innocent
feature is two locked doors disguised as one.

The surface. Wayland clients cannot position their own windows.
The escape hatch is zwlr_layer_shell_v1 — the protocol panels and
notification daemons use — which gives an overlay surface with its own
coordinate space. wlroots compositors (Hyprland, Sway) have it, and
KWin has implemented it for years, something we ourselves documented
wrong until we tested KWin 6.7.3 and watched the surface map exactly
as on Hyprland. Mutter has no layer-shell — there the tooltip maps as
an X11 override-redirect window through XWayland. The genuinely
uncovered case is a GNOME-like session with XWayland disabled.

The position. No protocol tells you where the text caret is. But
the accessibility bus does — the readable half of AT-SPI that
actually works: subscribe to object:text-caret-moved, ask the
focused widget for GetCharacterExtents, and you get the caret's
rectangle. Coordinates only; the tooltip code never requests text
content. One subtlety worth knowing: toolkits keep their accessibility
bridges dormant until something raises the session's
org.a11y.Status.IsEnabled flag — the same flag screen readers raise
— so PolterType raises it at startup and deliberately never unsets it,
because a real screen reader might start later. Applications launched
before the flag went up stay silent until restarted; then the tooltip
falls back to anchoring on the focused window or the pointer.
Terminals mostly ship no accessibility bridge at all — an honest gap,
and developers live in terminals.

The door that bit back: corrections versus your own typing

A correction is a burst of injected keystrokes. If you keep typing
while it is on the wire — and fast typists do — your keys land in the
middle of the burst and the word comes out interleaved. On Linux,
PolterType closes that race by holding the keyboard for the length of
the burst (EVIOCGRAB), then typing the held keystrokes out itself,
in order, once the correction is down.

Then it met keyd. Input remappers hold every keyboard exclusively —
including PolterType's own virtual one — and re-emit everything
through a single virtual device. Grab that proxy and you have blocked
your own corrections along with the user's typing. So the gate checks
whether it can grab its own emitter, and if the emitter turns out to
be proxied, stands down for the rest of the run; corrections still
work, they just repair the raced keystroke instead of preventing it.

That check used to run once, at startup. That was a bug, and it cost
us a whole session once: a remapper grabs a freshly created device
asynchronously, so the startup probe can win the race, arm the gate
— and the first correction funnels the entire session's input into a
queue nobody drains. Since 0.6.3 the gate re-verifies before every
single hold. The lesson generalises to the whole stack this app sits
on (kernel → remapper → compositor → XWayland → toolkit): probed at
startup
proves nothing at the moment of use.

What it costs, honestly

  • One sudo on Wayland for evdev/uinput access; zero permissions on X11. The portal path may eventually remove it on GNOME/KDE — once it has actually run there.
  • A GNOME session without XWayland gets no suggestion tooltip (auto-corrections still work).
  • Terminals are largely invisible to caret anchoring and per-app detection — accessibility bridges end where TUIs begin.
  • Installers are currently unsigned, so SmartScreen and Gatekeeper warn on first launch. We say so on the download page rather than let you find out.

Yes, it sees your keystrokes. Here is the entire surface

There is no way around it: anything in this category must see keys —
that is the category. What an open-source tool can offer is a surface
small enough to audit:

  • The word buffer is RAM-only and abandoned the moment it is judged or you go idle. Release builds never log typed text.
  • There is exactly one network call in the whole app: a daily update check. It sends no identifiers, it is one checkbox to turn off, and the manifest URL is printed in Settings so you can verify the destination yourself.
  • Zero telemetry. Two places in the binary can speak TLS — the updater and the optional AI client — and each talks only to what you can verify yourself: the manifest URL printed in Settings, and the endpoint you configured (or nothing).
  • The AI hook ships no model, no vendor SDK and no default endpoint — it can only talk to an endpoint you name in your own config, a non-loopback endpoint needs a second explicit opt-in, and with nothing configured (the default) there is no AI in PolterType at all: no detectors are built and no socket is opened.

The long version of all of this is the
privacy policy — and the code,
which is the version that counts.

Coda

PolterType bundles fifteen layouts today; adding a language is a TOML
file plus a wordlist, documented in
docs/ADDING_A_LANGUAGE.md
— it is data, not code, and it is the friendliest first contribution
the project has. Things we would genuinely love help with: validating
macOS on Apple Silicon
(#3) and the
macOS keystroke hold-back
(#8).

Code: github.com/Just-Code-NET/PolterType
· Download: poltertype.com

If you type in two layouts, the ghost is friendly.

Top comments (0)