Files
strans/wl.c
Hojun-Cho 6d511919e8 wl: a content purpose belongs to its activation
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>
2026-08-17 21:21:24 +09:00

711 lines
17 KiB
C

#include "dat.h"
#include "fn.h"
#include <errno.h>
#include <poll.h>
#include <sys/mman.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "imv2.h"
#include "vkv1.h"
/*
* text-input-v3 content purposes whose keys the engine never sees. IBus
* numbers these alike but does not share them: one enum here is cheaper
* than reaching into dat.h and ibus.c both.
*/
enum
{
Purposepassword = 8,
Purposepin = 9,
Maxcode = 8*32, /* keycodes the forwarded bitmap holds */
};
/*
* 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 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 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);
}
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.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, res);
}
/* 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;
if(kstate == nil)
return m;
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;
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.
*/
static void
takedraw(void)
{
Drawcmd dc;
if(channbrecv(drawc, &dc) > 0)
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 and the pending
* text, as a lost focus does everywhere else. A client that only
* disabled its text input still holds the focus and takes the commit;
* when the focus itself moved, the compositor sent the text input its
* leave first and drops the commit whatever serial it carries.
*/
static void
leave(void)
{
Keyres res;
releasekeys();
if(engaged){
sendrequest(Keyrelease, 0, 0, &res);
answer(&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;
}
static void
imdeactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 0;
}
static void
imsurrounding(void*, struct zwp_input_method_v2*, const char*, u32int, u32int)
{
}
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;
if(activated || (active && !pendingactive))
leave();
else if(active && !washidden && hidden())
flushpending();
if(activated)
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;
}
static void
grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
u32int time, u32int code, u32int state)
{
Keyres res;
char text[Maxutf];
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);
if(composekey(sym, text, sizeof text))
return; /* 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, state);
}
/*
* 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, int)
{
}
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;
takedraw();
hidepopup();
if(preshown){
zwp_input_method_v2_set_preedit_string(im, "", 0, 0);
zwp_input_method_v2_commit(im, serial);
preshown = 0;
}
}
/*
* 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;
char *scale;
int 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;
}
/* drawthread no longer runs, so the scale comes first: setfont
* reads Fontsz. */
scale = getenv("GDK_SCALE");
if(scale != nil && atoi(scale) > 1)
popupscale = min(atoi(scale), 4);
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;
n = poll(&pfd, 1, preshown || shown ? Ownerpoll : -1);
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();
hidepopup();
wl_display_flush(display);
bufclear(&bufs[0]);
bufclear(&bufs[1]);
textclose();
return;
}
checkowner();
}
if(wl_display_get_error(display) != 0)
die("wl: compositor disconnected");
die("wl: frontend stopped");
}