Files
strans/WAYLAND.md
Hojun-Cho e93ec43706 docs: the plan for a Wayland input method
zwp_input_method_v2 in one new file, with its popup, drawn from the reply
the engine already produced; the session picks the popup, so the engine,
the IPC and the X11 side are untouched.  Records what was measured, what
fcitx5 does differently and why, and what was refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:18:32 +09:00

302 lines
14 KiB
Markdown

# 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.