Files
strans/wl.c
Hojun-Cho 5d9a2a614e wl: the text before the cursor, and the runes to take back
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>
2026-08-18 14:31:44 +09:00

801 lines
20 KiB
C

#include "dat.h"
#include "fn.h"
#include <errno.h>
#include <poll.h>
#include <sys/mman.h>
#include <time.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "imv2.h"
#include "vkv1.h"
enum
{
Maxcode = 0x300, /* KEY_MAX+1: what evdev can send */
};
/*
* What popuplayout may use. The compositor puts the popup at the text
* cursor and constrains it there, so this need only be wide enough for
* the layout's own width and tall enough for every row it can show.
*/
#define Popupw PopupBasew
#define Popuph ((Maxdisp + 2)*Fontsz + 2*PopupPad + PopupSep)
/*
* One of two shm buffers, busy from attach until the compositor releases
* it. win.c's single grow-only image would be redrawn while the
* compositor was still reading it.
*/
typedef struct Buf Buf;
struct Buf
{
struct wl_buffer *b;
u32int *img;
int size;
int w;
int h;
int busy;
};
static struct wl_display *display;
static struct wl_seat *seat;
static struct wl_compositor *comp;
static struct wl_shm *shm;
static struct wl_surface *surface;
static struct zwp_input_popup_surface_v2 *popsurf;
static struct zwp_input_method_manager_v2 *immgr;
static struct zwp_virtual_keyboard_manager_v1 *vkmgr;
static struct zwp_input_method_v2 *im;
static struct zwp_virtual_keyboard_v1 *vk;
static struct zwp_input_method_keyboard_grab_v2 *grab;
static struct xkb_context *xkbctx;
static struct xkb_state *kstate;
static Channel *replyc;
/*
* The protocol gives one input context per seat, so the engine knows us
* by one address; nil would mean no owner at all.
*/
static int context;
static u32int serial; /* done events counted; the serial commit takes */
static int active; /* a text input is being served */
static int pendingactive;
static int activated; /* an activate arrived since the last done */
static u32int purpose, pendingpurpose;
static Str surround, pendingsurround; /* the client's text before the cursor */
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 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 preshown; /* our preedit is on the client's screen */
static int haskeymap;
static int gone; /* unavailable: the seat is another input method's */
static Buf bufs[2];
static int shown; /* a buffer is attached and the popup is up */
static Drawcmd held; /* a picture that came while both buffers were busy */
static int helddraw;
static void grabkeyboard(void);
static void popup(Drawcmd*);
static void
wllog(char *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
hidden(void)
{
return purpose == Purposepassword || purpose == Purposepin;
}
static void
sendrequest(int op, u32int ks, u32int mod, Keyres *res)
{
Keyreq kr;
memset(&kr, 0, sizeof kr);
kr.owner = &context;
kr.clientpre = 1; /* set_preedit_string draws it; the popup does not */
kr.op = op;
kr.ks = ks;
kr.mod = mod;
kr.surround = surround;
kr.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, res);
}
/* The bytes the last n runes of the client's text take. */
static int
backbytes(int n)
{
char utf[Maxutf];
Str s;
int i;
sclear(&s);
for(i = max(surround.n - n, 0); i < surround.n; i++)
sputr(&s, surround.r[i]);
return stoutf(&s, utf, sizeof utf);
}
/* The engine's modifier bits, read from the grab's own keyboard state. */
static u32int
modmask(void)
{
static char *name[] = {
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CTRL,
XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO,
};
static u32int bit[] = {Mshift, Mctrl, Malt, Msuper};
u32int m;
int i;
m = 0;
for(i = 0; i < nelem(name); i++)
if(xkb_state_mod_name_is_active(kstate, name[i],
XKB_STATE_MODS_EFFECTIVE) > 0)
m |= bit[i];
return m;
}
static int
issent(u32int code)
{
return code < Maxcode && (sent[code/32] & 1<<(code%32)) != 0;
}
/*
* A key the engine did not take goes to the client as a key, not as
* text: eaten = 0 means it is not ours, and committing it would lie to a
* terminal and to anything with a keybinding.
*/
static void
forward(u32int code, u32int state)
{
if(!haskeymap || code >= Maxcode)
return;
zwp_virtual_keyboard_v1_key(vk, lasttime, code, state);
if(state == WL_KEYBOARD_KEY_STATE_PRESSED)
sent[code/32] |= 1<<(code%32);
else
sent[code/32] &= ~(1<<(code%32));
}
/* Whatever we passed on is still down; the client must not keep it. */
static void
releasekeys(void)
{
u32int code;
for(code = 0; code < Maxcode; code++)
if(issent(code))
forward(code, WL_KEYBOARD_KEY_STATE_RELEASED);
}
/*
* The engine's text, then the composed text, in one commit_string: the
* compositor keeps only the last one sent before a commit. A commit is
* an event and goes only when there is something, while a preedit is
* state and goes every time, empty to take the last one down.
*/
static void
answer(Keyres *res, char *tail)
{
char utf[2*Maxutf];
int n;
/* The compositor takes the text back before it inserts ours. */
if(res->del > 0)
zwp_input_method_v2_delete_surrounding_text(im,
backbytes(res->del), 0);
n = stoutf(&res->commit, utf, sizeof utf);
if(tail[0] != '\0')
n += snprint(utf+n, sizeof utf - n, "%s", tail);
if(n > 0)
zwp_input_method_v2_commit_string(im, utf);
n = stoutf(&res->preedit, utf, sizeof utf);
if(n > 0 || preshown) /* an empty one only to take the last down */
zwp_input_method_v2_set_preedit_string(im, utf, n, n);
zwp_input_method_v2_commit(im, serial);
preshown = n > 0;
}
static void
bufrelease(void *p, struct wl_buffer*)
{
((Buf*)p)->busy = 0;
if(helddraw){
helddraw = 0;
popup(&held);
}
}
static const struct wl_buffer_listener buflisten = {bufrelease};
static void
bufclear(Buf *p)
{
if(p->b != nil)
wl_buffer_destroy(p->b);
if(p->img != nil)
munmap(p->img, p->size);
memset(p, 0, sizeof *p);
}
/* An anonymous file of pixels, shared with the compositor. */
static int
shmfd(int size)
{
char path[] = "/dev/shm/strans-XXXXXX";
int fd;
fd = mkstemp(path);
if(fd < 0)
return -1;
unlink(path);
if(ftruncate(fd, size) < 0){
close(fd);
return -1;
}
return fd;
}
/*
* A free buffer of the wanted size. Sizes differ from draw to draw, so
* a free one of the wrong size is remade; both busy means the compositor
* is still reading them and the picture has to wait.
*/
static Buf*
getbuf(int w, int h)
{
struct wl_shm_pool *pool;
Buf *p;
int fd, i, size;
for(i = 0; i < nelem(bufs); i++)
if(!bufs[i].busy && bufs[i].w == w && bufs[i].h == h)
return &bufs[i];
for(i = 0; i < nelem(bufs); i++)
if(!bufs[i].busy)
break;
if(i == nelem(bufs))
return nil;
p = &bufs[i];
bufclear(p);
size = w * h * sizeof p->img[0];
fd = shmfd(size);
if(fd < 0)
return nil;
p->img = mmap(nil, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p->img == MAP_FAILED){
p->img = nil;
close(fd);
return nil;
}
pool = wl_shm_create_pool(shm, fd, size);
p->b = wl_shm_pool_create_buffer(pool, 0, w, h,
w * sizeof p->img[0], WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
close(fd);
wl_buffer_add_listener(p->b, &buflisten, p);
p->size = size;
p->w = w;
p->h = h;
return p;
}
/* Unmapping takes the popup down; the compositor puts it back at the
* next activate with whatever buffer is still attached. */
static void
hidepopup(void)
{
helddraw = 0; /* a picture still waiting for a buffer is stale now */
if(!shown)
return;
wl_surface_attach(surface, nil, 0, 0);
wl_surface_commit(surface);
shown = 0;
}
static void
popup(Drawcmd *dc)
{
Popup p;
Buf *b;
popuplayout(dc, Popupw, Popuph, &p);
if(p.w <= 0 || p.h <= 0){
hidepopup();
return;
}
/* A buffer that is not a multiple of the scale is an invalid_size
* error at attach, which would kill the connection. */
p.w = (p.w + popupscale - 1) / popupscale * popupscale;
p.h = (p.h + popupscale - 1) / popupscale * popupscale;
b = getbuf(p.w, p.h);
if(b == nil){
held = *dc; /* the engine will not send it twice */
helddraw = 1;
return;
}
popupdraw(b->img, dc, &p);
wl_surface_attach(surface, b->b, 0, 0);
wl_surface_set_buffer_scale(surface, popupscale);
wl_surface_damage_buffer(surface, 0, 0, p.w, p.h);
wl_surface_commit(surface);
b->busy = 1;
shown = 1;
}
/*
* drawc is empty or holds exactly what the engine last published, so one
* take is enough. lastdraw follows the engine and not our surface, so
* take it wherever the two can part -- but show it only while the engine
* is ours: what it drew for another frontend is not ours to put up.
*/
static void
takedraw(void)
{
Drawcmd dc;
if(channbrecv(drawc, &dc) > 0 && engaged)
popup(&dc);
}
/* The compositor places the popup; where the text is does not concern us. */
static void
poprect(void*, struct zwp_input_popup_surface_v2*, int, int, int, int)
{
}
static const struct zwp_input_popup_surface_v2_listener poplisten = {poprect};
static void
releasegrab(void)
{
if(grab == nil)
return;
zwp_input_method_keyboard_grab_v2_release(grab);
grab = nil;
}
/*
* The context is going: give the client its keys back. The pending text
* goes nowhere either way -- the compositor drops a commit once the text
* input has its leave, and a client that disabled its own text input has
* stopped listening, so it throws a relayed commit away. XIM and IBus
* hand the text back here because their focus-out is a round trip the
* client still waits on; a deactivate is not.
*/
static void
leave(void)
{
Keyres res;
repdue = 0;
releasekeys();
composedrop(Composewl, &context);
if(engaged){
sendrequest(Keyrelease, 0, 0, &res);
engaged = 0;
takedraw();
}
hidepopup();
preshown = 0;
}
/* A purpose that turned secret with text pending gets the text first. */
static void
flushpending(void)
{
Keyres res;
if(!engaged)
return;
sendrequest(Keyreset, 0, 0, &res);
answer(&res, "");
takedraw();
}
/*
* A purpose belongs to the activation: the compositor sends a content type
* only for a client that asked for one, so a client that asks for none has
* none, whatever the last one wanted.
*/
static void
imactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 1;
activated = 1;
pendingpurpose = 0;
sclear(&pendingsurround);
}
static void
imdeactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 0;
}
/*
* The text around the cursor, of which a reading can use what comes
* before it. A text input that sends none leaves this empty, and then
* nothing is ever reached back into or taken away.
*/
static void
imsurrounding(void*, struct zwp_input_method_v2*, const char *text,
u32int cursor, u32int)
{
u32int n;
n = strlen(text);
stail(&pendingsurround, (char*)text, cursor < n ? cursor : n);
}
static void
imcause(void*, struct zwp_input_method_v2*, u32int)
{
}
static void
imcontent(void*, struct zwp_input_method_v2*, u32int, u32int what)
{
pendingpurpose = what;
}
/*
* activate, deactivate and content_type are pending until done, and the
* count of done events is the serial a commit must carry: wlroots throws
* away a whole commit whose serial does not match, without a word.
*/
static void
imdone(void*, struct zwp_input_method_v2*)
{
int washidden;
serial++;
washidden = hidden();
purpose = pendingpurpose;
surround = pendingsurround;
if(activated || (active && !pendingactive))
leave();
else if(active && !washidden && hidden())
flushpending();
if(activated && pendingactive)
grabkeyboard();
else if(!pendingactive)
releasegrab();
active = pendingactive;
activated = 0;
}
static void
imgone(void*, struct zwp_input_method_v2*)
{
gone = 1;
}
static const struct zwp_input_method_v2_listener imlisten = {
imactivate, imdeactivate, imsurrounding, imcause, imcontent, imdone,
imgone,
};
/*
* The same fd goes to the virtual keyboard, so a key we pass on means
* there what it meant here.
*/
static void
grabkeymap(void*, struct zwp_input_method_keyboard_grab_v2*, u32int format,
int fd, u32int size)
{
struct xkb_keymap *keymap;
struct xkb_state *state;
char *s;
if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1){
close(fd);
return;
}
s = mmap(nil, size, PROT_READ, MAP_PRIVATE, fd, 0);
if(s == MAP_FAILED){
close(fd);
return;
}
keymap = xkb_keymap_new_from_string(xkbctx, s,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(s, size);
state = keymap != nil ? xkb_state_new(keymap) : nil;
xkb_keymap_unref(keymap);
if(state == nil){
wllog("cannot read the keyboard mapping");
close(fd);
return;
}
xkb_state_unref(kstate);
kstate = state;
zwp_virtual_keyboard_v1_keymap(vk, format, fd, size);
close(fd);
haskeymap = 1;
}
/*
* A press, from the keyboard or from the repeat deadline. Returns whether
* 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;
char text[Maxutf];
u32int key, sym;
sym = xkb_state_key_get_one_sym(kstate, code + 8);
if(composekey(Composewl, &context, sym, text, sizeof text))
return 0; /* a sequence in the making */
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key))
engaged = 1;
if(text[0] != '\0')
/* Composed text follows whatever was pending. */
sendrequest(Keyreset, 0, 0, &res);
else
sendrequest(Keypress, key, modmask(), &res);
answer(&res, text);
takedraw();
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);
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;
}
/*
* Our own lookup must see Shift, and so must the client: wlroots derives
* no state from a virtual keyboard's keys, so a modifier forwarded as a
* keycode arrives bare and Ctrl+C reaches the client as c.
*/
static void
grabmodifiers(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
u32int depressed, u32int latched, u32int locked, u32int group)
{
if(kstate != nil)
xkb_state_update_mask(kstate, depressed, latched, locked,
0, 0, group);
if(haskeymap)
zwp_virtual_keyboard_v1_modifiers(vk, depressed, latched,
locked, group);
}
static void
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 = {
grabkeymap, grabkey, grabmodifiers, grabrepeat,
};
/*
* sway hands keys to the grab holder whenever the grab object exists,
* without asking whether the input method is active, so a grab held
* while inactive takes every key in the session. Two activates can also
* arrive without a deactivate between them, and a second grab_keyboard
* then fails and leaves a dead object: release before grabbing again.
*/
static void
grabkeyboard(void)
{
releasegrab();
grab = zwp_input_method_v2_grab_keyboard(im);
zwp_input_method_keyboard_grab_v2_add_listener(grab, &grablisten, nil);
}
static void
regglobal(void*, struct wl_registry *r, u32int name, const char *iface, u32int)
{
if(strcmp(iface, wl_seat_interface.name) == 0){
if(seat == nil) /* the first seat is ours */
seat = wl_registry_bind(r, name, &wl_seat_interface, 1);
}else if(strcmp(iface, wl_compositor_interface.name) == 0)
/* 4 for set_buffer_scale and damage_buffer */
comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if(strcmp(iface, wl_shm_interface.name) == 0)
shm = wl_registry_bind(r, name, &wl_shm_interface, 1);
else if(strcmp(iface, zwp_input_method_manager_v2_interface.name) == 0)
immgr = wl_registry_bind(r, name,
&zwp_input_method_manager_v2_interface, 1);
else if(strcmp(iface, zwp_virtual_keyboard_manager_v1_interface.name) == 0)
vkmgr = wl_registry_bind(r, name,
&zwp_virtual_keyboard_manager_v1_interface, 1);
}
static void
regremove(void*, struct wl_registry*, u32int)
{
}
static const struct wl_registry_listener reglisten = {regglobal, regremove};
/*
* Another frontend may have taken the engine; then our preedit and our
* popup are both stale. Nothing later will say so: the engine's own
* pictures go to whoever took it, and it settles back at blank.
*/
static void
checkowner(void)
{
Keyres res;
if(!preshown && !shown)
return;
sendrequest(Keycap, 0, 0, &res);
if(res.eaten)
return;
hidepopup();
if(preshown){
zwp_input_method_v2_set_preedit_string(im, "", 0, 0);
zwp_input_method_v2_commit(im, serial);
preshown = 0;
}
}
/*
* 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:
* GNOME and KWin set WAYLAND_DISPLAY and have no manager, and XIM is the
* only path left to them.
*/
int
wlinit(void)
{
struct wl_registry *reg;
display = wl_display_connect(nil);
if(display == nil)
return 0;
reg = wl_display_get_registry(display);
wl_registry_add_listener(reg, &reglisten, nil);
wl_display_roundtrip(display);
/* Without the virtual keyboard every key we do not eat is lost. */
if(seat != nil && immgr != nil && vkmgr != nil && comp != nil &&
shm != nil)
return 1;
wl_display_disconnect(display);
display = nil;
return 0;
}
/*
* One process touches libwayland: it reads events, dispatches them and
* answers the engine, in order. dispatch reads only when poll says
* there is something, so it never blocks and the owner check still runs.
*/
void
wlthread(void*)
{
struct pollfd pfd;
int ms, n;
threadsetname("wl");
replyc = chancreate(sizeof(Keyres), 0);
xkbctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(xkbctx == nil){
wllog("cannot make an xkb context");
return;
}
textinit();
im = zwp_input_method_manager_v2_get_input_method(immgr, seat);
zwp_input_method_v2_add_listener(im, &imlisten, nil);
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmgr, seat);
surface = wl_compositor_create_surface(comp);
popsurf = zwp_input_method_v2_get_input_popup_surface(im, surface);
zwp_input_popup_surface_v2_add_listener(popsurf, &poplisten, nil);
pfd.fd = wl_display_get_fd(display);
pfd.events = POLLIN;
for(;;){
if(wl_display_flush(display) < 0 && errno != EAGAIN)
break;
pfd.revents = 0;
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)
break;
if(pfd.revents & POLLIN){
if(wl_display_dispatch(display) < 0)
break;
}else if(wl_display_dispatch_pending(display) < 0)
break;
if(gone){
wllog("another input method already has the seat");
releasegrab();
leave();
wl_display_flush(display);
bufclear(&bufs[0]);
bufclear(&bufs[1]);
textclose();
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();
}
if(wl_display_get_error(display) != 0)
die("wl: compositor disconnected");
die("wl: frontend stopped");
}