Files
strans/wl.c
Hojun-Cho 4ac0625c6b wl: the input-method frontend
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>
2026-08-17 19:17:27 +09:00

489 lines
12 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 */
};
static struct wl_display *display;
static struct wl_seat *seat;
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 void grabkeyboard(void);
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
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 engine's
* pending text goes nowhere, because the compositor sends the text input
* its leave before it sends us the deactivate, and a commit with nothing
* focused to take it is dropped whatever serial it carries.
*/
static void
leave(void)
{
Keyres res;
releasekeys();
if(engaged){
sendrequest(Keyrelease, 0, 0, &res);
engaged = 0;
}
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, "");
}
static void
imactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 1;
activated = 1;
}
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);
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, 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 is stale. */
static void
checkowner(void)
{
Keyres res;
if(!preshown)
return;
sendrequest(Keycap, 0, 0, &res);
if(res.eaten)
return;
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)
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 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;
}
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);
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 ? 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();
wl_display_flush(display);
return;
}
checkowner();
}
if(wl_display_get_error(display) != 0)
die("wl: compositor disconnected");
die("wl: frontend stopped");
}