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>
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,deactivateandcontent_typeare pending state applied atdone; countdoneevents, that count is the serialcommittakes.- 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_keyboardthen fails and leaves a dead object. keymapgives an fd: compile it withxkb_keymap_new_from_stringand hand the same fd tozwp_virtual_keyboard_v1.keymap, so a key we forward means what it meant.keygives an evdev code:xkb_state_key_get_one_sym(state, code+8), thencomposekey(shared with the XIM and IBus frontends, no new table), thenipckeysymand 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 au32int 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_displayas 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), thenvk.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_surfaceand onezwp_input_popup_surface_v2, created once. The compositor makes them visible on activate and invisible on deactivate, and places them at the text cursor:popupareaandpopuppositionare not used here and stay X11-only. - Two shm buffers, used in turn, each busy from
attachuntilwl_buffer.release.win.c's single grow-only image would be redrawn while the compositor was still reading it. WL_SHM_FORMAT_XRGB8888isCAIRO_FORMAT_RGB24on a little-endian host, sopopupdrawwrites the buffer directly, as it does for X11.win.calready 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). popuplayoutwants an area: take it from thewl_outputthe surface entered, else the first output.- Scale stays
popupscalefromGDK_SCALE, as on X11.wl_surface.preferred_buffer_scaleis 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
ibus: SetCursorLocationRelative is a caret too— one entry inictab. Unrelated to the rest, and wrong today.build: the input-method and virtual-keyboard protocols— two XML files, the scanner rules, the LICENSES note. No behaviour.wl: the input-method frontend— activate, deactivate, content type, done and its serial; grab per activate; keys through xkb andcomposekey; commit and preedit; unhandled keys through the virtual keyboard, released on deactivate. Started frommain.cunderWAYLAND_DISPLAY. No popup yet, so the X11 popup still ownsdrawcand nothing collides.wl: the candidate popup— surface, two shm buffers, drawn fromdrawcwhere the reply arrives. In the same commit,main.cstops startingdrawthreadandximthreadunder Wayland, because that is where the two popups would collide.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 sendswl_keyboard'srepeatedkey state through it, which is opportunistic (fcitx5waylandimserverv2.cpp:494-503). Doing it ourselves is a timer overrepeat_info, twenty lines, later. - Compositor keybindings run before the input method (sway
keyboard.c:563), so a chord likeCtrl+Ncan be taken from us. - Text pending when the client loses focus may be lost:
deactivateis the first we hear of it and acommit_stringafterwards 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:170serial,:186-193releasing forwarded keys,:213-218regrabbing,:494-503repeat;waylandimserverbase.cpp:35-48commit-as-text;ui/classic/classicui.cpp:80-121a UI per display;ui/classic/waylandshmwindow.cpp:69-113two buffers;ui/classic/waylandinputwindow.cpp:158-168hiding;modules/wayland/waylandeventreader.cppthe reader thread;modules/xcb/xcbconnection.cpp:443-460the two frontends interfering. - ibus
client/gtk3/ibusimcontext.c:1767, the relative caret on Wayland.