ximbegin took a struct ct* only to USED() it and always returned 1, so
its seven `if(!ximbegin(...)) goto cleanup;` call sites tested nothing
and three of the cleanup labels they jumped to were unreachable.
ibusbegin malloc'd one byte twice so that its two fake DBusConnections
would differ by address, then CT_CHECKed the mallocs; two bytes in the
fixture are two addresses, and nothing has to be freed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sinit sets s->n = 0 as its first statement and no failure path restores
it, so stail's sclear after a failed sinit could never change anything;
say so in sinit's comment instead, where the contract belongs. bench
compared a uintmax_t against UINT64_MAX and a size_t against UINT64_MAX,
both constant on any host this builds for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
backbytes copied the runes into a Str and encoded them into a 257-byte
stack buffer only to throw the bytes away and keep the length. runelen
answers over the same runes with neither.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shift on a romaji letter marks where the okurigana begins, and Backspace
shortened the reading through im.l->back without clearing the mark.
dictqjp only applies a mark that still falls inside the reading, so the
stale index lay dormant while the reading was short and fired again the
moment it grew back past it: kaKu, Backspace, then nzi offered
噛んじ 兼んじ 漢字 幹事 感じ, where kanzi typed plainly offers 漢字 first.
A Backspace edits the reading, so the split marked in it is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
searchkey popped search.raw and then search.seed but never touched
search.back, so once the typed syllable was gone the query was nothing
but the client's own text: an empty preedit above a full candidate list
drawn from syllables the user had not selected, and Enter rewrote them.
With 상 written and 태 typed, Ctrl+H offered 狀態; one Backspace left
the query 상 and 128 candidates, and Enter replaced the 상 with 上 and
dropped the 태 altogether.
Undo now runs backwards through what happened -- the keys typed since
Ctrl+H, then the reach Ctrl+H made, then the syllable that seeded it --
so the same Backspace narrows 상태 to 태 and offers 太, which is also
the only way there was ever going to be to convert the syllable alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
startsearch let reachback walk the client's text whenever Ctrl+H found
im.sel < 0, without asking whether anything was pending, so a Ctrl+H
pressed to begin a reading took the syllables already written instead.
With the cursor after 입니다 the query became 다, the preedit showed
nothing at all, and Space committed 多 over the 다 the user had written;
typing the reading the key was pressed for gave 다한, so 입니다漢 came
out 입니多恨. A reading is what is pending, and there is none.
The seeded reach is untouched: 한 committed with 자 pending still
converts 한자 as a word and takes the 한 back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
RequireSurroundingText asks the client to send its text; it arrives
through SetSurroundingText as an IBusText and a cursor counted in
runes, and DeleteSurroundingText asks for runes back before the commit
that replaces them. The official libibus client now proves the whole
turn: 한자 typed, the Hanja key, Enter, and 漢字 arrives with the 한
taken away.
A client that has set EffectivePostProcessKeyEvent is never asked for
its text. It reads the key's commits back after ProcessKeyEvent
returns, and a DeleteSurroundingText signal is not one of the things
that reply can carry, so the deletion would land after the text it was
meant to make room for and eat the wrong runes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine can reach a Hanja reading back into the text the client
already holds, but only if the frontend hands that text over and can
take some of it away again. GTK 3 has both: retrieve-surrounding
brings the text around the cursor and delete-surrounding removes runes
before it, and a widget that answers neither leaves the text empty, so
nothing is ever reached into or taken from it.
The wire grows a control frame for the text, sent like the caret only
when it changes, and one byte in every response for the runes to take
back. That byte moves the length fields along, so the version goes to
2: an old daemon and a new module, either way round, fail the handshake
and the module falls through to GtkIMContextSimple rather than misread
a frame.
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>
srvnote and addrnote unlinked on any note at all, and plan9port marks
SIGPIPE Ignore: notify.c:59 lists it, and signotify runs the handler
chain first and only then finds the Ignore flag and returns. So one
broken pipe took the IPC socket and the IBus address file away from a
daemon that went on running -- measured on a private runtime dir, a
single kill -PIPE left the process in state Ssl with both files gone,
so every client that focused a widget afterwards silently had no input
method and only a restart brought it back.
libxcb writes with writev, so the note is a broken X connection away;
today xim.c's die() masks it by taking the daemon down on the same
event, which is exactly why the two must not depend on each other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Space and Tab convert a Japanese reading, and a reading with no
candidate to step through was committed and the key eaten. Katakana
mode converts nothing -- katakouho adds the Katakana form only when it
differs from the reading, and in Katakana mode it never does -- so
every Space between two Katakana words was swallowed and had to be
typed twice: カク<Space> committed カク and left no space, and the
second Space passed through only because nothing was pending any more.
Committing and passing the key on gives the space back. Hiragana is
unchanged wherever the reading has kana, since the Katakana form is
always one candidate there; it changes only for a reading that made no
kana at all, かx<Space>, which now commits and spaces too.
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>
map/symbol.src:12 says "Bare 1-9 choose from a prefix search; keep digit
aliases in matching slots", map/README:55 repeats it, and both
tests/engine_test.c:1001 and tests/mkemoji_test.py were written to it.
None of it happened. dictprefix ends in below(), which returns a node's
own words first and then its children in rune order (dict.c:29-39), and
under `^` the punctuation sorts ahead of the digits:
^ gave ⁽ ⁾ ⁺ ⁻ ⁰ ¹ ² ³ ⁴ ⁵ ...
_ gave ₍ ₎ ₊ ₋ ₀ ₁ ₂ ₃ ₄ ₅ ...
< gave ← ♥ 🫰🫶 ≤ ≠
so the file's own aliases picked the wrong character every time:
before after
^ then 1 ⁽ ¹
^ then 2 ⁾ ²
_ then 1 ₍ ₁
< then 3 🫰 ♥
mkemoji has no bare `^` row to emit, because no source row claims `^` as
an alias -- the prefix exists in the trie only as the parent of `^1`..`^9`
and `^(`..`^n`, and a parent has no words of its own. Giving the nine
superscripts `^` as a second alias, the nine subscripts `_`, and ←≤♥≠ `<`,
makes build() group them in source order and emit three rows:
< ← ≤ ♥ ≠
^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹
_ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉
which is what engine_test.c:1001's fixture has said all along, and the
first thing below() now returns. Nothing is lost: `^0`, `^(`, `^i` and
their kind still answer their own key, `0` is not a selection key so it
still extends the query, and addkouho drops the duplicate when a child
repeats what the parent already offered. Three rows on 18953.
tests/mkemoji_test.py was asking the wrong question. It checked that the
first nine `^` keys in the file are `^1`..`^9` -- true before and after,
and decided nothing, because the file's order is not the trie's. It now
checks the row the engine actually reads, and fails without this change
with a bare KeyError on `^`.
What this does not cover: `+1` and `-1`. They are not slot aliases, they
are words -- plus one, minus one -- and ➕ and ➖ are the right first
answers to `+` and `-`. 👍 is slot 3 of `+` and reachable there.
okuriletter() puts じ in the z row, so dictqokuri can never build a key
ending in j. map/kanji.dict has 17 of them, and 8 have no z twin at all:
before after
shinjiru 死んじる 信じる 死んじる
tojiru (nothing) 綴じる 閉じる
tsuujiru (nothing) 通じる
shoujiru (nothing) 生じる
toujiru 問うじる 投じる 問うじる
koujiru 乞うじる 請うじる 高じる 乞うじる 請うじる
gozonji 御存知 御存知 御存じ
majiru 混じる 先じる 交じる 混じる
信じる, 閉じる, 通じる, 生じる, 投じる, 交じる: the dictionary holds every
one and the engine could reach none. What it offered instead was the
next split down -- しんじる falls back to しn, so 死んじる is what the
popup shows for 信じる, and it is the only candidate.
The letter is not ours to choose; it is the one the SKK dictionary was
keyed by, which is the one the user typed. Real SKK carries both spellings
where both are typed, and here that is 9 of the 17 -- えんj/えんz, かんj/かんz
and their kin, identical values on both. Those 8 z rows go unreachable and
lose nothing, because their j twin says the same word. ま is the one pair
that differs, まj=交 混 against まz=混 先, and both stay reachable: じ takes
the j road and ぜ/ず keep the z one.
mazeru 混ぜる 先ぜる unchanged
kanarazu 必ず ... unchanged
mizu 水 見ず unchanged
dekizu 出来ず unchanged
The z row keeps ざずぜぞ, so nothing that is not じ moves.
What this does not cover, deliberately: ち. kanji.dict has 7 keys ending
in c, from users who typed "chi", and okuriletter puts ち in the t row.
Giving c its own row would have to take ち out of t, and t is where the
larger entry lives -- おt is 落 折 負 追 against おc's 落 alone -- so the c
rows stay unreachable and lose nothing. づ needed no such choice: it is
in the d row, もとd is 基, and もとz is its duplicate.
movekouho() answers every one of Up, Down, PageUp and PageDown the same
way when nothing is chosen yet -- `if(im.sel < 0) im.sel = 0;` -- and
delta is thrown away. For Up and Down that is right: the list is drawn
with no cursor, so the first move takes the first candidate. For
PageDown it is not, because there is no page state to move; pagefirst()
derives the page from im.sel alone, so selecting candidate 1 leaves the
page exactly where it was.
dictqjp() leaves sel at -1 on every keystroke, so a Japanese list is
always untouched when it first appears. Type かく, look at its thirty
candidates, and press PageDown for the next nine:
before after
かく, PageDown sel 0, page 0 sel 9, page 9
かく, PageDown twice sel 9, page 9 sel 18, page 18
かく, PageUp sel 0, page 0 unchanged
かく, Down sel 0, page 0 unchanged
Nothing on screen answers the first press but the highlight appearing on
row 1, and the page turns only on the second. fcitx5 pages on the first,
because its candidate list carries a page of its own and
toPageable()->next() does not touch the cursor
(ref-fcitx5-hangul/src/engine.cpp:326-336). strans has one number where
fcitx5 has two, which is the right trade for nine rows and 128
candidates -- but then the number has to move by a page when a page key
asks for one.
`delta == Maxdisp` rather than `delta > 0` is deliberate: Down must still
land on candidate 1, and writing it the loose way fails both this table
and engine/japanese-candidates at engine_test.c:1198, where the language
switch takes 漢字 and would take 幹事 instead. PageUp from an untouched
list still clamps to 0; there is no page above the first.
searchkey shares movekouho, and is unaffected: emojiquery and hanjaquery
both end in selectfirst(), so a search list is never untouched while it
has candidates.
Backspace, Enter, Tab, Escape and the arrow and page keys are matched by
keysym alone -- strans.c:852, 875 and 882 name them, and searchkey names
the same set at strans.c:746-786. Every other key above Kspec falls
through to the catch-all at strans.c:898, `ks >= Kspec || chord(mod)`,
and goes to the application. So the special keys strans knows by name
are the ones it takes under a modifier, and the ones it does not know are
the ones it hands over. That is backwards: a named special key under a
modifier is exactly the one the application has a binding for.
In Korean a syllable is pending for nearly all the time anyone is typing,
ko.c holding one and no more, and the guards at strans.c:876 and 883
return 0 only when nothing is pending -- so the key is eaten precisely
when it is wanted. Type 안녕하세요 and reach for Ctrl+Backspace to take
the word back: 요 loses ㅛ, then ㅇ, and the word itself goes on the third
press. Ctrl+Enter in a chat box, Ctrl+Tab in a browser and Ctrl+PageDown
in either are the same key eaten by the same lines.
before after
Korean 가, Ctrl+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Alt+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Super+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Backspace eaten, pre ㄱ unchanged
Korean 가, Shift+Backspace eaten, pre ㄱ unchanged
かく, Ctrl+Enter eaten, commit かく passed, commit かく
かく, Ctrl+Tab eaten, commit かく passed, commit かく
かく Space, Ctrl+PageDown eaten, sel 0 -> 9 passed, commit 確
かく Space, PageDown eaten, sel 0 -> 9 unchanged
かく Space, Shift+Tab eaten, sel 0 -> 30 unchanged
Ctrl+E sm, Ctrl+Backspace eaten, query s passed, commit sm
Ctrl+E sm, Backspace eaten, query s unchanged
chord() cannot be reused here. It is `(mod & ~Mshift) != 0 && mod !=
Mctrl` and the exclusion is deliberate, since Ctrl+letter is strans's
whole command set and chord() has to let plain Ctrl through. The rule
this needs is the other one -- any modifier that is not Shift -- and
Shift must stay in: Shift+Tab cycles the candidates backwards, pinned by
engine/candidate-completion and engine/emoji-navigation at
engine_test.c:914 and 1649, and Shift on a Korean key is what makes ㅃ.
One line serves both paths because it sits above the searchkey dispatch,
and it has to sit below the switch at strans.c:829: 한자, 한/영, 変換 and
無変換 arrive as keys above Kspec and are rewritten there into the Ctrl
chords they stand for. Above the switch, 한자 would commit and pass
instead of opening the Hanja list; below it, pressing it with Ctrl held
still opens the list, because the switch sets the modifier itself.
What this does not cover: Space, which is below Kspec, so searchkey:763
still picks a candidate on Ctrl+Space inside a search. transition tests
!(mod & ~Mshift) for its own Space at strans.c:865, so the two disagree
there. Left alone: what Ctrl+Space should mean wants its own argument,
not a widened guard.
takelost's own comment says the context the engine was taken from "gets
that text back when it is next heard from", but it was wired to the
Keyrelease and Keyreset arms only. The Keypress arm opens with
sclear(&lost), so the one way a context is most obviously heard from --
somebody typing into it -- is the way that threw its text away.
Compose in A, type in B before A's focus-out arrives, then go back to A
and type: A's reading is gone rather than handed back. That interleave is
not exotic here, it is the premise takelost exists for. Focus-out can
arrive after the next context's keys, which is why the engine changes
hands with text still pending; and a context whose focus-out is still in
flight can be typed into again just as easily as it can be reset.
engine_test.c: the taken owner types and gets its reading back:
want "か", got ""
The call has to go above the owner switch, because that switch's own
sclear(&lost) is what destroys the text -- putting it after the switch
instead fails the same line. Above it, the order also comes out right:
the recovered reading is appended to the commit first and whatever the key
itself commits follows it.
What this does not cover: there is one lost slot. A third context taking
over between the two still destroys the first one's text for good, and
widening that means an array where a single Str is now -- a new structure
for an interleave that needs three contexts and no focus-out from any of
them. Left as it is.
ibus_client_smoke was pinning the defect rather than merely missing it: it
required exactly three commits on context a, and its handler rejected any
commit text but か, so the recovered reading tripped both. Bumping the
count would have been a constant with nothing behind it, so the take-back
got a stage of its own and the text is asserted -- the official libibus
client now watches the whole thing, and without the fix reports
retook=0
One assertion was written and then deleted. takelost now runs on every
key rather than only on the two lifecycle ops, so re-committing the same
text on every keystroke looked like the hazard worth pinning, and "the
reading comes back once" went in beside the others. Making takelost skip
its lostowner = nil left all 90 passing: the owner switch reassigns
lostowner to the context it just displaced, so lostowner is never the
owner of the next key, and the hazard is unreachable. An assertion that
cannot fail is decoration, so it is not in the tree.
90 unit, check-live, check-stress and valgrind clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mark a mode switch leaves, from 6a6749e, is cleared at the top of
transition(), so the next key takes it away. Nothing takes it away when
there is no next key. reset() clears the pending text, the raw state, the okurigana split
and the search, and leaves modemark alone -- so a release or a lifecycle
reset flushes everything the popup was showing except the one thing that
is still drawn.
Then samedraw() sees a picture identical to the last one and redraw()
sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the
sole caller of both map and unmap). So the popup stays. Press the 한/영
key, type nothing, and alt-tab: 한 sits above every window, override
redirect and typed as a tooltip, until the next keystroke in any
strans-aware field -- which may be minutes. Counted on a private X
server, viewable override-redirect windows:
before after
at rest 0 0
after Ctrl+S 1 1
after letting go 1 0
reset() is where it belongs rather than the release arm, because the mark
is one more thing that is pending: it is drawn only when nothing else is
(snapshot), and it means the switch has not been typed on yet. Clearing
it there is safe for the switch itself only because transition() zeroes it
at the top and setlang() runs after flush() -- reset() is reached through
that flush, before the new mark is set. That ordering is not obvious, and
it is exactly what engine/direct-language-modes already asserts at
engine_test.c:584; breaking setlang so a switch marks nothing fails that
line and the new ones together.
Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally
after the release, so its popup was already coming down. X11 was the
frontend that trusted the picture.
The new case goes in engine/direct-language-modes beside the mark
assertions that were already there, and drives imhandlekey rather than
transition, since the defect is on the owner-release path. Without the
fix:
engine_test.c:619: check failed: draindraw(&dc) > 0
that is, the engine published no picture at all. Its second line needs
its own reason to exist, so it was checked by removing the fix and making
samedraw() answer 0 for everything: the picture then arrives and still
carries the mark,
engine_test.c:620: want 0, got 1 (dc.pre.n)
which is the assertion that would otherwise have been decoration. The two
setup lines are load-bearing too -- with setlang marking nothing they fail
rather than passing vacuously, since an empty picture equal to the last
one is never sent.
90 unit, check-live, check-stress and valgrind clean; the count is
unchanged because the case joined a test that already existed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isprivate() reads the GTK input purpose and nothing else, but the purpose
is not the only way an application marks a password field, and it is not
the older one. GTK 3.24.52, measured:
gtk_entry_set_visibility(entry, FALSE) purpose stays FREE_FORM
gtk_entry_set_input_purpose(PASSWORD) purpose PASSWORD
so an entry hidden the first way looks like ordinary text to the module.
This machine runs one. /usr/libexec/xfce-polkit, the XFCE authentication
dialog, is up right now under GTK_IM_MODULE=strans, and its binary calls
gtk_entry_set_visibility and never gtk_entry_set_input_purpose, so every
password typed into it goes through the engine. Against the real daemon,
in Korean mode:
typed hunter2 stored ㅗㅕ숟ㄱ2
typed correcthorse stored 책ㄱㄷㅊ쇅ㄴㄷ
typed P4ssw0rd stored ㅔ4ㄴㄵ0ㄱㅇ
The field draws bullets, so nothing on screen says why the authentication
failed -- except the pending syllable, which is drawn as itself: three
keys into such an entry the old module leaves 한 on screen where the
field should read ●●●. With this it reads ●●●, and the entry holds gks.
set_visibility sends the input context no signal, so the question cannot
be answered where the purpose is, in init and notify::input-purpose. It
has to be asked at the key, of the client window, which is the entry's
own: gdk_window_get_user_data on it returns the GtkEntry. fcitx5-gtk asks
it the same way in all three of its GTK versions -- gtk3/fcitximcontext
.cpp:1155-1162, under the comment "seems visibility != PASSWORD hint".
It costs one field read and one type check per key, on a path that then
does a socket round trip anyway. It does not cover XIM: that protocol has
no attribute for this, and no fcitx5 frontend answers it either -- only
its GTK and Qt client modules do -- so an X client reached over XIM still
composes in its password field. Nor does it notice a "show password" box
switched off in the middle of a composition: there is no signal for
visibility, so the pending text stays in the daemon until the next focus
change. Noticing that needs a signal connection on a widget the module
does not own, which is a bigger thing than the hole is.
The test is a live one and it earns its line: with the fix removed it says
gtk_live_test: hidden-entry key reached daemon or did not commit
Both guards were checked by removing them. Without GTK_IS_ENTRY the run
takes a Gtk-CRITICAL and fails; without the im->win test it exits 139, on
the stalled-peer context, which never gets a client window. An ishidden
that always answers yes fails earlier still, at the initial protocol
frames. Breaking the test's own helper so it hands back the toplevel's
window instead of the entry's fails too, which is what says the assertion
watches the right window rather than merely counting no events.
That count is the one thing that had to be made deterministic. A new
client window invalidates the caret, and the fake daemon records that
frame on its own thread, so sampling the event count straight after
set_client_window raced it: 2 failures in 8 runs. The block now waits for
that frame and names it, and there were 0 in 20 after.
90 unit, check-live, check-stress and valgrind clean.
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>
Every Korean keyboard's 한자 key answers a lone consonant with the KS X
1001 symbol palette, and has since 한글 워드프로세서: ㅁ for ※ ○ △ ㈜, ㄴ
for the brackets, ㄹ for the units, ㅇ for the circled numbers. strans
sends that key to the same search as a syllable -- Khanja is Ctrl+H at
strans.c:830, and startsearch seeds the query with whatever ko.c left
pending -- but every one of hanja.dict's 187286 readings is a syllable, so
the popup came up with a query in it and nothing to pick:
ㅁ: 0 candidates
ㄴ: 0 candidates
ㄹ: 0 candidates
한: 99 candidates 韓 漢 寒 限 閑 恨 旱 汗 翰 邯 罕 悍 澣 閒 瀚
libhangul ships that palette beside the Hanja table already imported here:
data/hanja/mssymbol.txt, same commit, same author, same BSD-3 terms, same
key:value:comment format -- and keyed by the compatibility jamo ko.c
already holds, U+3141 for ㅁ. So the engine does not change at all; the
same dictlookup on the same trie now finds something:
ㅁ: 75 candidates # & * @ § ※ ☆ ★ ○ ● ◎ ◇ ◆ □ ■ △ ▲ ▽ ▼
ㄴ: 23 candidates " ( ) [ ] { } ‘ ’ “ ” 〔 〕 〈 〉 《 》 「 」
ㄹ: 94 candidates $ % ₩ F ′ ″ ℃ Å ¢ £ ¥ ¤ ℉ ‰ € ㎕ ㎖ ㎗ ℓ
한: 99 candidates 韓 漢 寒 限 閑 恨 旱 汗 翰 邯 罕 悍 澣 閒 瀚
Both scripts widen by one rule -- a syllable reading gives Hanja, a jamo
reading gives a symbol -- and hanja.src regenerates byte for byte as it
was, because upstream's own non-syllable readings are words like ㄱ자집
whose values were never Hanja and still fall out. 985 of mssymbol.txt's
987 rows survive: its ideographic space and its soft hyphen do not, since
a candidate the popup cannot draw is not a candidate, and the row format
separates candidates with a space besides.
The two keyspaces cannot collide -- one is syllables, one is single jamo --
so the 187286 existing rows are unchanged, byte for byte, and 18 rows join
them. mkhanja takes a source list as mkemoji already does, and keeps each
upstream header, which is why the licence text now appears twice.
89 unit, check-live, check-stress and valgrind all clean. The five new
assertions were checked by breaking the change five ways: dropping
mssymbol.src from SOURCES, letting issymbol keep a formatting character,
letting a jamo reading keep Hanja, widening isjamo to the vowels, and
making mkhanja reject jamo readings. Each fails only the tests that exist
for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fcitx5, fcitx5-hangul, fcitx5-gtk, kime and libhangul are cloned into the
work tree as ref-*/ to be read against, so git has to be told to leave them
alone. One pattern rather than five names: whatever gets cloned next is
covered too, and none of it is ever ours to track.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
popupabove() took the stacking order from XQueryTree and then the map
state of each child from a round trip of its own. The daemon raises and
maps between those, in that order and in one flush, so the loop could pair
a stale order -- popup still below the client, where it was created -- with
a fresh IsViewable, and report the raise that had already happened as a
popup stacked below the client. 1 run in 25 under load, which is the worst
kind: often enough to teach you to re-run a red instead of reading it.
Widening the gap to 300ms shows it with no load at all, and shows the fix
is the right one. Same test, same widener, 15 runs each:
before pass=11 fail=4
after pass=15 fail=0
XGrabServer is what makes the two one observation, and the widener is then
harmless because nothing can raise inside it. Unwidened, 20 runs clean.
Nothing about win.c was wrong: the popup does go above, and the test now
only says so when it is looking at a single moment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Another input method taking the seat made wlthread hide its popup and
return, leaving activeowner pointing at our context with its text still
pending, and whatever keys we had passed on still down at the client.
leave() is that cleanup and all of it: it gives the keys back, drops a
half-typed sequence, sends the Keyrelease and takes the popup down.
Nothing visible changes either way -- the compositor drops anything an
unavailable input method commits, and the next key from any other
frontend takes the engine regardless. This is the engine left tidy, in
the one word that already means it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e64e4ea took the picture out of the owner poll and argued a later take
could not show a stale one: "either our own key replaced it, or the
engine suppressed the send because our snapshot is that same picture".
The second half is the hole. redraw() returns before it drains when the
picture has not changed, and under Wayland wl is drawc's only reader, so
every IBus interaction leaves its last picture sitting there -- the mode
mark after Ctrl+T, a hanja list, whatever was up. Press a bare modifier
in a text-input-v3 window while IBus owns the engine: keymeaningful(0) is
false, the engine changes nothing, the send is suppressed, and grabkey's
takedraw puts the other window's candidates on our popup surface.
I could not make it visible. checkowner() runs later in the same loop
iteration, finds we are not the owner and hides it, and both commits
leave in one wl_display_flush, so the compositor renders nothing between
them. What is wrong is the rule: README says an IBus client under
Wayland "shows no candidates, because the popup belongs to the Wayland
frontend", and that is now true by construction rather than by luck.
leave() and flushpending() already take only while engaged, so the guard
costs nothing where it is and covers all three callers. The drain still
happens either way, which is what keeps drawc and lastdraw in step.
Phases A-G under headless sway at both scales; the phase E hanja popup
is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One xkb_compose_state served the whole session, so a dead key left half
typed in one window was completed by the first key in the next. Two IBus
contexts through the real processkey(), before this:
want length 0, got length 2; byte 0: want <end>, got 0xc3 (text)
want 0, got 1 (req.op)
Context A pressed dead_acute; context B pressed e, got é, and went down
the composed-text branch instead of sending a key. Only COMPOSING is
sticky -- xkbcommon starts over by itself after COMPOSED and CANCELLED,
which the table in compose_test already pins -- and nothing here ever
called xkb_compose_state_reset.
That static was also fed from three procs, two of them live at once, with
no lock, which xkbcommon forbids. Reaching it needs two focused windows,
so it cannot be made to fail on demand and has no test: it goes because
the sharing goes, not because anything guards it.
So a state per frontend, each starting over when the key comes from
another context. All are made in composeinit(), on threadmain, before
proccreate, because xkb_compose_state_new refs the table and that ref is
a plain increment --
b160: mov (%rdi),%edx b16a: add $0x1,%edx b170: mov %edx,(%rdi)
-- so making a state from a shared table on two procs would race in place
of the feed. Made before the procs exist they can still share the one
table. An owner address that gets reused says so with composedrop: ibus
hands out a contexts[] slot again, xim can malloc an Ic at a freed one,
and wl's single context, which serves every client in turn, drops at
every activate and deactivate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It took the engine's picture and drew it, then took the popup straight back
down: a buffer allocated, filled and attached for nothing, and for one
frame it was another context's candidates at our client's cursor. The
drain it was doing is not needed either. redraw() empties drawc before
every send, so the channel holds at most one picture and that picture is
always lastdraw -- what the engine wants shown now, whoever owns it. A
later take therefore cannot show something stale: either our own key
replaced it, or the engine suppressed the send because our snapshot is
that same picture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mainline dwl has no input method: dwl.c creates a virtual keyboard manager
and never a wlr_input_method_manager_v2, so IME there is a third-party
patch and strans falls back to XIM through Xwayland -- not what the line
promised. Being a wlroots or smithay compositor is the wrong criterion,
which is the general lesson: the compositor has to wire the protocol up.
river and labwc do, and were checked; Wayfire, niri and COSMIC never were.
So the opening says the criterion instead of a list to maintain, and the
Run section already says how the choice is made. The README says it once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
40fd4ab was half right and the sway run said which half. The compositor
does relay the commit when a client merely disabled its text input while
holding the focus -- that much was read correctly out of sway -- but the
client has stopped listening by then. On the wire, in that order:
-> zwp_text_input_v3.disable()
-> zwp_text_input_v3.commit()
<- zwp_text_input_v3.preedit_string("")
<- zwp_text_input_v3.commit_string("가")
<- zwp_text_input_v3.done()
and the entry unchanged. GTK3 drops global->current at its own
focus-out, and any client that follows text-input-v3 does the same, since
events between disable and the next enable are to be ignored. So the
composition is unrecoverable there, by no fault of ours, and the commit
was a request nobody could take.
The code goes back to what it was; the comment does not. The old one
said the compositor drops the commit, which is true only when the focus
moved, and that half-truth is what invited the change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It feeds the Compose table from the grab's keysyms like the other two; the
sentence was written when there were only two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>