Auto-correcting wrong-layout typing on Wayland is nearly impossible. We did it anyway
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.
本文最初发布于 PolterType 博客。如果你使用多种键盘布局,你一定经历过这样的时刻:抬头看屏幕,发现刚才打出的词变成了 Espa;a。因为键盘还停留在美式英语布局,而西班牙语的 ñ 恰好就在那个键位上。于是你删掉单词,切换布局,重新输入——这已经是今天第三十次了。字母表差异越大(如希腊语、希伯来语、西里尔语),情况就越糟糕。
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. 自动修复此类问题的工具由来已久。Windows 从 2000 年代起就有这类软件(如 Punto Switcher 及其衍生品),X11 环境下则有 xneur:它们监视输入内容,识别出当前布局下的乱码在另一种布局下是有效单词,然后自动切换布局并重打。后来 Linux 桌面转向了 Wayland,这类应用便悄然消亡了——并非因为没人需要,而是因为 Wayland 的设计初衷就是让这类应用无法实现。PolterType 是一款开源(MIT 协议)的纯 Rust 编写的托盘应用,它打破了这一限制,支持 Windows、macOS 和 Linux。在 Linux 上,它甚至支持最难攻克的 Wayland,这也是本文要讨论的主题。
Three doors, all locked
三道紧锁的门
To fix a word typed in the wrong layout you need three capabilities: hear keystrokes globally, in every application; inject keystrokes, to erase the wrong word and retype it; 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. 要修复错误布局输入的单词,需要三种能力:全局监听所有应用的按键;注入按键以删除错误单词并重打;在编辑文本旁绘制提示框。X11 将这三种能力赋予了任何能打开显示窗口的客户端:根窗口的原始 XInput2 事件、用于注入的 XTest,以及用于弹出窗口的 override-redirect 窗口。这正是 X11 成为键盘记录器天堂的原因,也是 Wayland 设计者拒绝保留这些能力的原因。Wayland 没有全局按键事件协议,没有在屏幕坐标放置窗口的协议。合成输入仅存在于大多数合成器多年未实现的 Portal 接口之后。从安全角度看,这一切都是正确的;但从布局纠错器的角度看,这三道门都被锁死了。以下是我们为每一道门找到的钥匙。
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.
如果合成器不转发输入,那就去合成器读取的地方读取:evdev。PolterType 的监听器直接读取 /dev/input/event*,位于显示服务器之下,因此它在任何合成器(Hyprland、Sway、KDE、GNOME 等)上都能以相同方式工作。代价是:需要加入 input 用户组并配置 udev 规则,安装后只需执行一次 sudo 即可(scripts/setup-linux.sh 会自动处理;应用内的设置面板会检测你的机器并提示缺失的部分,包括 usermod -aG input 无法对当前登录会话生效的经典陷阱)。在 X11 上,该应用无需任何权限——这种对比正是 Wayland 安全模型预期的效果,我们在此如实陈述,而非掩盖。
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.
读取 evdev 意味着你得到的是键码而非字符。PolterType 自带了布局映射——通过 xkeyboard-config 生成的 TOML 文件,而非手动转录——目前捆绑了 15 种布局,且仅加载操作系统实际启用的布局。我们曾考虑过另一种方案,但在测试后放弃了:AT-SPI(辅助功能总线)在文档上提供了一个按键监听 API。但在 wlroots 合成器上注册它会返回失败,即使在内核层注入按键也无法获取内容,因为 at-spi2-registryd 本身没有键盘——在 Wayland 上,它只转发合成器给它的内容,而目前只有 Mutter 这样做。在它能工作的环境(X11)中,现有的监听器根本不需要权限。因此:我们放弃了 AT-SPI 监听器,这是一个基于测试结果的决定,而非待办事项。
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.
纠错通过 uinput 虚拟键盘输出:删除单词、切换布局、重新输入。同样的安装脚本也处理了 /dev/uinput,所以权限问题是一次性解决的。从 0.10.0 版本开始,对于无法打开 uinput 的会话,我们提供了备选方案:RemoteDesktop portal,这是请求合成器合成输入的标准授权方式。它仅在 uinput 不可用时触发,因此运行过安装脚本的用户不会看到授权弹窗。坦白说:该路径是根据规范编写的,尚未在真实的 GNOME 或 KDE 会话中运行——代码中已对此做了标记,如果出现异常,我们会先假设是 PolterType 的问题,而不是合成器的问题。
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.
这里有一个没人提醒你的难点:Wayland 也没有通用的布局切换 API。每个桌面环境都有自己的布局管理方式,因此 PolterType 会按优先级探测一系列后端:hyprctl switchxkblayout、KDE 的 org.kde.keyboard D-Bus 接口、GNOME 系列的 gsettings 输入源、IBus、Fcitx5,以及作为基础窗口管理器备选的 X11 XKB 组锁定。如果这些都无法响应,应用会拒绝启动,而不是留在托盘里检测那些它无法修复的错误。
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…
当 PolterType 对自动纠错没有把握时,它会显示一个小提示,你可以通过组合键接受。在 Wayland 上,这个看似简单的功能其实是两道锁在一起的门。首先是窗口表面:Wayland 客户端无法自行定位窗口。逃生通道是 zwlr_layer_shell_v1(面板和通知守护进程使用的协议),它提供了一个拥有独立坐标空间的覆盖层。wlroots 合成器(Hyprland、Sway)支持它,KWin 也已实现多年(我们之前记录有误,直到测试 KWin 6.7.3 后发现其表现与 Hyprland 完全一致)。Mutter 没有 layer-shell,在那里提示框通过 XWayland 映射为 X11 的 override-redirect 窗口。唯一未覆盖的情况是禁用了 XWayland 的 GNOME 类会话。其次是位置:没有任何协议告诉你文本光标在哪里。但辅助功能总线可以——AT-SPI 中唯一真正可用的部分:订阅 object:text-caret-moved 事件,向焦点组件查询 GetCharacterExtents,你就能得到……