wl: a key the engine takes is ours to repeat

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>
This commit is contained in:
2026-08-18 02:26:36 +09:00
parent 62ddc2177b
commit dadda19b3a
4 changed files with 129 additions and 18 deletions

View File

@@ -127,3 +127,4 @@ void xim_adapter_callback_owner_loss(struct ct*);
void xim_adapter_commit_encoding(struct ct*);
void wl_modifier_mask(struct ct*);
void wl_forwarded_keys(struct ct*);
void wl_repeat_ends(struct ct*);

View File

@@ -147,6 +147,7 @@ static const struct ct_test tests[] = {
{ "xim/commit-encoding", xim_adapter_commit_encoding },
{ "wl/modifier-mask", wl_modifier_mask },
{ "wl/forwarded-keys", wl_forwarded_keys },
{ "wl/repeat-ends", wl_repeat_ends },
};
#else
static const struct ct_test tests[] = {

View File

@@ -102,3 +102,40 @@ wl_forwarded_keys(struct ct *t)
CT_EQ_INT(t, 0, nkeysent);
CT_CHECK(t, !issent(30));
}
/*
* Arming a repeat needs the engine, which is not here; ending one does
* not, and a repeat that outlives its key is the worst this can do.
*/
void
wl_repeat_ends(struct ct *t)
{
haskeymap = 1;
memset(sent, 0, sizeof sent);
/* Not a rate or a delay any seat would pick: the body was empty
* once, and a stored constant would pass for the real thing. */
grabrepeat(nil, nil, 33, 410);
CT_EQ_INT(t, 33, reprate);
CT_EQ_INT(t, 410, repdelay);
/* The key's own release ends it; another key's release does not. */
repcode = 14;
repdue = 1;
grabkey(nil, nil, 0, 0, 15, WL_KEYBOARD_KEY_STATE_RELEASED);
CT_CHECK(t, repdue != 0);
grabkey(nil, nil, 0, 0, 14, WL_KEYBOARD_KEY_STATE_RELEASED);
CT_EQ_INT(t, 0, repdue);
/* So does a seat that has stopped repeating anything. */
repdue = 1;
grabrepeat(nil, nil, 0, 410);
CT_EQ_INT(t, 0, repdue);
/* A due repeat with no text input to serve is dropped, not sent. */
grabrepeat(nil, nil, 33, 410);
active = 0;
repdue = 1;
repeat();
CT_EQ_INT(t, 0, repdue);
}