docs: Wayland

One popup per session, so the frontend is picked at startup: a compositor
with zwp_input_method_v2 gets that frontend and neither XIM nor the X11
popup is started.  The answer for a user is one sentence -- on Wayland set
none of the module variables -- and the two exceptions the plan asked to
check turned out this way: GTK 4 binds text-input-v3 by itself, with
GTK_IM_MODULE unset (4.22 under sway 1.12, typed and composed), and
Chromium is beyond help either way, since it asks for text-input-v1 and
wlroots implements only v3.

The plan is deleted, as it said to be once the README described what
landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 19:43:38 +09:00
parent 5664a008de
commit 2e1e0b9645
2 changed files with 35 additions and 313 deletions

View File

@@ -1,8 +1,10 @@
# strans # strans
strans is a small, single-user input method for Korean, Japanese, English, strans is a small, single-user input method for Korean, Japanese, English,
emoji, and symbols. It provides one engine for IBus (which GTK 4 and emoji, and symbols. It provides one engine for Wayland compositors that
Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a speak `zwp_input_method_v2` — the wlroots and smithay families, so sway,
river, labwc, Wayfire, dwl, niri and COSMIC — and for IBus (which GTK 4
and Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a
compatibility mode. compatibility mode.
## Input modes ## Input modes
@@ -72,6 +74,7 @@ clicking elsewhere commits what is pending.
| Frontend | Preedit | Candidates | | Frontend | Preedit | Candidates |
| --- | --- | --- | | --- | --- | --- |
| Wayland input method | inline in the client | strans popup |
| GTK 3 and IBus | inline in the client | strans popup | | GTK 3 and IBus | inline in the client | strans popup |
| XIM PreeditCallbacks | inline through XIM callbacks | strans popup | | XIM PreeditCallbacks | inline through XIM callbacks | strans popup |
| XIM PreeditPosition | strans popup | strans popup | | XIM PreeditPosition | strans popup | strans popup |
@@ -90,11 +93,13 @@ style, and above the line when there is no room below; otherwise the popup
sits under the focus window, or under the client window when no focus sits under the focus window, or under the client window when no focus
window is set. window is set.
The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is On a compositor with `zwp_input_method_v2` the popup is a Wayland surface
X11-only, and GTK popup placement converts the GTK caret to X11 root that the compositor itself puts at the text cursor, flipping it above the
coordinates. There is no native Wayland popup surface or Wayland caret line when there is no room below; a client that reports no cursor location
positioning. GTK and IBus clients can still use inline preedit where their gets it under its window instead. Everywhere else the popup is an X11 window,
display environment supports it. placed from the XIM spot or from the GTK caret converted to root
coordinates, and XIM is X11-only. Either way `GDK_SCALE` sizes it for
HiDPI displays.
## Build and test ## Build and test
@@ -109,7 +114,7 @@ make docker-build
Build output: Build output:
```text ```text
strans daemon with IBus and XIM frontends strans daemon with Wayland, IBus, and XIM frontends
gtk/im-strans.so GTK 3 frontend module gtk/im-strans.so GTK 3 frontend module
``` ```
@@ -133,6 +138,8 @@ development files for:
- Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`); - Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`);
- D-Bus development files; - D-Bus development files;
- XCB, XCB RandR, XCB XKB, xcb-util, xcb-imdkit, xkbcommon, and xkbcommon-x11; - XCB, XCB RandR, XCB XKB, xcb-util, xcb-imdkit, xkbcommon, and xkbcommon-x11;
- the Wayland client library and `wayland-scanner`, which writes the
input-method and virtual-keyboard bindings from the XML in `proto/`;
- Pango, PangoCairo, Cairo, and Fontconfig; - Pango, PangoCairo, Cairo, and Fontconfig;
- GTK 3, plus libibus and Xlib for the live clients; - GTK 3, plus libibus and Xlib for the live clients;
- Xvfb for the X11 live tests; - Xvfb for the X11 live tests;
@@ -158,8 +165,9 @@ build-tree daemon in the background, and returns to the shell. It requires
`pkill` from procps. Startup and frontend failures remain visible on the `pkill` from procps. Startup and frontend failures remain visible on the
caller's standard error. A service supervisor should instead run caller's standard error. A service supervisor should instead run
`./strans map` directly in the foreground, with the session's `./strans map` directly in the foreground, with the session's
`XDG_RUNTIME_DIR` and `DISPLAY`: the GTK module finds the daemon at `XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `DISPLAY`: the GTK module
`$XDG_RUNTIME_DIR/strans.sock` (else `/tmp/strans.UID`), and IBus clients finds the daemon at `$XDG_RUNTIME_DIR/strans.sock` (else
`/tmp/strans.UID`), and IBus clients
through the address file libibus expects under `~/.config/ibus/bus/`, through the address file libibus expects under `~/.config/ibus/bus/`,
which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY` which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY`
when there is one and `DISPLAY` otherwise. when there is one and `DISPLAY` otherwise.
@@ -169,6 +177,20 @@ opens `hira.map`, `kata.map`, `telex.map`, `kanji.dict`, `emoji.dict`, and
`hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/` `hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/`
directory. Only the GTK subdirectory has install and uninstall targets. directory. Only the GTK subdirectory has install and uninstall targets.
strans shows one popup per session, so it picks its frontend at startup: a
session whose compositor offers `zwp_input_method_v2` gets that frontend,
and neither XIM nor the X11 popup is started; every other session gets XIM
as before. The IBus endpoint and the GTK 3 module are served in both.
On Wayland, then, set none of the variables below. GTK, Qt and Firefox
speak text-input-v3 themselves and need nothing — GTK 4 binds it without
`GTK_IM_MODULE`, checked with 4.22 under sway 1.12 — and `GTK_IM_MODULE`
is what pushes them off the path that works. A client that reaches strans
over IBus or the GTK 3 module while the Wayland frontend runs still
composes inline, but shows no candidate list, because the popup belongs to
that frontend. Chromium is helped by neither: it still asks for
`text-input-v1`, which wlroots does not implement at all.
Configure clients as needed: Configure clients as needed:
```sh ```sh
@@ -201,8 +223,9 @@ doas make -C gtk uninstall
strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not
required. IBus clients that process keys synchronously — GTK 4, or GTK 3 required. IBus clients that process keys synchronously — GTK 4, or GTK 3
with `IBUS_ENABLE_SYNC_MODE=1` — get their commits and preedits through with `IBUS_ENABLE_SYNC_MODE=1` — get their commits and preedits through
`PostProcessKeyEvent`, in order with the key. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM `PostProcessKeyEvent`, in order with the key. With neither
is not started and the popup is disabled. `WAYLAND_DISPLAY` nor `DISPLAY`, the daemon, its IBus endpoint and its
socket still work, but no frontend draws a popup.
## Dictionary data ## Dictionary data

View File

@@ -1,301 +0,0 @@
# 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.