diff --git a/WAYLAND.md b/WAYLAND.md new file mode 100644 index 0000000..4d780e4 --- /dev/null +++ b/WAYLAND.md @@ -0,0 +1,301 @@ +# Wayland input method: design and plan + +Plan for work not yet written. Delete this file once the work has landed and +the README describes what is there. + +## 1. What is wrong today + +The popup is X11 only. `win.c` is the sole consumer of `drawc`, it connects +with `xcb_connect`, and when that fails it prints `popup disabled` and +returns (`win.c:206`). Under a Wayland session with no X server there are no +candidates at all, which makes Kanji, Hanja and Emoji unusable. Preedit still +works for clients that reach us over IBus or the GTK module. + +A second, smaller fault: libibus sends `SetCursorLocationRelative` instead of +`SetCursorLocation` when the client runs on Wayland (ibus +`client/gtk3/ibusimcontext.c:1767`). `ictab` (`ibus.c:904`) does not list it, +so libdbus answers `UnknownMethod` and the caret never arrives. + +## 2. What to implement, and what not + +Implement `zwp_input_method_v2` with `zwp_virtual_keyboard_v1`. One protocol +covers every compositor we can serve: + +| Compositor | Protocol | +| --- | --- | +| wlroots: sway, river, labwc, Wayfire, dwl | `zwp_input_method_v2` | +| smithay: niri, COSMIC | `zwp_input_method_v2` | +| KWin | `zwp_input_method_v1` only | +| GNOME | none | + +Not `zwp_input_method_v1`: it buys KWin alone, and KWin starts the input +method itself from its own configuration. + +Not GNOME: it has no input-method protocol, and our IBus endpoint writes the +address file that gnome-shell's own ibus-daemon owns. strans cannot work +there whatever we write. + +`experimental/xx-input-method` in wayland-protocols is where this is heading: +a popup positioner and a per-key consume/passthrough filter that matches our +engine exactly. No compositor implements it (checked: wlroots, smithay, KWin +trees have no trace of it). Revisit once v2 works and we have something to +say about it. + +## 3. Design + +### 3.1 One new file + +`wl.c`, about 600 lines, holding the frontend and its popup. Two protocol XML +files are vendored under `proto/` and turned into `imv2.[ch]` and `vkv1.[ch]` +by wayland-scanner at build time; only the XML is tracked. + +The popup lives in `wl.c` rather than beside `win.c` because +`get_input_popup_surface` is a request on the input-method object: the popup +surface must come from the same connection. Splitting it would mean exporting +the connection, which is a worse trade than breaking the symmetry. + +### 3.2 No drawing process + +`imhandlekey` produces the picture before it answers: + +```c + redraw(); /* strans.c:999, channbsend(drawc, &dc) */ + ... + chansend(kr->reply, &res); /* strans.c:1006 */ +``` + +So by the time a frontend has its `Keyres`, the picture it caused is already +in `drawc`, and `redraw` drains its own stale sends before pushing a new one +(`strans.c:257`), so `drawc` holds at most one, and that one is the engine's +current state. `wl.c` therefore draws inline: + +```c + sendrequest(Keypress, ks, mod, &res); + ... + if(channbrecv(drawc, &dc) > 0) + popup(&dc); +``` + +after `Keypress`, always take and draw; after the `Keycap` owner poll, draw +only when `res.eaten` says the engine is still ours, else hide. + +The consequence is the point: **one process touches libwayland**. It reads +events, dispatches them, draws, and handles `wl_buffer.release`, all in +order. No `wl_display_prepare_read` dance, no mutex, no pipe from a channel +into a poll loop. fcitx5 spends `waylandeventreader.cpp`, 144 lines of thread +plus mutex plus condition variable, on exactly this problem, because its +reader and its dispatcher are different threads. We do not have that problem +and must not invent it. + +`main.c` gains one `proccreate` and `fn.h` one line. `drawthread` is not +started under Wayland, so `drawc` keeps its single consumer. + +### 3.3 One popup per session + +```c + /* main.c */ + if(getenv("WAYLAND_DISPLAY") != nil) + proccreate(wlthread, nil, 32768); + else{ + proccreate(drawthread, nil, 16384); + if(getenv("DISPLAY") != nil) + proccreate(ximthread, nil, 32768); + } +``` + +`srvthread` and `ibusthread` start as they do now, in both cases. + +What this costs: XIM is not served under Wayland, and clients that reach us +over IBus or the GTK module while on Wayland get inline preedit with no +candidate list. Both are documented, and the answer for a user is the same +one sentence: **on Wayland, set none of the X11 variables.** GTK, Qt and +Firefox speak text-input-v3 themselves; `GTK_IM_MODULE` is what pushes them +off the path that works. Two exceptions to check while writing commit 5: +GTK 4 has been reported in 2026 not to bind `zwp_text_input_manager_v3` +unless `GTK_IM_MODULE=wayland` is set, and Chromium still defaults to +text-input-v1. + +### 3.4 Keys + +* `activate`, `deactivate` and `content_type` are pending state applied at + `done`; count `done` events, that count is the serial `commit` takes. +* Grab the keyboard on every activate, releasing the previous grab first. Two + activates can arrive without a deactivate between them when focus moves + between clients, and the second `grab_keyboard` then fails and leaves a + dead object. +* `keymap` gives an fd: compile it with `xkb_keymap_new_from_string` and hand + the same fd to `zwp_virtual_keyboard_v1.keymap`, so a key we forward means + what it meant. +* `key` gives an evdev code: `xkb_state_key_get_one_sym(state, code+8)`, then + `composekey` (shared with the XIM and IBus frontends, no new table), then + `ipckeysym` and the modifier mask to the engine. +* A key the engine did not eat goes back through + `zwp_virtual_keyboard_v1.key`. Record forwarded codes in a `u32int + sent[8]` bitmap: eat the release of a press we ate, and release whatever is + still down when the input method deactivates, or the client is left holding + a key. +* The virtual keyboard must be created on the same `wl_display` as the input + method. Nothing in the protocol says so; sway skips the grab for a virtual + keyboard whose client owns the grab (`sway/input/keyboard.c:419-431`, with + a TODO pointing at wlroots#2322), and on another connection our forwarded + keys would come straight back to us. fcitx5 relies on the same contract. +* Order within a key: `commit_string`, `set_preedit_string`, `commit(serial)`, + then `vk.key`, then one flush. Requests on one connection are processed in + order, which is what keeps a commit ahead of the key that caused it. + +We do not commit ordinary keys as text. fcitx5 does by default +(`waylandimserverbase.cpp:35-48`, unmodified printable keys become +`commit_string`), to dodge exactly the ordering question above. Our `eaten = +0` means "this key is not mine", and a key event is the honest answer to +that; committing text would lie to terminals and to anything with a +keybinding. If ordering does break on some compositor, that workaround is +known and can be added then. + +### 3.5 Popup + +* One `wl_surface` and one `zwp_input_popup_surface_v2`, created once. The + compositor makes them visible on activate and invisible on deactivate, and + places them at the text cursor: `popuparea` and `popupposition` are not + used here and stay X11-only. +* Two shm buffers, used in turn, each busy from `attach` until + `wl_buffer.release`. `win.c`'s single grow-only image would be redrawn + while the compositor was still reading it. +* `WL_SHM_FORMAT_XRGB8888` is `CAIRO_FORMAT_RGB24` on a little-endian host, + so `popupdraw` writes the buffer directly, as it does for X11. `win.c` + already checks the same assumption for the X server (`validformat`). +* Hide by attaching a nil buffer and committing. If a compositor keeps a + stale popup, destroy the surface instead, which is what fcitx5 does + (`waylandinputwindow.cpp:158-168`). +* `popuplayout` wants an area: take it from the `wl_output` the surface + entered, else the first output. +* Scale stays `popupscale` from `GDK_SCALE`, as on X11. + `wl_surface.preferred_buffer_scale` is the better answer, later. + +### 3.6 What does not change + +| | | +| --- | --- | +| `strans.c`, `Keyreq`, `Keyres`, `Drawcmd`, `ipc.*` | nothing | +| `popup_layout.c`, `font.c` | nothing | +| `ibus.c`, `srv.c`, `xim.c`, `gtk/` | nothing, beyond the one-line IBus fix | +| `dat.h` | `Ibuspurposepassword`, `Ibuspurposepin` move out of `ibus.c` as `Purposepassword`, `Purposepin`; both frontends read the same two numbers | +| `main.c` | frontend choice | +| `fn.h` | `void wlthread(void*);` | + +The engine needing no change is the argument that this is the right shape. +`Keyres.eaten` is already consume-or-pass, `Keyres.commit` is already +`commit_string`, `Keyres.preedit` is already `set_preedit_string`, and +`clientpre` is simply always true here, which leaves the popup showing +candidates and the mode mark and nothing else. + +## 4. Rejected + +**A popup per display, routed by context.** This is what fcitx5 does: +`uis_["x11:..."]` and `uis_["wayland:..."]` side by side +(`classicui.cpp:80-121`), chosen by the focus group's display name. It would +keep XIM alive under XWayland. It costs a channel in `Keyreq`, a `lastdraw` +per sink, blanking the loser on every owner change, and `font.c`'s single +Pango layout drawn from two processes. It also makes the frontends know about +each other: fcitx5 has to drop its X keyboard grab whenever a Wayland client +takes focus (`xcb/xcbconnection.cpp:443-460`). fcitx5 needs this because it +is a multi-seat, multi-display, multi-user daemon. strans is one user's. If +XIM under XWayland is ever wanted, the routing key to use is the one fcitx5 +uses, and it is about ten lines. + +**A second process for drawing, or a second connection.** See 3.2. The popup +surface cannot come from a second connection, and a second process sharing +the first buys only the locking rules we currently do not need. + +**A GNOME fallback to the X11 popup.** Machinery for a case that cannot work +anyway. + +**Hand-marshalling with `wl_proxy_marshal_flags`** instead of +wayland-scanner. Less code, unreadable. The generated files are 1448 lines +and none of them are ours to read. + +**Choosing candidates with the mouse.** fcitx5's popup takes clicks, hover, +wheel and touch. Ours eats clicks and does nothing, on X11 today; keep it +that way. + +## 5. Commit plan + +1. `ibus: SetCursorLocationRelative is a caret too` — one entry in `ictab`. + Unrelated to the rest, and wrong today. +2. `build: the input-method and virtual-keyboard protocols` — two XML files, + the scanner rules, the LICENSES note. No behaviour. +3. `wl: the input-method frontend` — activate, deactivate, content type, + done and its serial; grab per activate; keys through xkb and `composekey`; + commit and preedit; unhandled keys through the virtual keyboard, released + on deactivate. Started from `main.c` under `WAYLAND_DISPLAY`. No popup + yet, so the X11 popup still owns `drawc` and nothing collides. +4. `wl: the candidate popup` — surface, two shm buffers, drawn from `drawc` + where the reply arrives. In the same commit, `main.c` stops starting + `drawthread` and `ximthread` under Wayland, because that is where the two + popups would collide. +5. `docs: Wayland` — set nothing; no XIM; no candidates for IBus and GTK + module clients. + +Later, separately: key repeat, `preferred_buffer_scale`, reconversion through +`surrounding_text`, and a headless live test. + +## 6. Verification + +The toolchain question is already answered. In the container, wayland-scanner +output, libwayland-client, xkbcommon and Plan 9 port headers compile together +under `9c -Wall -Wextra` with no warnings, and `9l` links them with +`-lthread -lbio`: + +``` +imv2.c 133 imv2.h 955 vkv1.c 78 vkv1.h 282 (generated, not tracked) +9c -I/src $(pkg-config --cflags wayland-client xkbcommon) -Wall -Wextra -c wl.c +9l -o strans ... -lthread -lbio $(pkg-config --libs wayland-client xkbcommon) +``` + +`Dockerfile` needs `wayland` added, and `sway` (extra, 1.12) when the live +test arrives. + +Unit tests cover only what is pure: the xkb modifier mask and the forwarded +key bitmap, in the shape `xim_adapter_test` already uses, compiling `wl.c` +into the test and building a keymap from a string, no server. Everything else +is checked by running it under sway. + +A headless live test — a compositor, a text-input-v3 client, and a second +virtual keyboard to type with — is the size of `ibus_live_test.c`. Decide +after commits 3 and 4 work by hand; do not build it first. + +## 7. Known limits + +* A key we eat does not repeat. `zwp_input_method_v2`'s grab has no + compositor-side repeat unless the compositor sends `wl_keyboard`'s + `repeated` key state through it, which is opportunistic + (fcitx5 `waylandimserverv2.cpp:494-503`). Doing it ourselves is a timer + over `repeat_info`, twenty lines, later. +* Compositor keybindings run before the input method (sway + `keyboard.c:563`), so a chord like `Ctrl+N` can be taken from us. +* Text pending when the client loses focus may be lost: `deactivate` is the + first we hear of it and a `commit_string` afterwards can be dropped. +* The protocol gives one input context per seat, so we cannot tell which + application we are typing into. We keep no per-application state, so this + costs us nothing; fcitx5 tracks the focused window over a separate protocol + to work around it. + +## 8. References + +Checked at the versions current on 2026-08-17. + +* wayland-protocols 1.49: `unstable/text-input/text-input-unstable-v3.xml`, + `experimental/xx-input-method/xx-input-method-v2.xml`, + `experimental/xx-keyboard-filter/xx-keyboard-filter-v1.xml`. +* wlroots: `protocol/input-method-unstable-v2.xml`, + `protocol/virtual-keyboard-unstable-v1.xml`, the two files to vendor. +* sway `sway/input/keyboard.c:419-431`, the virtual keyboard client-identity + rule; `:563`, bindings before the grab. +* fcitx5 `a31cff7`: `frontend/waylandim/waylandimserverv2.cpp:170` serial, + `:186-193` releasing forwarded keys, `:213-218` regrabbing, + `:494-503` repeat; `waylandimserverbase.cpp:35-48` commit-as-text; + `ui/classic/classicui.cpp:80-121` a UI per display; + `ui/classic/waylandshmwindow.cpp:69-113` two buffers; + `ui/classic/waylandinputwindow.cpp:158-168` hiding; + `modules/wayland/waylandeventreader.cpp` the reader thread; + `modules/xcb/xcbconnection.cpp:443-460` the two frontends interfering. +* ibus `client/gtk3/ibusimcontext.c:1767`, the relative caret on Wayland.