diff --git a/README.md b/README.md index bd7e29f..7ef9982 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # strans 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 -Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a +emoji, and symbols. It provides one engine for Wayland compositors that +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. ## Input modes @@ -72,6 +74,7 @@ clicking elsewhere commits what is pending. | Frontend | Preedit | Candidates | | --- | --- | --- | +| Wayland input method | 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 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 window is set. -The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is -X11-only, and GTK popup placement converts the GTK caret to X11 root -coordinates. There is no native Wayland popup surface or Wayland caret -positioning. GTK and IBus clients can still use inline preedit where their -display environment supports it. +On a compositor with `zwp_input_method_v2` the popup is a Wayland surface +that the compositor itself puts at the text cursor, flipping it above the +line when there is no room below; a client that reports no cursor location +gets it under its window instead. Everywhere else the popup is an X11 window, +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 @@ -109,7 +114,7 @@ make docker-build Build output: ```text -strans daemon with IBus and XIM frontends +strans daemon with Wayland, IBus, and XIM frontends gtk/im-strans.so GTK 3 frontend module ``` @@ -133,6 +138,8 @@ development files for: - Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`); - D-Bus development files; - 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; - GTK 3, plus libibus and Xlib for the live clients; - 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 caller's standard error. A service supervisor should instead run `./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/strans.sock` (else `/tmp/strans.UID`), and IBus clients +`XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `DISPLAY`: the GTK module +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/`, which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY` 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/` 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: ```sh @@ -201,8 +223,9 @@ doas make -C gtk uninstall strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not required. IBus clients that process keys synchronously — GTK 4, or GTK 3 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 -is not started and the popup is disabled. +`PostProcessKeyEvent`, in order with the key. With neither +`WAYLAND_DISPLAY` nor `DISPLAY`, the daemon, its IBus endpoint and its +socket still work, but no frontend draws a popup. ## Dictionary data diff --git a/WAYLAND.md b/WAYLAND.md deleted file mode 100644 index 4d780e4..0000000 --- a/WAYLAND.md +++ /dev/null @@ -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.