Commit Graph

291 Commits

Author SHA1 Message Date
bbc0c97656 main: GDK_SCALE is read once
Both popups read it, each just before its own textinit, and the second
copy needed a comment to say why it was there at all.  One frontend runs
per session and popupscale is one global, so threadmain reads it before it
starts either -- which is before any setfont, the only ordering the two
copies were keeping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:26:59 +09:00
aef577026d dat: one pair of content purposes
Two frontends read the same two numbers, and wl.c carried four lines of
comment to say why it kept its own copy of them.  IBus and text-input-v3
number the purposes alike because both took them from GTK, so the header
is where they belong and the explanation goes away with the copy.  hidden
stays in both files: one asks about a context, the other about the seat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:26:48 +09:00
8472474332 wl: modmask's caller has already looked at the keymap
grabkey returns before it asks for the mask when there is no keyboard
state, so the guard inside modmask answered a question nobody put to it --
except the test, which was the only caller that could reach it.  A check
kept alive by the test that reaches it is one line of code and one line of
test to delete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:22:36 +09:00
33710178d4 wl: no keyboard grab while inactive
An activate and a deactivate in one batch left the grab taken with nothing
active, which is the one state the grab must never be in: sway hands keys
to the grab holder without asking whether the input method is active.
sway cannot produce that batch -- relay_send_im_state sends a done after
each -- so this is the table being total, not a bug that bit.  The
deactivate arm was already right; only the activate arm forgot to look at
where the batch ended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:22:10 +09:00
5cb9f69590 wl: every keycode evdev can send
The bitmap held 256, and forward dropped anything above it rather than
passing it on, so a key the engine does not want vanished instead of
reaching the client.  Keys do live up there: KEY_FN is 0x1d0,
KEY_VOICECOMMAND 0x246, KEY_MACRO1 0x290, and xkeyboard-config maps
keycodes to <I709>, so they carry a keysym and are nothing special to us.
sway matches its bindings before it hands a key to the grab, so an unbound
one of those was lost for as long as any text field had the focus.

The whole evdev range costs 96 bytes of bitmap instead of 32.  The test
already writes the bound rather than the number, so it still says what it
said.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:54 +09:00
6d511919e8 wl: a content purpose belongs to its activation
pendingpurpose was never cleared, and sway sends content_type only when
the text input asked for one: active_features is frozen at the enable
commit (wlr_text_input_v3.c) and relay_send_im_state tests it before
sending.  So a client that sets no content type leaves the last client's
purpose standing -- and after a GTK password entry that purpose is
password, so every key in the next window was forwarded raw and strans
looked dead there until a client that does declare one took the focus.

A purpose is per activation, as it is per context in ibus.c.  Clearing it
on activate is enough: the content type, if any, arrives after the
activate and before the done that applies both.

The sway runs could not have found this; the password client was GTK,
which always declares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:24 +09:00
40fd4ab551 wl: a deactivate hands the pending text back
Two things arrive as a deactivate.  When the focus moved, the compositor
sent the text input its leave first, and sway's handle_im_commit finds no
focused text input and drops the whole commit -- which is what the old
comment described.  But a client that merely disabled its text input
still holds the focus: sway relays that deactivate only in that case
(handle_text_input_disable returns early once the surface is unfocused),
so the commit would have been delivered, and half a syllable was thrown
away instead.  A GtkEntry losing the focus to a button in its own window
is that case.

XIM and IBus both hand the text back there and drop it only when the
client itself is gone -- ibus.c keeps the two apart as flushcontext and
releasecontext.  This gives the Wayland frontend the same halves.  The
commit that goes nowhere costs one empty request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:07 +09:00
841d93ac35 docs: the README says it once
It had grown to 256 lines, a third of them the engine's fine behaviour
told twice and packaging trivia that belongs in the Makefile.  Every
user-facing fact is kept; the prose around it is not.  The dependency
list is one sentence and a pointer at the Dockerfile that already names
the packages exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:45:04 +09:00
f6dd056ffb build: 9c and 9l can report again
Both filter the compiler's output through $egrep, and plan9port assigns
that variable nowhere -- six scripts read it, one unrelated developer
script sets it.  The pipeline dies on the empty command and takes the
whole diagnostic with it, so -Wall -Wextra was decorative and a build
that failed to compile printed nothing but an exit status.  That is how
the tests' own dialect bug hid until it was hunted with a patched 9c.

Both scripts source $PLAN9/config, which the distribution does not ship,
so one line in it is the fix the scripts themselves ask for; it needs no
patching of a packaged file and covers 9c and 9l together.  The daemon it
builds is byte-identical, and with the diagnostics visible the whole tree
and every test object compile without a warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:04:33 +09:00
ad2f2fc5eb test: the xkb modifier mask and the forwarded key bitmap
The two pure things in the Wayland frontend, in the shape the XIM adapter
test already uses: wl.c included behind one define that makes the virtual
keyboard inert, and a keymap built from a string, so no compositor and no
xkb data files are wanted.  The mask test holds Caps Lock apart from
Shift, which is what a Korean key turns on; the bitmap test holds a
release of a key we never passed on, a code the bitmap cannot hold, and
the release of everything still down when the context goes.

wayland-scanner's output is the parent Makefile's to write, as ../strans
already is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:51:37 +09:00
2e1e0b9645 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>
2026-08-17 19:47:50 +09:00
5664a008de wl: the candidate popup
One wl_surface and one zwp_input_popup_surface_v2, made once; the
compositor makes them visible on activate, puts them at the text cursor
and takes them down again, so popuparea, popupposition and Caret stay
X11-only and popuplayout gets a constant area.  Two shm buffers are used
in turn, each busy from attach until the compositor releases it, because
win.c's single grow-only image would be redrawn while the compositor was
still reading it; a picture that arrives while both are busy waits for
the next release, and the engine will not send it twice.  drawthread no
longer runs, so wl.c reads GDK_SCALE itself and calls textinit after it,
since setfont reads Fontsz.  The buffer is rounded up to a multiple of
the scale: one that is not is an invalid_size error at attach, and a
preedit alone really does lay out to an odd width at GDK_SCALE=2.

drawc is taken where the engine's picture and our surface can part:
after every key, after the Keyrelease a deactivate sends, and after the
owner poll, hiding for the last two.  The poll is not an optimisation --
once another frontend takes the engine, its pictures go unconsumed and
no later send will ever say hide.

main.c now starts either the Wayland frontend or the X11 popup and XIM,
which is where the two popups would otherwise collide.

Checked by hand under headless sway 1.12, reading grim screenshots: the
hanja list for 가 with its selection and 1-9/125 marker, the emoji
search, the popup gone after Escape, and the same at GDK_SCALE=2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:35:19 +09:00
4ac0625c6b wl: the input-method frontend
zwp_input_method_v2 over one connection, in one process: activate,
deactivate and content type applied at done, whose count is the serial a
commit must carry.  The keyboard is grabbed at every activate and
released at every deactivate, because sway hands keys to the grab holder
without asking whether the input method is active, and a grab held while
inactive would take every key in the session.  Keys come as evdev codes,
go through xkb and the Compose table the other frontends already share,
and reach the engine as an ipckeysym and a modifier mask; the grab's
modifiers go on to the virtual keyboard as well, since wlroots derives no
state from a virtual keyboard's own keys.  A key the engine does not eat
goes back as a key, not as text, and whatever is still down is released
when the context goes.  A password or a PIN purpose never reaches the
engine at all.

No popup yet: the X11 one still owns drawc, and with no Keycaret from us
it follows the pointer.

Checked by hand under headless sway 1.12 with a GTK3 entry driven by
wtype: ascii passes through, Ctrl+s selects Korean, rk shows the preedit
and Enter commits 가 and lets the Return on, Ctrl+c arrives with its
modifier, focus moving between two clients re-grabs and keeps typing, a
password entry types literally, a second strans says so and stops, and
WAYLAND_DISPLAY without a compositor falls back to XIM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:17:27 +09:00
233884e51f build: the input-method and virtual-keyboard protocols
zwp_input_method_v2 and zwp_virtual_keyboard_v1, vendored verbatim from
wlroots 012ca825 under proto/ and turned into imv2.[ch] and vkv1.[ch] by
wayland-scanner; only the XML is tracked, and nothing compiles them yet.
The Dockerfile gains wayland, which carries the scanner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:55:41 +09:00
27180bc0fe test: a key passed through is not committed text
A Korean Enter commits the syllable and lets the key on, so xim.c sends
the commit and then forwards the key.  pumpinput took the text of either
and kept the last, so under load the Return's own carriage return
overwrote 가: 22 of 25 runs failed on a loaded machine and none on an
idle one.  An XIM commit arrives as XLookupChars; a forwarded key comes
back as XLookupBoth, and is not a commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:12 +09:00
f6477cb97d test: the unit build takes the dialect the daemon is built with
9c compiles with -std=gnu11 and UNIT_CFLAGS overrode that with -std=c99,
so the tests built the daemon's own sources under a dialect the daemon
never sees.  Under it glibc hides lstat, and server_test.c reaches
<sys/stat.h> before srv.c pulls in u.h -- it must, since thread.h
defines recv as chanrecv and the socket recv it calls has to be declared
first.  So make all was green while make check would not compile.
HOST_CFLAGS keeps -std=c99: those are host programs with no Plan 9
headers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:12 +09:00
059e8bbc75 ibus: SetCursorLocationRelative is not a caret
A GTK client on the Wayland backend sends this where an X11 one sends
SetCursorLocation, and ictab did not list it, so libdbus answered
UnknownMethod.  Accept it and throw it away: the absolute translation
sits inside GDK_IS_X11_DISPLAY, so the coordinates are surface local,
and ibus's own daemon will not hand them to an engine either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:04 +09:00
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
544ed835b7 gtk: find the module directory through the GTK runtime again
A module built in the container is installed on a host that has no GTK
development files, and there pkg-config knows nothing: dropping the
runtime query left `doas make install` with nowhere to put the module.
gtk-query-immodules-3.0 answers where GTK looks, so ask it when
pkg-config cannot, as before, and say so in the README.
2026-08-17 14:37:57 +09:00
ae7dbae993 engine: Shift on the okurigana asks for that reading, as SKK does
kaku offers the twenty-two readings of かく before 書く, because the SKK
dictionary keys a verb by its stem and gives no frequency to rank the two
lists by.  SKK's own answer is the shift key: kaKu says where the
okurigana starts, and that split now comes first — the reading's own
candidates still follow it.  Caps Lock sends no Shift, so it marks
nothing, and a word typed without Shift is unchanged.
2026-08-17 12:53:12 +09:00
2e53627b7d data(hanja): words, not only syllables
The import kept only readings of one syllable, so the Hanja search could
convert 한 but never 한자, 학교, or 대한민국 — the conversion every other
Korean input method offers.  libhangul's table has 187k readings; both
scripts now keep them all, and the search finds a word as readily as a
syllable.  The daemon pays for it: 24 MB instead of 12, and 170 ms to
start instead of 30.
2026-08-17 12:50:05 +09:00
3527bcd489 trie: one node per rune, children in rune order, and the last path kept
The trie walked a key byte by byte, so a Hangul syllable cost three
nodes and every lookup first spelled its key back into UTF-8.  It walks
runes now: a third of the nodes for Korean data, and no buffer in the
lookup.  Children are kept in rune order, so both the search and the
insert stop early, and a trie remembers the path of the last key put,
which a file sorted by key — hanja.dict is one — walks straight down.
2026-08-17 12:50:05 +09:00
6a6749e824 engine: a mode switch shows the mode it switched to
Ctrl+S, Ctrl+N and the rest changed the mode with nothing to see; the
next key was the only way to tell.  The popup now shows A, 한, あ, ア, or
ă until a key is typed, beside the ☺ and 漢 a search already shows.
2026-08-17 12:38:00 +09:00
c2aec44aa9 engine: the digits type until the candidate list is engaged
Every complete reading shows its candidates, so 1-9 were candidate picks
in the middle of composing: ka then 5 committed 家, and heya then 2
typed a 2 the application never asked for.  They pick only once Space,
Tab, or an arrow has chosen a candidate, as in Mozc; before that they are
ordinary keys.  A search is unchanged: its digits pick from the start.
2026-08-17 12:36:23 +09:00
cf8eca2ff1 docs: fewer claims, the right dependencies 2026-08-17 12:24:34 +09:00
d129f406d5 test: one Xvfb launcher and one daemon for the live tests
The XIM test carried its own Xvfb and daemon spawner, its own child
struct, log files and process-group teardown, and the GTK test a second
Xvfb launcher, because live.c's startdaemon always unset DISPLAY.  Live
now carries the display its own startxvfb reports, startdaemon passes it
on, and both tests use the shared pair: 300 lines fewer, one place that
knows how a child is started, watched, and stopped.
2026-08-17 12:24:12 +09:00
e54783dc92 test: Telex goes in the language table like every other map
The unit suite built a Lang of its own for Telex and loaded telex.map
into it, so the tests that drive the real table had to swap the map in
and out around themselves.  testmapinit loads it where it belongs.
2026-08-17 12:20:39 +09:00
3a90826238 gtk, docs: the module goes where GTK says, and the README says less
The install target guessed the module directory from whichever immodule
was already installed when pkg-config had nothing to say — a guess that
finds nothing on a fresh system and needed seven lines of README to
explain.  It uses GTK's own pkg-config variables now, or the
GTK_MODULE_DIR a packager sets.  The README also loses a negation about
a font argument that no longer exists and two paragraphs that repeat the
dependency list.
2026-08-17 12:19:28 +09:00
0ba7dc7f50 build, data: the Greek capitals by name; one way to run python3; bench.sh checks its own binary
Δ Γ Λ Ω Φ Ψ Σ Θ answered only to De, Ga, La, Om, Ph, Ps, Si, Th, while
their small letters answered to delta, gamma and the rest; they answer to
the names too now.  mkemoji and cldr2emoji write UTF-8 whatever the
locale, as the other generators already did; verify-map calls python3 the
one way; the tests' include path drops a directory nothing includes
through; and bench.sh says which binary is missing instead of blaming the
daemon ten seconds later.
2026-08-17 12:17:18 +09:00
ef7fb627c6 compose: one Compose table for the XIM and IBus frontends
An IBus client throws away a dead key its engine did not take — the GTK
module's own comment says so, and Qt does the same — so é and ü were
lost in every IBus application, while XIM composed them with a table of
its own.  That table moves to compose.c, which both frontends now use;
xim.c is the shorter for it.

A finished sequence is text, not a key: the engine is asked to hand back
what it had pending, and the composed text follows it, so the composed
character can no longer land before the syllable typed before it.
2026-08-17 12:16:07 +09:00
a0e83f98c6 engine: a takeover keeps the text it took for the context it took it from
Two applications' focus events cross — ibus-daemon documents the case —
so the first key of the new one can arrive before the old one says it
lost focus.  The engine dropped whatever that context was composing; it
now keeps it and hands it back with the reset or release that follows,
which every frontend already commits.  Normal ordering is unchanged.
2026-08-17 12:07:19 +09:00
34c0c501aa engine: say what key 0 means 2026-08-17 12:04:12 +09:00
0f684ae6d3 gtk: a dead key hands the pending text back first
GtkIMContextSimple composes dead keys, and while it did the daemon still
held the syllable typed before them: the composed é was inserted first
and 하 reappeared after it.  A key the daemon does not take now resets
the composition when Simple begins composing on it — a modifier press,
which is also no key, leaves it alone.
2026-08-17 11:59:42 +09:00
c102c87d55 ibus: a key stands for the FocusIn a client may never send
A context that had not sent FocusIn had every key rejected outright, so
the input method was silently dead in a client that omits or delays it,
and in the documented case of two applications whose focus events cross.
ibus-daemon and fcitx5 both treat a key as focus; strans, which plays the
daemon here, now does too.  A release still does not focus, and the
engine's own owner rule is unchanged.
2026-08-17 11:58:08 +09:00
bfa919f623 ibus: name the address file as libibus does under Wayland
libibus looks for the file under WAYLAND_DISPLAY when a session has one,
DISPLAY only otherwise; strans always used DISPLAY, so in a Wayland
session it wrote <machine-id>-unix-0 while every IBus client looked for
<machine-id>-unix-wayland-0 and found nothing.  The machine id now comes
from D-Bus's copy first, as libibus reads it, and a host with neither
file gets libibus's own "machine-id" rather than a daemon that dies.
2026-08-17 11:53:40 +09:00
5a6f352d72 docs: where clients find the daemon 2026-08-17 10:59:21 +09:00
88ad5f7630 engine: the one capability is a flag
Cclientpreedit was a one-bit mask that every producer set as "want ?
Cclientpreedit : 0" and every consumer masked out again; nothing else
was ever going to join it.  Keyreq carries clientpre, an int that says
whether the client draws the preedit, and the engine and the XIM
context keep it under that name.
2026-08-17 10:57:15 +09:00
33023d7f51 ipc: a modifier keysym is no key
Shift, Control, and their kin were mapped to special keys that the
engine then had a range and a check to ignore, and the GTK module made a
round trip to the daemon for each press.  ipckeysym maps them to key 0,
which was already the "no key" every frontend and the engine skip;
Kmodfirst, Kmodlast, and ismodkey go.
2026-08-17 10:56:03 +09:00
46bb8b1d46 ibus: the InputContext interface as a table
onmsg matched the member name, then handleplain matched four of the
names again after checking their empty signature, and the handlers that
take arguments each re-checked theirs with a message of their own.  One
table of member, signature, and handler does the matching and the
checking once; the handlers read arguments a signature has already
vouched for.
2026-08-17 10:55:09 +09:00
82cb07eba2 docs: the input modes as they now are, in one piece
The paragraphs on candidates, keys, and searches had grown by patches;
they read as three: Japanese, Korean, and the searches.
2026-08-17 02:01:02 +09:00
b979afc3f7 xim: a spot is a baseline; a popup flipped above it clears the line
An XIM spot has no height, so a popup that flips above it at the bottom
of the screen ended on the baseline and covered the line being typed.
The spot now stands for the row above it, one popup row tall, as GTK
and IBus carets carry their line height; below the spot nothing moves.
2026-08-17 01:59:44 +09:00
dd389edc4f x11: xcb_aux_get_screen instead of two hand-rolled screen walks
win.c and xim.c each iterated the setup's roots to find the screen
xcb_connect had chosen; xcb-util, already linked through imdkit, has
xcb_aux_get_screen for that.
2026-08-17 01:58:30 +09:00
268b687ee6 engine: Backspace and Escape share their way back to the reading 2026-08-17 01:55:57 +09:00
e32cea3296 engine: Backspace deletes the last kana shown; Tab converts like Space
Backspace undid a keystroke: な became ん, かんじ became かんj, きゃ
became ky.  Like every Japanese IME it now deletes the last kana as
shown — な goes, かんじ becomes かん, きゃ becomes き — while a romaji
letter that never became kana still goes one at a time.  Tab stepped
through candidates in a search and committed in Japanese; it steps
through them there too, with Space, and Shift+Tab steps back.
2026-08-17 01:55:25 +09:00
a03fb05324 ibus: PostProcessKeyEvent for clients that process keys synchronously
GTK 4's IBus module, and GTK 3's with IBUS_ENABLE_SYNC_MODE=1, waits for
its ProcessKeyEvent reply and cannot take signals meanwhile; since IBus
1.5.29 it then reads the PostProcessKeyEvent property for what the key
produced.  strans answered that read with an error, so every key logged
a warning and a commit made by a key that passed on arrived after the
key: 한 and a comma became ,한.  A context that sets
EffectivePostProcessKeyEvent now has its commits and preedits held
during the key and handed over as the (yv) list IBus defines.
2026-08-17 01:53:51 +09:00
42568ab020 build, data: skk2ktrans keeps the SKK header; the recipe names every generator; an inert config line goes
An SKK dictionary's ";;" header carries its license notice, which
LICENSES/README.md relies on for kanji.dict, and skk2ktrans dropped
every ";;" line: a re-import as map/README describes lost the grant.
The leading comment block is kept now.  README's regenerate-and-verify
recipe omitted mktelex.py although verify-map checks its output; and
the Dockerfile set a shell variable named egrep in plan9's config that
9c never reads, calling egrep by name.
2026-08-17 01:49:45 +09:00
7c9e736996 xim: Compose results of any length, StatusNone styles; ibus: no cursor size check
A Compose sequence whose result is more than one character has no
keysym, so xkb_compose_state_get_one_sym gave nothing and the result was
lost; the UTF-8 the compose state holds is committed instead.  Clients
that ask for a StatusNone style — fcitx5 offers them — failed to create
an input context; the three preedit styles come in both status flavours
now.  IBus SetCursorLocation returned an error for a negative width or
height that nothing reads and no client ever waits for.
2026-08-17 01:49:06 +09:00
0cb395579f xim: one file at the top level, keymaplookup in it
xim/keymap.c held one 37-line function apart from xim.c, with its own
prototype typed by hand in xim.c and nelem spelt out because it avoided
dat.h — all so that a test could link it without imdkit, though the XIM
adapter test #includes xim.c whole anyway.  It lives in xim.c now, and
xim.c, the last file of its directory, sits beside ibus.c.
2026-08-17 01:47:40 +09:00
33848709a5 data(symbol): the symbols by their names too
II, PP, SS, sq, mul, vv, xx, oo, dn, inf, and deg were the only way to
∫ ∏ ∑ √ × ✓ ✗ ● ↓ ∞ °; integral, prod, sum, sqrt, times, check, cross,
circle, down, infinity, and degree find them as well.
2026-08-17 01:45:30 +09:00
47f9801ad8 engine: a search just begun shows itself
Ctrl+E and Ctrl+H gave no sign until a key produced candidates; with
nothing pending the popup even went away.  An empty search now shows
☺ or 漢 in the popup, so the user knows the next keys are a query.
2026-08-17 01:45:27 +09:00