diff --git a/Makefile b/Makefile index 2d180cb..d19c239 100644 --- a/Makefile +++ b/Makefile @@ -12,29 +12,34 @@ TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig) TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig) POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-aux xcb-randr) POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-aux xcb-randr) +WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon) +WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon) PROJECT_CPPFLAGS = PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \ - $(XIM_LIBS) $(POPUP_LIBS) + $(XIM_LIBS) $(POPUP_LIBS) $(WL_LIBS) PROG = strans DOCKER_IMAGE = strans-build DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \ -v "$(CURDIR):/src" $(DOCKER_IMAGE) SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \ - srv.c str.c strans.c trie.c vi.c win.c xim.c + srv.c str.c strans.c trie.c vi.c win.c wl.c xim.c OBJS = $(SRCS:.c=.o) # wayland-scanner writes these from the XML in proto/; only the XML is tracked. PROTO = imv2.c imv2.h vkv1.c vkv1.h +PROTOOBJ = imv2.o vkv1.o all: $(PROG) gtk -$(PROG): $(OBJS) - $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS) +$(PROG): $(OBJS) $(PROTOOBJ) + $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS) $(OBJS): dat.h fn.h ipc.h +wl.o: imv2.h vkv1.h compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS) font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS) win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS) +wl.o $(PROTOOBJ): PROJECT_CPPFLAGS += $(WL_CFLAGS) xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS) %.o: %.c @@ -50,7 +55,7 @@ vkv1.c: proto/virtual-keyboard-unstable-v1.xml $(SCANNER) private-code $< $@ clean: - rm -f $(OBJS) $(PROG) $(PROTO) + rm -f $(OBJS) $(PROG) $(PROTO) $(PROTOOBJ) $(MAKE) -C tests clean $(MAKE) -C gtk/ clean $(MAKE) -C bench/ clean diff --git a/fn.h b/fn.h index 300f31f..327761f 100644 --- a/fn.h +++ b/fn.h @@ -43,6 +43,8 @@ void srvinit(void); void srvthread(void*); void ibusthread(void*); void ximthread(void*); +int wlinit(void); +void wlthread(void*); int keymeaningful(u32int); void composeinit(void); diff --git a/main.c b/main.c index 2d4a759..a79b8c5 100644 --- a/main.c +++ b/main.c @@ -60,6 +60,8 @@ threadmain(int argc, char **argv) proccreate(drawthread, nil, 16384); proccreate(srvthread, nil, 16384); proccreate(ibusthread, nil, 32768); + if(getenv("WAYLAND_DISPLAY") != nil && wlinit()) + proccreate(wlthread, nil, 32768); display = getenv("DISPLAY"); if(display != nil && display[0] != '\0') proccreate(ximthread, nil, 32768); diff --git a/wl.c b/wl.c new file mode 100644 index 0000000..5e910fb --- /dev/null +++ b/wl.c @@ -0,0 +1,488 @@ +#include "dat.h" +#include "fn.h" + +#include +#include +#include +#include +#include +#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"); +}