aboutsummaryrefslogtreecommitdiff
path: root/backend-wayland.c
diff options
context:
space:
mode:
authorLena <lena@omega>2026-01-01 00:00:00 +0000
committerLena <lena@omega>2026-01-01 00:00:00 +0000
commit2fa72ee0dc67d973330aec4ba98260e9f57f7821 (patch)
treee6a25c07c8da61d29e4e1f8a72436565e63c5777 /backend-wayland.c
downloadmouseboard-0.1.tar.gz
Enter mouseboardHEADv0.1master
Keyboard-only pointing: a labelled grid over every monitor, type a label to jump to a cell, bisect it by quadrant until the crosshair is exact, then one key clicks. The coarse grid keeps labels short and bisection keeps precision unlimited, so a hotkey and a few keystrokes replace the mouse. The core talks only to the backend interface in platform.h; wlroots Wayland and Win32 backends supply the overlay, keyboard grab and pointer.
Diffstat (limited to 'backend-wayland.c')
-rw-r--r--backend-wayland.c854
1 files changed, 854 insertions, 0 deletions
diff --git a/backend-wayland.c b/backend-wayland.c
new file mode 100644
index 0000000..041c648
--- /dev/null
+++ b/backend-wayland.c
@@ -0,0 +1,854 @@
+/*
+ * backend-wayland.c - wlroots backend.
+ *
+ * Overlay : zwlr_layer_shell_v1, one fullscreen surface per output with
+ * exclusive keyboard interactivity - that is the keyboard grab.
+ * Pointer : zwlr_virtual_pointer_v1 (absolute motion + buttons).
+ * Keyboard : wl_keyboard keymap fed to xkbcommon for keysym translation.
+ * Buffers : double-buffered wl_shm ARGB8888, drawn by the core's renderer.
+ */
+#define _GNU_SOURCE
+#include "platform.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <wayland-client.h>
+#include <xkbcommon/xkbcommon.h>
+#include <xkbcommon/xkbcommon-keysyms.h>
+#include <linux/input-event-codes.h>
+
+#include "xdg-shell-protocol.h"
+#include "xdg-output-unstable-v1-protocol.h"
+#include "wlr-layer-shell-unstable-v1-protocol.h"
+#include "wlr-virtual-pointer-unstable-v1-protocol.h"
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define KQ_SIZE 64
+#define MAX_OUTPUT_SCALE 16
+#define MAX_KEYMAP_SIZE (16U * 1024U * 1024U)
+
+struct buffer {
+ struct wl_buffer *wl;
+ uint32_t *data;
+ size_t size;
+ int busy;
+ struct mb_buffer pub;
+};
+
+struct output {
+ struct wl_state *st;
+ struct wl_output *wl;
+ struct zxdg_output_v1 *xdg;
+ uint32_t name;
+ int32_t x, y; /* global (logical) position */
+ int width, height; /* configured surface size = logical size */
+ int scale; /* integer output scale */
+ int configured;
+
+ struct wl_surface *surface;
+ struct zwlr_layer_surface_v1 *layer;
+ struct buffer bufs[2];
+ int cur;
+};
+
+struct wl_state {
+ struct wl_display *display;
+ struct wl_compositor *compositor;
+ struct wl_shm *shm;
+ struct wl_seat *seat;
+ struct wl_keyboard *keyboard;
+ struct zwlr_layer_shell_v1 *layer_shell;
+ struct zwlr_virtual_pointer_manager_v1 *vp_manager;
+ struct zwlr_virtual_pointer_v1 *vp;
+ struct zxdg_output_manager_v1 *xdg_output_manager;
+
+ struct output outputs[MB_MAX_SCREENS];
+ int n_outputs;
+ int configured_count;
+ /* Bounding box of every output in logical pixels - the virtual
+ * pointer's coordinate space. Fixed once ready: a later topology
+ * change aborts the run. */
+ int bx, by, bw, bh;
+
+ struct xkb_context *xkb_ctx;
+ struct xkb_keymap *xkb_map;
+ struct xkb_state *xkb_state;
+
+ uint32_t keyq[KQ_SIZE];
+ int kq_head, kq_tail;
+
+ int alive;
+ int failed;
+ int ready;
+};
+
+static struct platform g_platform;
+
+static uint32_t now_ms(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
+}
+
+void platform_sleep_ms(int ms)
+{
+ struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
+ nanosleep(&ts, NULL);
+}
+
+void platform_early_init(void)
+{
+ /* Nothing to do: this is an ordinary console program. */
+}
+
+static void kq_push(struct wl_state *st, uint32_t k)
+{
+ int n = (st->kq_tail + 1) % KQ_SIZE;
+ if (n == st->kq_head)
+ return; /* full; drop */
+ st->keyq[st->kq_tail] = k;
+ st->kq_tail = n;
+}
+
+/* ---- shm buffers ----------------------------------------------------- */
+
+static void buffer_release(void *data, struct wl_buffer *wl)
+{
+ (void)wl;
+ ((struct buffer *)data)->busy = 0;
+}
+static const struct wl_buffer_listener buffer_listener = {
+ .release = buffer_release,
+};
+
+static void buffer_destroy(struct buffer *b)
+{
+ if (b->wl)
+ wl_buffer_destroy(b->wl);
+ if (b->data)
+ munmap(b->data, b->size);
+ memset(b, 0, sizeof *b);
+}
+
+static int buffer_init(struct wl_state *st, struct buffer *b, int w, int h,
+ int scale)
+{
+ if (w < 1 || h < 1 || scale < 1 || scale > MAX_OUTPUT_SCALE)
+ return -1;
+ size_t dw = (size_t)w * (size_t)scale;
+ size_t dh = (size_t)h * (size_t)scale;
+ if (dw > INT32_MAX / 4 || dh > INT32_MAX)
+ return -1;
+ size_t stride = dw * 4;
+ if (dh > INT32_MAX / stride)
+ return -1;
+ size_t size = stride * dh;
+ int fd = memfd_create("mouseboard", MFD_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ if (ftruncate(fd, (off_t)size) < 0) {
+ close(fd);
+ return -1;
+ }
+ void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED) {
+ close(fd);
+ return -1;
+ }
+ struct wl_shm_pool *pool = wl_shm_create_pool(st->shm, fd, (int32_t)size);
+ b->wl = wl_shm_pool_create_buffer(pool, 0, (int32_t)dw, (int32_t)dh,
+ (int32_t)stride,
+ WL_SHM_FORMAT_ARGB8888);
+ wl_shm_pool_destroy(pool);
+ close(fd);
+ wl_buffer_add_listener(b->wl, &buffer_listener, b);
+
+ b->data = data;
+ b->size = size;
+ b->busy = 0;
+ b->pub.px = data;
+ b->pub.w = w;
+ b->pub.h = h;
+ b->pub.stride = (int)dw;
+ b->pub.scale = scale;
+ return 0;
+}
+
+/* ---- frame interface ------------------------------------------------- */
+
+static struct mb_buffer *wl_frame_begin(struct platform *p, int s)
+{
+ struct wl_state *st = p->priv;
+ struct output *o = &st->outputs[s];
+
+ if (!st->alive)
+ return NULL;
+ while (o->bufs[0].busy && o->bufs[1].busy) {
+ if (!st->alive || wl_display_dispatch(st->display) < 0) {
+ fprintf(stderr,
+ "mouseboard: display closed while waiting for frame\n");
+ return NULL;
+ }
+ }
+ if (!st->alive)
+ return NULL;
+ int idx = o->bufs[0].busy ? 1 : 0;
+ o->cur = idx;
+ struct buffer *b = &o->bufs[idx];
+
+ if (!b->wl || b->pub.w != o->width || b->pub.h != o->height ||
+ b->pub.scale != o->scale) {
+ buffer_destroy(b);
+ if (buffer_init(st, b, o->width, o->height, o->scale) < 0) {
+ fprintf(stderr, "mouseboard: shm allocation failed\n");
+ return NULL;
+ }
+ }
+ memset(b->data, 0, b->size);
+ return &b->pub;
+}
+
+static int wl_frame_commit(struct platform *p)
+{
+ struct wl_state *st = p->priv;
+ for (int s = 0; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ struct buffer *b = &o->bufs[o->cur];
+ if (!b->wl)
+ continue;
+ wl_surface_set_buffer_scale(o->surface, o->scale);
+ wl_surface_attach(o->surface, b->wl, 0, 0);
+ wl_surface_damage(o->surface, 0, 0, b->pub.w, b->pub.h);
+ wl_surface_commit(o->surface);
+ b->busy = 1;
+ }
+ if (wl_display_flush(st->display) < 0 && errno != EAGAIN) {
+ fprintf(stderr, "mouseboard: display flush failed: %s\n",
+ strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+/* ---- keyboard -------------------------------------------------------- */
+
+static uint32_t map_sym(xkb_keysym_t sym)
+{
+ switch (sym) {
+ case XKB_KEY_Escape:
+ return MB_KEY_ESC;
+ case XKB_KEY_BackSpace:
+ return MB_KEY_BACKSPACE;
+ case XKB_KEY_Return:
+ case XKB_KEY_KP_Enter:
+ return MB_KEY_ENTER;
+ }
+ uint32_t u = xkb_keysym_to_utf32(sym);
+ if (u >= 0x20 && u < 0x7f)
+ return u;
+ return MB_KEY_NONE;
+}
+
+static void kb_keymap(void *data, struct wl_keyboard *k, uint32_t format,
+ int32_t fd, uint32_t size)
+{
+ (void)k;
+ struct wl_state *st = data;
+ /* The keymap can arrive before init has built the xkb context; xkb
+ * dereferences the context, so a NULL one must never reach it. */
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 || !st->xkb_ctx) {
+ close(fd);
+ return;
+ }
+ struct stat sb;
+ if (!size || size > MAX_KEYMAP_SIZE || fstat(fd, &sb) < 0 ||
+ sb.st_size < (off_t)size) {
+ fprintf(stderr, "mouseboard: invalid Wayland keymap size\n");
+ close(fd);
+ return;
+ }
+ char *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ close(fd);
+ if (map == MAP_FAILED)
+ return;
+ struct xkb_keymap *km = xkb_keymap_new_from_buffer(
+ st->xkb_ctx, map, size, XKB_KEYMAP_FORMAT_TEXT_V1,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+ munmap(map, size);
+ if (!km)
+ return;
+ struct xkb_state *ks = xkb_state_new(km);
+ if (!ks) {
+ xkb_keymap_unref(km);
+ return;
+ }
+ if (st->xkb_state)
+ xkb_state_unref(st->xkb_state);
+ if (st->xkb_map)
+ xkb_keymap_unref(st->xkb_map);
+ st->xkb_map = km;
+ st->xkb_state = ks;
+}
+
+static void kb_key(void *data, struct wl_keyboard *k, uint32_t serial,
+ uint32_t time, uint32_t key, uint32_t state)
+{
+ (void)k;
+ (void)serial;
+ (void)time;
+ struct wl_state *st = data;
+ if (state != WL_KEYBOARD_KEY_STATE_PRESSED || !st->xkb_state)
+ return;
+ xkb_keysym_t sym = xkb_state_key_get_one_sym(st->xkb_state, key + 8);
+ uint32_t mb = map_sym(sym);
+ if (mb != MB_KEY_NONE)
+ kq_push(st, mb);
+}
+
+static void kb_modifiers(void *data, struct wl_keyboard *k, uint32_t serial,
+ uint32_t dep, uint32_t lat, uint32_t lock,
+ uint32_t group)
+{
+ (void)k;
+ (void)serial;
+ struct wl_state *st = data;
+ if (st->xkb_state)
+ xkb_state_update_mask(st->xkb_state, dep, lat, lock, 0, 0, group);
+}
+
+static void kb_enter(void *d, struct wl_keyboard *k, uint32_t s,
+ struct wl_surface *su, struct wl_array *a)
+{
+ (void)d; (void)k; (void)s; (void)su; (void)a;
+}
+static void kb_leave(void *d, struct wl_keyboard *k, uint32_t s,
+ struct wl_surface *su)
+{
+ (void)d; (void)k; (void)s; (void)su;
+}
+static void kb_repeat(void *d, struct wl_keyboard *k, int32_t r, int32_t delay)
+{
+ (void)d; (void)k; (void)r; (void)delay;
+}
+
+static const struct wl_keyboard_listener kb_listener = {
+ .keymap = kb_keymap,
+ .enter = kb_enter,
+ .leave = kb_leave,
+ .key = kb_key,
+ .modifiers = kb_modifiers,
+ .repeat_info = kb_repeat,
+};
+
+static uint32_t wl_next_key(struct platform *p)
+{
+ struct wl_state *st = p->priv;
+ while (st->kq_head == st->kq_tail) {
+ if (!st->alive)
+ return MB_KEY_NONE; /* overlay closed */
+ wl_display_flush(st->display);
+ if (wl_display_dispatch(st->display) < 0)
+ return MB_KEY_NONE;
+ }
+ if (!st->alive)
+ return MB_KEY_NONE;
+ uint32_t k = st->keyq[st->kq_head];
+ st->kq_head = (st->kq_head + 1) % KQ_SIZE;
+ return k;
+}
+
+/* ---- pointer --------------------------------------------------------- */
+
+static int wl_pointer_move(struct platform *p, int x, int y)
+{
+ struct wl_state *st = p->priv;
+ if (!st->alive || !st->vp)
+ return -1;
+ int lx = x - st->bx, ly = y - st->by;
+ if (lx < 0)
+ lx = 0;
+ if (ly < 0)
+ ly = 0;
+ if (lx >= st->bw)
+ lx = st->bw - 1;
+ if (ly >= st->bh)
+ ly = st->bh - 1;
+ zwlr_virtual_pointer_v1_motion_absolute(st->vp, now_ms(), (uint32_t)lx,
+ (uint32_t)ly, (uint32_t)st->bw,
+ (uint32_t)st->bh);
+ zwlr_virtual_pointer_v1_frame(st->vp);
+ if (wl_display_flush(st->display) < 0 && errno != EAGAIN) {
+ fprintf(stderr, "mouseboard: display flush failed: %s\n",
+ strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int wl_pointer_button(struct platform *p, enum mb_button b, int press)
+{
+ struct wl_state *st = p->priv;
+ if (!st->alive || !st->vp)
+ return -1;
+ uint32_t btn = b == MB_LEFT ? BTN_LEFT
+ : b == MB_MIDDLE ? BTN_MIDDLE
+ : BTN_RIGHT;
+ zwlr_virtual_pointer_v1_button(st->vp, now_ms(), btn,
+ press ? WL_POINTER_BUTTON_STATE_PRESSED
+ : WL_POINTER_BUTTON_STATE_RELEASED);
+ zwlr_virtual_pointer_v1_frame(st->vp);
+ if (wl_display_flush(st->display) < 0 && errno != EAGAIN) {
+ fprintf(stderr, "mouseboard: display flush failed: %s\n",
+ strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+/* ---- output, seat, layer surface listeners --------------------------- */
+
+static void runtime_changed(struct wl_state *st, const char *what)
+{
+ if (st->alive) {
+ const char *when = st->ready ? "changed while active" :
+ "became unavailable during setup";
+ fprintf(stderr, "mouseboard: %s %s\n", what, when);
+ }
+ st->failed = 1;
+ st->alive = 0;
+}
+
+static void out_geometry(void *data, struct wl_output *o, int32_t x, int32_t y,
+ int32_t pw, int32_t ph, int32_t subpix,
+ const char *make, const char *model, int32_t tr)
+{
+ (void)o; (void)pw; (void)ph; (void)subpix; (void)make; (void)model;
+ (void)tr;
+ struct output *out = data;
+ /* xdg-output carries the logical position; comparing against these
+ * physical coordinates would misfire on scaled layouts. */
+ if (out->st->xdg_output_manager)
+ return;
+ if (out->st->ready && (out->x != x || out->y != y)) {
+ runtime_changed(out->st, "output geometry");
+ return;
+ }
+ out->x = x;
+ out->y = y;
+}
+static void out_mode(void *d, struct wl_output *o, uint32_t f, int32_t w,
+ int32_t h, int32_t r)
+{
+ (void)d; (void)o; (void)f; (void)w; (void)h; (void)r;
+}
+static void out_done(void *d, struct wl_output *o) { (void)d; (void)o; }
+static void out_scale(void *d, struct wl_output *o, int32_t f)
+{
+ (void)o;
+ struct output *out = d;
+ if (f < 1 || f > MAX_OUTPUT_SCALE) {
+ fprintf(stderr, "mouseboard: invalid output scale %d\n", f);
+ out->st->failed = 1;
+ out->st->alive = 0;
+ return;
+ }
+ if (out->st->ready && out->scale != f) {
+ runtime_changed(out->st, "output scale");
+ return;
+ }
+ out->scale = f;
+}
+static void out_name(void *d, struct wl_output *o, const char *n)
+{
+ (void)d; (void)o; (void)n;
+}
+static void out_desc(void *d, struct wl_output *o, const char *n)
+{
+ (void)d; (void)o; (void)n;
+}
+static const struct wl_output_listener output_listener = {
+ .geometry = out_geometry,
+ .mode = out_mode,
+ .done = out_done,
+ .scale = out_scale,
+ .name = out_name,
+ .description = out_desc,
+};
+
+/* xdg-output gives the authoritative logical position, robust under fractional
+ * scaling and transforms; it overrides wl_output.geometry when available. */
+static void xdg_out_logical_position(void *data, struct zxdg_output_v1 *xo,
+ int32_t x, int32_t y)
+{
+ (void)xo;
+ struct output *out = data;
+ if (out->st->ready && (out->x != x || out->y != y)) {
+ runtime_changed(out->st, "output position");
+ return;
+ }
+ out->x = x;
+ out->y = y;
+}
+static void xdg_out_logical_size(void *d, struct zxdg_output_v1 *xo, int32_t w,
+ int32_t h)
+{
+ (void)d; (void)xo; (void)w; (void)h; /* configure size drives the buffer */
+}
+static void xdg_out_done(void *d, struct zxdg_output_v1 *xo)
+{
+ (void)d; (void)xo;
+}
+static void xdg_out_name(void *d, struct zxdg_output_v1 *xo, const char *n)
+{
+ (void)d; (void)xo; (void)n;
+}
+static void xdg_out_desc(void *d, struct zxdg_output_v1 *xo, const char *n)
+{
+ (void)d; (void)xo; (void)n;
+}
+static const struct zxdg_output_v1_listener xdg_output_listener = {
+ .logical_position = xdg_out_logical_position,
+ .logical_size = xdg_out_logical_size,
+ .done = xdg_out_done,
+ .name = xdg_out_name,
+ .description = xdg_out_desc,
+};
+
+static void seat_caps(void *data, struct wl_seat *seat, uint32_t caps)
+{
+ struct wl_state *st = data;
+ if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !st->keyboard) {
+ st->keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(st->keyboard, &kb_listener, st);
+ } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && st->keyboard) {
+ wl_keyboard_release(st->keyboard);
+ st->keyboard = NULL;
+ runtime_changed(st, "keyboard capability");
+ }
+}
+static void seat_name(void *d, struct wl_seat *s, const char *n)
+{
+ (void)d; (void)s; (void)n;
+}
+static const struct wl_seat_listener seat_listener = {
+ .capabilities = seat_caps,
+ .name = seat_name,
+};
+
+static void ls_configure(void *data, struct zwlr_layer_surface_v1 *ls,
+ uint32_t serial, uint32_t w, uint32_t h)
+{
+ struct output *o = data;
+ zwlr_layer_surface_v1_ack_configure(ls, serial);
+ if (!w || !h || w > INT_MAX || h > INT_MAX) {
+ fprintf(stderr, "mouseboard: invalid output size %ux%u\n", w, h);
+ o->st->failed = 1;
+ o->st->alive = 0;
+ return;
+ }
+ if (o->st->ready &&
+ (o->width != (int)w || o->height != (int)h)) {
+ runtime_changed(o->st, "output size");
+ return;
+ }
+ o->width = (int)w;
+ o->height = (int)h;
+ if (!o->configured) {
+ o->configured = 1;
+ o->st->configured_count++;
+ }
+}
+static void ls_closed(void *data, struct zwlr_layer_surface_v1 *ls)
+{
+ (void)ls;
+ ((struct output *)data)->st->alive = 0;
+}
+static const struct zwlr_layer_surface_v1_listener ls_listener = {
+ .configure = ls_configure,
+ .closed = ls_closed,
+};
+
+/* ---- registry -------------------------------------------------------- */
+
+static void reg_global(void *data, struct wl_registry *r, uint32_t name,
+ const char *iface, uint32_t version)
+{
+ struct wl_state *st = data;
+ if (!strcmp(iface, wl_compositor_interface.name)) {
+ st->compositor = wl_registry_bind(r, name,
+ &wl_compositor_interface,
+ MIN(version, 4));
+ } else if (!strcmp(iface, wl_shm_interface.name)) {
+ st->shm = wl_registry_bind(r, name, &wl_shm_interface, 1);
+ } else if (!strcmp(iface, wl_seat_interface.name)) {
+ st->seat = wl_registry_bind(r, name, &wl_seat_interface,
+ MIN(version, 5));
+ wl_seat_add_listener(st->seat, &seat_listener, st);
+ } else if (!strcmp(iface, zwlr_layer_shell_v1_interface.name)) {
+ st->layer_shell =
+ wl_registry_bind(r, name,
+ &zwlr_layer_shell_v1_interface,
+ MIN(version, 4));
+ } else if (!strcmp(iface,
+ zwlr_virtual_pointer_manager_v1_interface.name)) {
+ st->vp_manager = wl_registry_bind(
+ r, name, &zwlr_virtual_pointer_manager_v1_interface,
+ MIN(version, 2));
+ } else if (!strcmp(iface, zxdg_output_manager_v1_interface.name)) {
+ st->xdg_output_manager = wl_registry_bind(
+ r, name, &zxdg_output_manager_v1_interface,
+ MIN(version, 3));
+ } else if (!strcmp(iface, wl_output_interface.name)) {
+ if (st->ready) {
+ runtime_changed(st, "output topology");
+ return;
+ }
+ if (st->n_outputs >= MB_MAX_SCREENS)
+ return;
+ struct output *o = &st->outputs[st->n_outputs++];
+ memset(o, 0, sizeof *o);
+ o->st = st;
+ o->name = name;
+ o->scale = 1;
+ o->wl = wl_registry_bind(r, name, &wl_output_interface,
+ MIN(version, 4));
+ wl_output_add_listener(o->wl, &output_listener, o);
+ }
+}
+static void reg_remove(void *d, struct wl_registry *r, uint32_t name)
+{
+ (void)r;
+ struct wl_state *st = d;
+ for (int s = 0; s < st->n_outputs; s++) {
+ if (st->outputs[s].name == name) {
+ runtime_changed(st, "output topology");
+ return;
+ }
+ }
+}
+static const struct wl_registry_listener reg_listener = {
+ .global = reg_global,
+ .global_remove = reg_remove,
+};
+
+/* ---- teardown -------------------------------------------------------- */
+
+static void wl_teardown(struct platform *p)
+{
+ struct wl_state *st = p->priv;
+ if (!st)
+ return;
+ for (int s = 0; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ buffer_destroy(&o->bufs[0]);
+ buffer_destroy(&o->bufs[1]);
+ if (o->xdg)
+ zxdg_output_v1_destroy(o->xdg);
+ if (o->layer)
+ zwlr_layer_surface_v1_destroy(o->layer);
+ if (o->surface)
+ wl_surface_destroy(o->surface);
+ if (o->wl)
+ wl_output_destroy(o->wl);
+ }
+ if (st->vp)
+ zwlr_virtual_pointer_v1_destroy(st->vp);
+ if (st->xdg_output_manager)
+ zxdg_output_manager_v1_destroy(st->xdg_output_manager);
+ if (st->xkb_state)
+ xkb_state_unref(st->xkb_state);
+ if (st->xkb_map)
+ xkb_keymap_unref(st->xkb_map);
+ if (st->xkb_ctx)
+ xkb_context_unref(st->xkb_ctx);
+ if (st->display) {
+ wl_display_flush(st->display);
+ wl_display_disconnect(st->display);
+ }
+ free(st);
+ p->priv = NULL;
+}
+
+/* ---- init ------------------------------------------------------------ */
+
+struct platform *platform_init(void)
+{
+ struct wl_state *st = calloc(1, sizeof *st);
+ if (!st)
+ return NULL;
+ st->alive = 1;
+
+ st->display = wl_display_connect(NULL);
+ if (!st->display) {
+ fprintf(stderr, "mouseboard: cannot connect to Wayland "
+ "(is WAYLAND_DISPLAY set?)\n");
+ free(st);
+ return NULL;
+ }
+
+ /* Build the xkb context before the first roundtrip: the seat's
+ * wl_keyboard.keymap can be delivered during any roundtrip below (the
+ * xdg-output one in particular), and kb_keymap hands it straight to xkb,
+ * which dereferences the context. Creating it later crashes at launch.
+ *
+ * NO_DEFAULT_INCLUDES: only the fully expanded keymap the compositor
+ * sends is ever compiled (xkb_keymap_new_from_buffer), never one resolved
+ * by name, so no XKB data directory is needed. Without this the static
+ * build fails at startup: its bundled xkbcommon has a default include
+ * path baked to the build sysroot, which does not exist at runtime. */
+ st->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_DEFAULT_INCLUDES);
+ if (!st->xkb_ctx) {
+ fprintf(stderr, "mouseboard: xkb_context_new failed\n");
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+
+ struct wl_registry *reg = wl_display_get_registry(st->display);
+ wl_registry_add_listener(reg, &reg_listener, st);
+ wl_display_roundtrip(st->display); /* globals + output binds */
+ wl_display_roundtrip(st->display); /* output geometry, seat caps */
+
+ const char *missing = NULL;
+ if (!st->compositor)
+ missing = "wl_compositor";
+ else if (!st->shm)
+ missing = "wl_shm";
+ else if (!st->seat)
+ missing = "wl_seat";
+ else if (!st->layer_shell)
+ missing = "zwlr_layer_shell_v1";
+ else if (!st->vp_manager)
+ missing = "zwlr_virtual_pointer_manager_v1";
+ else if (st->n_outputs == 0)
+ missing = "wl_output";
+ if (missing) {
+ fprintf(stderr,
+ "mouseboard: compositor lacks %s; a wlroots-based "
+ "compositor (sway, Hyprland, river, ...) is required\n",
+ missing);
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+
+ /* Optional: authoritative logical geometry via xdg-output. */
+ if (st->xdg_output_manager) {
+ for (int s = 0; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ o->xdg = zxdg_output_manager_v1_get_xdg_output(
+ st->xdg_output_manager, o->wl);
+ zxdg_output_v1_add_listener(o->xdg,
+ &xdg_output_listener, o);
+ }
+ wl_display_roundtrip(st->display);
+ }
+
+ for (int s = 0; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ o->surface = wl_compositor_create_surface(st->compositor);
+ o->layer = zwlr_layer_shell_v1_get_layer_surface(
+ st->layer_shell, o->surface, o->wl,
+ ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "mouseboard");
+ zwlr_layer_surface_v1_add_listener(o->layer, &ls_listener, o);
+ zwlr_layer_surface_v1_set_anchor(
+ o->layer,
+ ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
+ ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
+ ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
+ ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);
+ zwlr_layer_surface_v1_set_exclusive_zone(o->layer, -1);
+ zwlr_layer_surface_v1_set_keyboard_interactivity(
+ o->layer,
+ ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE);
+ zwlr_layer_surface_v1_set_size(o->layer, 0, 0);
+
+ /* Empty input region: the overlay grabs the keyboard but lets
+ * pointer events fall through, so the synthesized click reaches
+ * the window underneath rather than the overlay itself. */
+ struct wl_region *empty =
+ wl_compositor_create_region(st->compositor);
+ wl_surface_set_input_region(o->surface, empty);
+ wl_region_destroy(empty);
+
+ wl_surface_commit(o->surface);
+ }
+ /* One device covers the complete layout, so a cross-output drag sends its
+ * press, motion, and release through the same virtual pointer. */
+ st->vp = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(
+ st->vp_manager, st->seat);
+
+ while (st->alive && st->configured_count < st->n_outputs) {
+ if (wl_display_dispatch(st->display) < 0) {
+ fprintf(stderr, "mouseboard: display error\n");
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+ }
+ if (!st->alive) {
+ if (!st->failed)
+ fprintf(stderr, "mouseboard: overlay closed during setup\n");
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+ wl_display_roundtrip(st->display); /* settle keymap/modifiers */
+ if (!st->keyboard || !st->xkb_state) {
+ fprintf(stderr, "mouseboard: no usable Wayland keyboard\n");
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+
+ int64_t min_x = st->outputs[0].x, min_y = st->outputs[0].y;
+ int64_t max_x = min_x + st->outputs[0].width;
+ int64_t max_y = min_y + st->outputs[0].height;
+ for (int s = 1; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ if (o->x < min_x)
+ min_x = o->x;
+ if (o->y < min_y)
+ min_y = o->y;
+ if ((int64_t)o->x + o->width > max_x)
+ max_x = (int64_t)o->x + o->width;
+ if ((int64_t)o->y + o->height > max_y)
+ max_y = (int64_t)o->y + o->height;
+ }
+ if (max_x > INT_MAX || max_y > INT_MAX || max_x - min_x > INT_MAX ||
+ max_y - min_y > INT_MAX) {
+ fprintf(stderr, "mouseboard: output layout is too large\n");
+ wl_display_disconnect(st->display);
+ free(st);
+ return NULL;
+ }
+ st->bx = (int)min_x;
+ st->by = (int)min_y;
+ st->bw = (int)(max_x - min_x);
+ st->bh = (int)(max_y - min_y);
+
+ g_platform.priv = st;
+ g_platform.n_screens = st->n_outputs;
+ for (int s = 0; s < st->n_outputs; s++) {
+ struct output *o = &st->outputs[s];
+ g_platform.screens[s] = (struct mb_screen){ o->x, o->y,
+ o->width, o->height };
+ }
+ st->ready = 1;
+ g_platform.frame_begin = wl_frame_begin;
+ g_platform.frame_commit = wl_frame_commit;
+ g_platform.next_key = wl_next_key;
+ g_platform.pointer_move = wl_pointer_move;
+ g_platform.pointer_button = wl_pointer_button;
+ g_platform.teardown = wl_teardown;
+ return &g_platform;
+}