The surrounding_text event was taken and thrown away. It carries the
text around the cursor with a byte offset into it, and belongs to the
activation like the content type, so it is pending until done and
starts empty at every activate. delete_surrounding_text counts bytes
where the engine counts runes, and the frontend holds the text those
runes are in, so it measures them itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Korean commits a syllable as the next one begins, so by the time the
한자 key is pressed only the last syllable is still ours: typing 한자 and
then Ctrl+H asked about 자 alone and answered 子, leaving 한子 in the
document -- the mixed Hangul and Hanja that was refused as dictionary
data, made by the interaction instead. To get 漢字 the key had to be
pressed before typing, which no other Korean input method asks for.
A request now carries the client's own text just before the cursor, and
the reading reaches back through it as far as the dictionary still knows
the whole of it: 한 joins 자 and 대한민 joins 국. Nothing but the
dictionary says where to stop, because a reading is syllables, so a key
holding a space or an already converted Hanja leads nowhere and the
reach ends there. A pick answers with the count of runes to take back
first; Escape gives back only what was pending, and a query with no
candidate types only the part the client lacks.
A frontend that sends no surrounding text reaches back by nothing and
behaves exactly as before, which is what XIM will keep doing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hanjaquery matched the reading exactly, so a word was reachable only
once every syllable of it had been typed, and the popup went blank on
the way there. Measured over map/hanja.dict's 187,304 readings:
95,024 proper prefixes of a word answer with nothing today, and 66,731
of those are the keystroke just before the word completes -- 대한민 is
one, so 대한민국 looks absent until the last key lands.
dictprefix walks the entry at a node before its children, so the
reading's own conversions keep their place and the words follow: 34,441
readings gain candidates and none of the 187,304 has its existing order
changed. A jamo reading is untouched, since the two keyspaces do not
meet and ㅁ has no children.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
grabrepeat was an empty body, and under the grab the compositor feeds the
client nothing while a key is down: one press, then only repeat_info,
leaving the repeat to whoever holds the grab. So a key strans ate acted
once however long it was held. Hold Backspace over a syllable and one
jamo goes; hold a jamo key and you get one where every other window gives
four a second. X11 has none of this -- the server auto-repeats and xim.c
dispatches every press -- so the same keyboard behaved differently
depending on which frontend the window went through.
Under headless sway, which advertises repeat_info(25, 600), holding
Backspace 1.5s over 라 with 가가가나다 committed behind it:
before preedit 라 -> ㄹ, and no key at the client at all
after preedit 라 -> ㄹ -> empty, then 9 BackSpace at the client
fcitx5 and kime both keep driving the repeat after the engine stops
wanting the key, and both then must send a release before every press, or
the client sees a key held down and starts a repeat of its own on top of
theirs. That machinery exists only to undo the choice that created it.
strans does not need it: forward() already hands a key the engine did not
take to the client as a key, and the client repeats that correctly -- the
same hold with nothing pending, which takes that untouched path, delivers
24. So the rule is one sentence. strans repeats what the engine takes;
when the engine stops taking it, presskey has already passed the key on,
the deadline is dropped, and the client repeats it from there. 9 against
24 is that handover: driving every tick here would have made them one
number.
It costs one pause of the client's own delay, 600ms, where the preedit
empties. It buys no release-and-press fiction anywhere in the file, no
second repeat engine, and no timestamp arithmetic -- exactly one key is
forwarded per hold, so there is no run of stale timestamps to mend.
nsec() is gettimeofday, which steps; a deadline wants the clock ipc.c
already uses. The owner poll stands down while a key repeats, because the
repeat's own Keypress carries our owner and takes the engine back, so it
would have nothing to find.
Arming needs the engine and cannot be reached from the unit suite; ending
a repeat can, and a repeat outliving its key is the worst this could do.
wl/repeat-ends was checked by breaking it five ways -- a constant rate, a
constant delay, the empty body back again, the key's own release no longer
ending it, and a repeat into a dead context staying armed -- each failing
only its own line.
Phases A-G unchanged at both scales; 90 unit, check-live, check-stress and
valgrind clean. One limit worth writing down: the GTK probe prints the
BackSpace it receives and never deletes on one, on this path or on the
plain pass-through, so the phase proves the keys arrive and says nothing
about the text behind them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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.
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.
ibusinit unwound its server and un-registered its atexit handler on a
failure whose only sequel was ibusthread dying anyway; now each failure
dies with its own message, and the test that existed to walk that
unwind goes with it. Also gone: an empty watch-toggle callback where
libdbus takes nil, Maxconns as a second name for Maxclients, fprintf
and strerror where the rest of the daemon says fprint and %r, USED()
where a parameter can simply be unnamed, a second findcontext() for the
Properties branch, and emitcommit() taking a connection and path apart
from the context that has both. Ownerpoll lives once, in dat.h.
dictlookup took a Lang for two reasons that were not its business: to
know whether to walk below the key (the emoji dictionary) and to drop a
candidate equal to the key except there. No dictionary lists its key
among its candidates — the rule was left from a first version that put
the reading first itself — so it goes, and the walk is dictprefix(),
called by the emoji search alone.
server_test, ibus_test and xim_adapter_test each ran their own copy of
the same alt loop that stands in for imthread, and two of them had
private checkstr/checkenginepreedit variants. test_util.c now provides
Pump (trace, optional hold on a chosen op, stop) in its own proc so a
test may block in socket I/O while the engine runs, and test.h declares
the engine hooks once; every .c includes dat.h and fn.h itself as the
rest of the tree does. server_test's three copies of fixture setup and
teardown became serverbegin/serverend.
ko_test and vi_test replayed keys with private copies of transstr's
loop, and test_util reimplemented the shown preedit; the engine exports
impre(Im*, Str*) and transstr() instead, so a change to composition
rules is one edit.
XIM text always went out through imdkit's COMPOUND_TEXT converter,
which already wraps every UTF-8 string in ESC%G; the UTF8_STRING
negotiation, the per-client encoding list, ximtext.c's fallback and its
wrapper never changed a byte on the wire. The spot location is honoured
for every style now (GTK's XIM module sends it with PreeditCallbacks)
and an unset focus window means the client window, as the spec says,
so those clients get the popup at the caret. readattrs/place kept four
transient fields to pass values between them; ximclose tore down state
right before die(); the OOM passthrough context was a third policy for
one small calloc where emalloc dies like everything else. Both frontends
now poll for engine-owner loss only while a preedit shows instead of
XIM waking on a pipe the engine had to know about; ibus stops waking
five times a second when idle. keymeaningful() is the engine's own
predicate. The standalone xim_test moved into the unit suite, so
xim/Makefile is gone.
The dictionary thread ran in imthread's own proc, so a lookup could
only start once the engine blocked, and it was a trie probe anyway; the
emoji and Hanja searches already called dictlookup directly. The
request/result channels, sequence numbers, staleness checks and the
second draw per Japanese key are gone; dictqjp fills the candidates
in place. Emit.dict and Lang.dictq only ever triggered lookups for
Vietnamese, which has no dictionary. dictlookup(Lang*, key, out, max)
returns the count. The Hanja lookup no longer pre-checks for a single
syllable; a reading either has an entry or it does not.
For Vietnamese the preedit is the map key ('as', 'oong'); only the popup
and the commit path looked it up. Clients with inline preedit therefore
showed 'as' while Enter committed 'á'. impre() now maps once for every
reader, and snapshot() no longer maps a second time.
The hash map served a single exact-match lookup that the trie already
answers, at the price of a second container, a second file loader with
its own drift, and a Str-to-UTF-8 conversion on every chain probe. The
files are small (kanji.dict is 7.5k lines), so the trie holds both.
trieopen validates keys and each space-separated value word against Str
and reports path:line; trielookup takes the Str every caller holds and
treats a nil trie as an unloaded map; trienew/trieput exist for tests.
The overflow guards on growth, Trie.root (always 0) and the per-language
init loop written twice are gone.