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:
@@ -127,3 +127,4 @@ void xim_adapter_callback_owner_loss(struct ct*);
|
|||||||
void xim_adapter_commit_encoding(struct ct*);
|
void xim_adapter_commit_encoding(struct ct*);
|
||||||
void wl_modifier_mask(struct ct*);
|
void wl_modifier_mask(struct ct*);
|
||||||
void wl_forwarded_keys(struct ct*);
|
void wl_forwarded_keys(struct ct*);
|
||||||
|
void wl_repeat_ends(struct ct*);
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ static const struct ct_test tests[] = {
|
|||||||
{ "xim/commit-encoding", xim_adapter_commit_encoding },
|
{ "xim/commit-encoding", xim_adapter_commit_encoding },
|
||||||
{ "wl/modifier-mask", wl_modifier_mask },
|
{ "wl/modifier-mask", wl_modifier_mask },
|
||||||
{ "wl/forwarded-keys", wl_forwarded_keys },
|
{ "wl/forwarded-keys", wl_forwarded_keys },
|
||||||
|
{ "wl/repeat-ends", wl_repeat_ends },
|
||||||
};
|
};
|
||||||
#else
|
#else
|
||||||
static const struct ct_test tests[] = {
|
static const struct ct_test tests[] = {
|
||||||
|
|||||||
@@ -102,3 +102,40 @@ wl_forwarded_keys(struct ct *t)
|
|||||||
CT_EQ_INT(t, 0, nkeysent);
|
CT_EQ_INT(t, 0, nkeysent);
|
||||||
CT_CHECK(t, !issent(30));
|
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);
|
||||||
|
}
|
||||||
|
|||||||
106
wl.c
106
wl.c
@@ -4,6 +4,7 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
|
#include <time.h>
|
||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#include <xkbcommon/xkbcommon.h>
|
#include <xkbcommon/xkbcommon.h>
|
||||||
#include "imv2.h"
|
#include "imv2.h"
|
||||||
@@ -66,6 +67,10 @@ static int activated; /* an activate arrived since the last done */
|
|||||||
static u32int purpose, pendingpurpose;
|
static u32int purpose, pendingpurpose;
|
||||||
static u32int sent[Maxcode/32]; /* keycodes passed on and still down */
|
static u32int sent[Maxcode/32]; /* keycodes passed on and still down */
|
||||||
static u32int lasttime; /* the compositor's clock, for a release of our own */
|
static u32int lasttime; /* the compositor's clock, for a release of our own */
|
||||||
|
static int reprate; /* repeats a second the seat asks for; 0 is none */
|
||||||
|
static int repdelay; /* ms a key is held before the first repeat */
|
||||||
|
static u32int repcode; /* the key repeating */
|
||||||
|
static vlong repdue; /* when its next repeat falls due; 0 if none */
|
||||||
static int engaged; /* the engine owes this context a release */
|
static int engaged; /* the engine owes this context a release */
|
||||||
static int preshown; /* our preedit is on the client's screen */
|
static int preshown; /* our preedit is on the client's screen */
|
||||||
static int haskeymap;
|
static int haskeymap;
|
||||||
@@ -84,6 +89,16 @@ wllog(char *msg)
|
|||||||
fprint(2, "strans: wl: %s\n", msg);
|
fprint(2, "strans: wl: %s\n", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A repeat deadline needs a clock that cannot step; nsec() is not one. */
|
||||||
|
static vlong
|
||||||
|
nowms(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (vlong)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
hidden(void)
|
hidden(void)
|
||||||
{
|
{
|
||||||
@@ -356,6 +371,7 @@ leave(void)
|
|||||||
{
|
{
|
||||||
Keyres res;
|
Keyres res;
|
||||||
|
|
||||||
|
repdue = 0;
|
||||||
releasekeys();
|
releasekeys();
|
||||||
composedrop(Composewl, &context);
|
composedrop(Composewl, &context);
|
||||||
if(engaged){
|
if(engaged){
|
||||||
@@ -489,27 +505,21 @@ grabkeymap(void*, struct zwp_input_method_keyboard_grab_v2*, u32int format,
|
|||||||
haskeymap = 1;
|
haskeymap = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
/*
|
||||||
grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
* A press, from the keyboard or from the repeat deadline. Returns whether
|
||||||
u32int time, u32int code, u32int state)
|
* the engine took it: what it did not take has gone on to the client as a
|
||||||
|
* key, and a key at the client is the client's own to repeat.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
presskey(u32int code)
|
||||||
{
|
{
|
||||||
Keyres res;
|
Keyres res;
|
||||||
char text[Maxutf];
|
char text[Maxutf];
|
||||||
u32int key, sym;
|
u32int key, sym;
|
||||||
|
|
||||||
lasttime = time;
|
|
||||||
if(state != WL_KEYBOARD_KEY_STATE_PRESSED){
|
|
||||||
if(issent(code))
|
|
||||||
forward(code, state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!active || hidden() || kstate == nil){
|
|
||||||
forward(code, state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sym = xkb_state_key_get_one_sym(kstate, code + 8);
|
sym = xkb_state_key_get_one_sym(kstate, code + 8);
|
||||||
if(composekey(Composewl, &context, sym, text, sizeof text))
|
if(composekey(Composewl, &context, sym, text, sizeof text))
|
||||||
return; /* a sequence in the making */
|
return 0; /* a sequence in the making */
|
||||||
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
||||||
if(keymeaningful(key))
|
if(keymeaningful(key))
|
||||||
engaged = 1;
|
engaged = 1;
|
||||||
@@ -521,7 +531,33 @@ grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
|||||||
answer(&res, text);
|
answer(&res, text);
|
||||||
takedraw();
|
takedraw();
|
||||||
if(text[0] == '\0' && !res.eaten)
|
if(text[0] == '\0' && !res.eaten)
|
||||||
|
forward(code, WL_KEYBOARD_KEY_STATE_PRESSED);
|
||||||
|
/* A finished sequence is one act; only a key is held down. */
|
||||||
|
return text[0] == '\0' && res.eaten;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
||||||
|
u32int time, u32int code, u32int state)
|
||||||
|
{
|
||||||
|
lasttime = time;
|
||||||
|
if(state != WL_KEYBOARD_KEY_STATE_PRESSED){
|
||||||
|
if(code == repcode)
|
||||||
|
repdue = 0;
|
||||||
|
if(issent(code))
|
||||||
forward(code, state);
|
forward(code, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repdue = 0; /* one key repeats at a time, and this is the newest */
|
||||||
|
if(!active || hidden() || kstate == nil){
|
||||||
|
forward(code, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!presskey(code) || reprate <= 0 ||
|
||||||
|
!xkb_keymap_key_repeats(xkb_state_get_keymap(kstate), code + 8))
|
||||||
|
return;
|
||||||
|
repcode = code;
|
||||||
|
repdue = nowms() + repdelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -542,8 +578,13 @@ grabmodifiers(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
grabrepeat(void*, struct zwp_input_method_keyboard_grab_v2*, int, int)
|
grabrepeat(void*, struct zwp_input_method_keyboard_grab_v2*, int rate,
|
||||||
|
int delay)
|
||||||
{
|
{
|
||||||
|
reprate = rate;
|
||||||
|
repdelay = delay;
|
||||||
|
if(rate <= 0)
|
||||||
|
repdue = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct zwp_input_method_keyboard_grab_v2_listener grablisten = {
|
static const struct zwp_input_method_keyboard_grab_v2_listener grablisten = {
|
||||||
@@ -614,6 +655,26 @@ checkowner(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One repeat, once the key has been held long enough. Under the grab the
|
||||||
|
* compositor feeds the client nothing while the key is down, so what it
|
||||||
|
* would have repeated there is ours to make -- but only while the engine
|
||||||
|
* wants the key. Backspace stops being ours once the preedit is empty,
|
||||||
|
* and by then presskey has passed it on, so the key is held down at the
|
||||||
|
* client and the client repeats it from there.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
repeat(void)
|
||||||
|
{
|
||||||
|
if(nowms() < repdue)
|
||||||
|
return;
|
||||||
|
if(!active || hidden() || kstate == nil || !presskey(repcode)){
|
||||||
|
repdue = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repdue = nowms() + 1000/reprate;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Whether this session has the protocol, not whether it is Wayland:
|
* Whether this session has the protocol, not whether it is Wayland:
|
||||||
* GNOME and KWin set WAYLAND_DISPLAY and have no manager, and XIM is the
|
* GNOME and KWin set WAYLAND_DISPLAY and have no manager, and XIM is the
|
||||||
@@ -648,7 +709,7 @@ void
|
|||||||
wlthread(void*)
|
wlthread(void*)
|
||||||
{
|
{
|
||||||
struct pollfd pfd;
|
struct pollfd pfd;
|
||||||
int n;
|
int ms, n;
|
||||||
|
|
||||||
threadsetname("wl");
|
threadsetname("wl");
|
||||||
replyc = chancreate(sizeof(Keyres), 0);
|
replyc = chancreate(sizeof(Keyres), 0);
|
||||||
@@ -670,7 +731,13 @@ wlthread(void*)
|
|||||||
if(wl_display_flush(display) < 0 && errno != EAGAIN)
|
if(wl_display_flush(display) < 0 && errno != EAGAIN)
|
||||||
break;
|
break;
|
||||||
pfd.revents = 0;
|
pfd.revents = 0;
|
||||||
n = poll(&pfd, 1, preshown || shown ? Ownerpoll : -1);
|
if(repdue != 0){
|
||||||
|
ms = repdue - nowms();
|
||||||
|
if(ms < 0)
|
||||||
|
ms = 0;
|
||||||
|
}else
|
||||||
|
ms = preshown || shown ? Ownerpoll : -1;
|
||||||
|
n = poll(&pfd, 1, ms);
|
||||||
if(n < 0 && errno != EINTR)
|
if(n < 0 && errno != EINTR)
|
||||||
break;
|
break;
|
||||||
if(pfd.revents & POLLIN){
|
if(pfd.revents & POLLIN){
|
||||||
@@ -688,6 +755,11 @@ wlthread(void*)
|
|||||||
textclose();
|
textclose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* A repeating key's own press keeps the engine ours, so
|
||||||
|
* while one repeats the owner poll has nothing to find. */
|
||||||
|
if(repdue != 0)
|
||||||
|
repeat();
|
||||||
|
else
|
||||||
checkowner();
|
checkowner();
|
||||||
}
|
}
|
||||||
if(wl_display_get_error(display) != 0)
|
if(wl_display_get_error(display) != 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user