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>
489 lines
12 KiB
C
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, ®listen, 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");
|
|
}
|