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

14 KiB

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:

	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:

	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

	/* 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.