<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vyacheslav Strelnikov</title>
    <description>The latest articles on DEV Community by Vyacheslav Strelnikov (@vl360).</description>
    <link>https://dev.to/vl360</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064815%2Fe0cb57ac-337a-48bb-8782-f754fd1ef4aa.png</url>
      <title>DEV Community: Vyacheslav Strelnikov</title>
      <link>https://dev.to/vl360</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vl360"/>
    <language>en</language>
    <item>
      <title>Auto-correcting wrong-layout typing on Wayland is nearly impossible. We did it anyway</title>
      <dc:creator>Vyacheslav Strelnikov</dc:creator>
      <pubDate>Wed, 05 Aug 2026 21:28:52 +0000</pubDate>
      <link>https://dev.to/vl360/auto-correcting-wrong-layout-typing-on-wayland-is-nearly-impossible-we-did-it-anyway-j19</link>
      <guid>https://dev.to/vl360/auto-correcting-wrong-layout-typing-on-wayland-is-nearly-impossible-we-did-it-anyway-j19</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://poltertype.com/blog/wrong-layout-typing-on-wayland/" rel="noopener noreferrer"&gt;PolterType blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;a href="https://github.com/Just-Code-NET/PolterType" rel="noopener noreferrer"&gt;PolterType&lt;/a&gt; is an&lt;br&gt;
open-source (MIT) tray app, pure Rust, that does it anyway — on&lt;br&gt;
Windows, macOS and Linux. On Linux that includes Wayland, which was&lt;br&gt;
the hard part, and which is what this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three doors, all locked
&lt;/h2&gt;

&lt;p&gt;To fix a word typed in the wrong layout you need three capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;hear keystrokes&lt;/strong&gt; globally, in every application;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;inject keystrokes&lt;/strong&gt;, to erase the wrong word and retype it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;draw a small suggestion&lt;/strong&gt; next to the text being edited.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;From a security standpoint, all of this is correct. From the&lt;br&gt;
standpoint of a layout corrector, all three doors are locked. Here is&lt;br&gt;
the key we found for each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 1: hearing keystrokes — go under the display server, not over it
&lt;/h2&gt;

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

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

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

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

&lt;h2&gt;
  
  
  Door 2: typing it back
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  Door 3: a tooltip next to your caret
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  The door that bit back: corrections versus your own typing
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  What it costs, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;code&gt;sudo&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;A GNOME session without XWayland gets no suggestion tooltip
(auto-corrections still work).&lt;/li&gt;
&lt;li&gt;Terminals are largely invisible to caret anchoring and per-app
detection — accessibility bridges end where TUIs begin.&lt;/li&gt;
&lt;li&gt;Installers are currently &lt;strong&gt;unsigned&lt;/strong&gt;, so SmartScreen and Gatekeeper
warn on first launch. We say so on the download page rather than let
you find out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Yes, it sees your keystrokes. Here is the entire surface
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;The word buffer is RAM-only and abandoned the moment it is judged
or you go idle. Release builds never log typed text.&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;exactly one network call&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;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).&lt;/li&gt;
&lt;li&gt;The AI hook ships &lt;strong&gt;no model, no vendor SDK and no default
endpoint&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The long version of all of this is the&lt;br&gt;
&lt;a href="https://poltertype.com/privacy/" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt; — and the code,&lt;br&gt;
which is the version that counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coda
&lt;/h2&gt;

&lt;p&gt;PolterType bundles fifteen layouts today; adding a language is a TOML&lt;br&gt;
file plus a wordlist, documented in&lt;br&gt;
&lt;a href="https://github.com/Just-Code-NET/PolterType/blob/main/docs/ADDING_A_LANGUAGE.md" rel="noopener noreferrer"&gt;docs/ADDING_A_LANGUAGE.md&lt;/a&gt;&lt;br&gt;
— it is data, not code, and it is the friendliest first contribution&lt;br&gt;
the project has. Things we would genuinely love help with: validating&lt;br&gt;
macOS on Apple Silicon&lt;br&gt;
(&lt;a href="https://github.com/Just-Code-NET/PolterType/issues/3" rel="noopener noreferrer"&gt;#3&lt;/a&gt;) and the&lt;br&gt;
macOS keystroke hold-back&lt;br&gt;
(&lt;a href="https://github.com/Just-Code-NET/PolterType/issues/8" rel="noopener noreferrer"&gt;#8&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/Just-Code-NET/PolterType" rel="noopener noreferrer"&gt;github.com/Just-Code-NET/PolterType&lt;/a&gt;&lt;br&gt;
· Download: &lt;a href="https://poltertype.com/#download" rel="noopener noreferrer"&gt;poltertype.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you type in two layouts, the ghost is friendly.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>linux</category>
      <category>wayland</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
