From 2fa72ee0dc67d973330aec4ba98260e9f57f7821 Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: Enter mouseboard 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. --- .gitignore | 8 + LICENSE | 21 + Makefile | 126 +++ Makefile.win | 51 ++ README | 190 ++++ backend-wayland.c | 854 +++++++++++++++++ backend-windows.c | 403 ++++++++ config.c | 273 ++++++ config.example | 44 + config.h | 60 ++ core.c | 755 +++++++++++++++ core.h | 21 + draw.c | 111 +++ draw.h | 28 + font.c | 16 + font.h | 15 + font8x8_basic.h | 152 ++++ main.c | 131 +++ mouseboard.1 | 104 +++ platform.h | 79 ++ proto/SOURCES | 29 + proto/wlr-layer-shell-unstable-v1.xml | 407 +++++++++ proto/wlr-virtual-pointer-unstable-v1.xml | 152 ++++ proto/xdg-output-unstable-v1.xml | 222 +++++ proto/xdg-shell.xml | 1415 +++++++++++++++++++++++++++++ provision-static.sh | 190 ++++ selftest.c | 12 + 27 files changed, 5869 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 Makefile.win create mode 100644 README create mode 100644 backend-wayland.c create mode 100644 backend-windows.c create mode 100644 config.c create mode 100644 config.example create mode 100644 config.h create mode 100644 core.c create mode 100644 core.h create mode 100644 draw.c create mode 100644 draw.h create mode 100644 font.c create mode 100644 font.h create mode 100644 font8x8_basic.h create mode 100644 main.c create mode 100644 mouseboard.1 create mode 100644 platform.h create mode 100644 proto/SOURCES create mode 100644 proto/wlr-layer-shell-unstable-v1.xml create mode 100644 proto/wlr-virtual-pointer-unstable-v1.xml create mode 100644 proto/xdg-output-unstable-v1.xml create mode 100644 proto/xdg-shell.xml create mode 100755 provision-static.sh create mode 100644 selftest.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0e668f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +mouseboard +mouseboard.exe +mouseboard-selftest +*.o +*-protocol.h +*-protocol.c +/musl-sysroot +/musl-build diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d80dabc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Lena + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24fa1f6 --- /dev/null +++ b/Makefile @@ -0,0 +1,126 @@ +# mouseboard - keyboard-driven virtual pointer for Wayland. +# +# Override on the command line, e.g. make PREFIX=$HOME/.local +# +# `make` is the dev build: it links the system's shared wayland-client and +# xkbcommon (install the dev packages; see README). `make static` is the +# shipping build: it compiles and links against the static archives in +# musl-sysroot/, which ./provision-static.sh builds once per machine. + +VERSION = 0-dev + +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +MANDIR ?= $(PREFIX)/share/man/man1 + +CC ?= cc +PKG_CONFIG ?= pkg-config +WAYLAND_SCANNER ?= wayland-scanner + +PKGS = wayland-client xkbcommon + +# STATIC=1 (what the `static` target runs) switches to the provisioned musl +# sysroot: its cc wrapper, its pkg-config metadata, and a fully static link. +STATIC ?= 0 +ifeq ($(STATIC),1) +SYSROOT := $(CURDIR)/musl-sysroot +CC = $(SYSROOT)/bin/cc +# provision-static.sh builds a matching wayland-scanner into the sysroot, so +# the static build needs no host wayland-scanner (versions can't drift). +WAYLAND_SCANNER = $(SYSROOT)/bin/wayland-scanner +PKGCONF = PKG_CONFIG_PATH=$(SYSROOT)/lib/pkgconfig:$(SYSROOT)/share/pkgconfig $(PKG_CONFIG) +# -s strips at link: the shipped binary carries no symbols or debug info +# (musl's and libffi's archives ship with debug sections). The dev build +# keeps symbols for gdb. +MB_LDFLAGS = -static -s +MB_LDLIBS = $(shell $(PKGCONF) --static --libs $(PKGS) 2>/dev/null) +else +PKGCONF = $(PKG_CONFIG) +MB_LDLIBS = $(shell $(PKGCONF) --libs $(PKGS) 2>/dev/null) +endif + +CFLAGS ?= -std=c11 -Wall -Wextra -O2 +override CPPFLAGS += -DMOUSEBOARD_VERSION='"$(VERSION)"' +override CPPFLAGS += $(shell $(PKGCONF) --cflags $(PKGS) 2>/dev/null) + +# Protocols vendored under proto/; generated glue lives at the top level. +PROTOCOLS = \ + xdg-shell \ + xdg-output-unstable-v1 \ + wlr-layer-shell-unstable-v1 \ + wlr-virtual-pointer-unstable-v1 + +PROTO_H = $(PROTOCOLS:%=%-protocol.h) +PROTO_C = $(PROTOCOLS:%=%-protocol.c) +PROTO_O = $(PROTOCOLS:%=%-protocol.o) + +OBJ = main.o core.o config.o draw.o font.o backend-wayland.o $(PROTO_O) +SELFTEST_SRC = selftest.c core.c config.c draw.c font.c + +# font8x8_basic.h is vendored verbatim; its 0xFF rows overflow signed char. +font.o: override CFLAGS += -Wno-overflow + +all: mouseboard + +check: mouseboard-selftest + ./mouseboard-selftest + +mouseboard-selftest: $(SELFTEST_SRC) config.h core.h draw.h font.h platform.h \ + font8x8_basic.h + $(CC) $(CPPFLAGS) $(CFLAGS) -Wno-overflow -o $@ $(SELFTEST_SRC) + +# Objects from the other mode were built by a different compiler, so switching +# always starts clean. Going back: make clean && make. +static: + $(MAKE) clean + $(MAKE) STATIC=1 + +mouseboard: $(OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) $(MB_LDFLAGS) -o $@ $(OBJ) \ + $(LDLIBS) $(MB_LDLIBS) + +# Every object depends on every header: over-rebuilds a little, never links +# stale. +$(OBJ): $(PROTO_H) config.h core.h draw.h font.h platform.h font8x8_basic.h + +ifeq ($(STATIC),1) +# provision-static.sh writes the stamp last, once the sysroot is complete. +check-deps: + @test -f $(SYSROOT)/.provisioned || { \ + echo "mouseboard: musl-sysroot/ missing or incomplete;"; \ + echo " run ./provision-static.sh first"; \ + exit 1; } +else +check-deps: + @command -v $(WAYLAND_SCANNER) >/dev/null 2>&1 && \ + $(PKG_CONFIG) --exists $(PKGS) 2>/dev/null || { \ + echo "mouseboard: wayland-client/xkbcommon not found by pkg-config."; \ + echo " dev build: install wayland-scanner, libwayland-dev,"; \ + echo " and libxkbcommon-dev"; \ + echo " static binary (needs no system libs):"; \ + echo " ./provision-static.sh && make static"; \ + exit 1; } +endif + +# Every object needs the generated headers, so nothing compiles before the +# toolchain check runs. +$(PROTO_H) $(PROTO_C): | check-deps + +%-protocol.h: proto/%.xml + $(WAYLAND_SCANNER) client-header $< $@ + +%-protocol.c: proto/%.xml + $(WAYLAND_SCANNER) private-code $< $@ + +install: mouseboard + install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR) + install -m 755 mouseboard $(DESTDIR)$(BINDIR)/mouseboard + install -m 644 mouseboard.1 $(DESTDIR)$(MANDIR)/mouseboard.1 + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/mouseboard $(DESTDIR)$(MANDIR)/mouseboard.1 + +clean: + rm -f mouseboard mouseboard-selftest $(OBJ) $(PROTO_H) $(PROTO_C) + +.PHONY: all static check check-deps install uninstall clean diff --git a/Makefile.win b/Makefile.win new file mode 100644 index 0000000..fffe03f --- /dev/null +++ b/Makefile.win @@ -0,0 +1,51 @@ +# Makefile.win - cross-compile the Windows build with mingw-w64. +# +# Links the Win32 backend instead of the Wayland one (no protocols, no +# wayland-scanner, no pkg-config). Produces a single static mouseboard.exe. +# +# make -f Makefile.win # x86_64 +# make -f Makefile.win CC=i686-w64-mingw32-gcc # 32-bit +# +# Needs a mingw-w64 cross toolchain on the build host: +# Debian / Ubuntu : sudo apt install gcc-mingw-w64-x86-64 +# Arch : sudo pacman -S mingw-w64-gcc +# Fedora : sudo dnf install mingw64-gcc +# +# Built for the GUI subsystem (-mwindows) so launching it - e.g. from a hotkey - +# creates no console window. The backend still calls AttachConsole() at startup, +# so --help / --dry-run / --selftest output appears when run from a terminal. + +VERSION = 0-dev + +# Unconditional: make predefines CC=cc, which ?= would keep, silently +# building with the host compiler. `make -f Makefile.win CC=...` still wins. +CC = x86_64-w64-mingw32-gcc +CFLAGS ?= -std=c11 -Wall -Wextra -O2 +override CPPFLAGS += -DMOUSEBOARD_VERSION='"$(VERSION)"' +# -s strips at link: no symbols in the shipped exe. +MB_LDFLAGS = -static -mwindows -s +MB_LDLIBS = -lgdi32 -luser32 + +# Distinct object names: the Linux Makefile compiles main.o etc. with the +# host cc, and sharing names would link objects from the wrong compiler when +# switching builds without a clean. +OBJ = main.win.o core.win.o config.win.o draw.win.o font.win.o \ + backend-windows.win.o + +# font8x8_basic.h is vendored verbatim; its 0xFF rows overflow signed char. +font.win.o: override CFLAGS += -Wno-overflow + +mouseboard.exe: $(OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) $(MB_LDFLAGS) -o $@ $(OBJ) \ + $(LDLIBS) $(MB_LDLIBS) + +# Every object depends on every header: over-rebuilds a little, never stale. +$(OBJ): config.h core.h draw.h font.h platform.h font8x8_basic.h + +%.win.o: %.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +clean: + rm -f mouseboard.exe $(OBJ) + +.PHONY: clean diff --git a/README b/README new file mode 100644 index 0000000..ee81bde --- /dev/null +++ b/README @@ -0,0 +1,190 @@ +mouseboard +========== +Keyboard-driven virtual pointer for Wayland and Windows. Bring up a labelled +grid over the screen, type a label to jump to a cell, refine the spot by +bisection, then press one key to click. No mouse, no daemon. + + +What it does +------------ +Run `mouseboard` (bound to a hotkey) and a grid is drawn over every monitor: + +1. Grid Type the two-or-so letter label in a cell to jump there. +2. Refine The cell becomes the active region. Split it by quadrant with + u/i/j/k until the crosshair sits exactly where you want; the pointer + tracks the region centre live. Backspace undoes a split. +3. Click m left, , middle, . right, ; double, v drag. Esc cancels. + +Bisection gives unlimited precision in a few keystrokes, so the coarse grid can +stay coarse and the labels short. + + +Requirements +------------ +Linux: a wlroots-based compositor (sway, Hyprland, river, Wayfire, labwc, ...) +implementing `wlr-layer-shell` and `wlr-virtual-pointer`. GNOME and KDE are not +supported yet (see Portability). A shipped static binary needs nothing else. + +Windows: Windows 10 1703 or newer (per-monitor DPI v2). The shipped .exe is +static and needs nothing else. + + +Build and install +----------------- +The Wayland protocols are vendored under proto/, so a build needs only a +toolchain. The release build is static: provision its dependencies once, then +build and install. + + ./provision-static.sh # once per machine: static wayland/xkbcommon/ffi + # archives (pinned, checksummed) into musl-sysroot/ + make static # -> ./mouseboard, one static binary + sudo make install + +The result is self-contained, already stripped, and runs on any Linux. + +Besides a C compiler, make and pkg-config, provisioning needs meson, ninja, +bison, expat, curl and the Linux kernel headers - plus a musl toolchain on a +glibc distro (Debian, Arch). It builds its own wayland-scanner from source, so +the host's version does not matter. On musl-native distros (Alpine, Void-musl) +gcc is already musl and provision-static.sh uses it automatically; set CC only +to override the detected compiler. + +These commands install enough for both Linux builds: + + # Debian / Ubuntu + sudo apt install build-essential pkgconf libwayland-bin libwayland-dev \ + libxkbcommon-dev meson ninja-build bison curl musl-tools libexpat1-dev + + # Arch + sudo pacman -S --needed base-devel wayland libxkbcommon meson ninja bison \ + curl linux-api-headers musl expat + + # Alpine (musl-native: gcc is already musl, detected automatically) + sudo apk add build-base pkgconf wayland-dev libxkbcommon-dev meson samurai \ + bison curl linux-headers expat-dev + + # Void (use the musl flavour: gcc is already musl, detected automatically) + sudo xbps-install -S base-devel pkgconf wayland-devel libxkbcommon-devel \ + meson ninja bison curl expat-devel + + # Gentoo (gcc/make/pkg-config already in @system) + sudo emerge --ask dev-libs/wayland x11-libs/libxkbcommon dev-libs/expat \ + meson ninja sys-devel/bison net-misc/curl sys-kernel/linux-headers + + +Windows build +------------- +The .exe is cross-built from Linux with a mingw-w64 toolchain (native mingw on +Windows builds the same way). It links only gdi32/user32, so there are no other +libraries: + + # Debian / Ubuntu + sudo apt install gcc-mingw-w64-x86-64 make + + # Arch + sudo pacman -S --needed mingw-w64-gcc make + + # Fedora + sudo dnf install mingw64-gcc make + + make -f Makefile.win # -> mouseboard.exe + +Copy the result to the Windows machine and run it; there is nothing to install. + + +Developing (dynamic build) +-------------------------- +A plain `make` links against the system's shared libraries, so it needs no +provisioning - only wayland-scanner (it ships with the Wayland package) plus the +wayland-client, xkbcommon and Linux kernel development headers: + + make # dev build; produces ./mouseboard + make check # dependency-free core and state-machine tests + +If anything is missing it tells you exactly what to install. `make static` +cleans first (the modes use different compilers); run `make clean` when +switching back. + + +Usage +----- +Wayland does not let a client grab a global hotkey, so bind the command in your +compositor. One binding runs the whole flow; the final key decides the button. + +sway (~/.config/sway/config): + + bindsym $mod+g exec mouseboard + +Hyprland (~/.config/hypr/hyprland.conf): + + bind = SUPER, G, exec, mouseboard + +Windows: run mouseboard.exe directly, or bind it to a hotkey with a shortcut or +a tool such as AutoHotkey. It grabs the keyboard while the grid is up, so keys +do not leak to the focused window; Esc cancels. + +Try it without clicking anything; it prints the target and action instead: + + mouseboard --dry-run + + +Keys +---- +Defaults; all are configurable (see Configuration). + + label chars f j d k s l a g h r u e i w o q p t y v b c n x m z + refine u top-left i top-right j bottom-left k bottom-right + click m left , middle . right ; double v drag + Enter left click (once a cell is selected) + Backspace undo last split (or return to the grid) + Esc cancel + +Drag is two targets: press v over the start, then pick the end the same way +(grid, refine) and press any click key to release. + + +Configuration +------------- +Optional file at $XDG_CONFIG_HOME/mouseboard/config (default +~/.config/mouseboard/config; on Windows %APPDATA%\mouseboard\config). Lines are +`key = value`; lines starting with # are comments. Command-line flags override +the file. See config.example for every key with its default. Colours are +#RRGGBB or #RRGGBBAA. Two opacities (0..100): `opacity_bg` dims the desktop +(default 33; 100 is solid) and `opacity_fg` sets how strong the labels, grid +and crosshair are (default 65). + + +How to debug +------------ + mouseboard --dry-run print "x y action" instead of moving the pointer + mouseboard --selftest run offline checks of labels/bisection/config + WAYLAND_DEBUG=1 mouseboard --dry-run trace every protocol message + +If it exits with "compositor lacks ...", the compositor does not implement a +required wlroots protocol and is not supported. If labels do not appear, confirm +the binding actually launched mouseboard (run it from a terminal to see errors). + + +Portability +----------- +The core (grid, labels, bisection, rendering, config) talks only to the backend +interface in platform.h and never includes an OS header. A backend is one file +providing platform_init(), and exactly one is linked into the binary. + +The wlroots Wayland backend uses layer-shell for the overlay and virtual-pointer +for input. The Win32 backend draws a per-monitor layered overlay with +UpdateLayeredWindow, grabs the keyboard with a low-level hook, and clicks via +SendInput. It is a GUI-subsystem binary, so a hotkey launch shows no console; it +attaches to a parent console when run from a terminal, so --dry-run and --help +still print. + + +Credits +------- +mouseboard is released under the MIT licence; see LICENSE. It bundles +third-party files, each kept under its own terms: + +- font8x8_basic.h: 8x8 bitmap font by Daniel Hepper and Marcel Sondaar / IBM, + public domain. +- proto/*.xml: Wayland protocol descriptions from wayland-protocols and + wlr-protocols, MIT/Expat. Per-file provenance is in proto/SOURCES. 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#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, ®_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; +} diff --git a/backend-windows.c b/backend-windows.c new file mode 100644 index 0000000..d37c3c4 --- /dev/null +++ b/backend-windows.c @@ -0,0 +1,403 @@ +/* + * backend-windows.c - Win32 backend. + * + * Overlay : one per-monitor layered window (WS_EX_LAYERED) that is + * click-through (WS_EX_TRANSPARENT) and topmost. Its pixels come + * straight from the core's ARGB8888 buffer via UpdateLayeredWindow + * with per-pixel alpha - no conversion, since the buffer is already + * premultiplied BGRA, which is exactly what a 32bpp DIB holds. + * Keyboard : a low-level keyboard hook (WH_KEYBOARD_LL) grabs every key + * globally, so no window focus is needed; consumed keys are + * swallowed. + * Pointer : SendInput with absolute, virtual-desktop coordinates. + */ +#define WINVER 0x0A00 +#define _WIN32_WINNT 0x0A00 +#include "platform.h" + +#include +#include +#include +#include + +#define KQ_SIZE 64 + +struct win_screen { + HWND hwnd; + HDC memdc; + HBITMAP dib; + HGDIOBJ old; /* default bitmap, restored on teardown */ + int x, y, w, h; /* monitor rect, physical pixels */ + struct mb_buffer pub; /* px points straight at the DIB bits */ +}; + +struct win_state { + struct win_screen scr[MB_MAX_SCREENS]; + int n; + HHOOK hook; + DWORD tid; /* thread that owns the hook + message loop */ + uint32_t kq[KQ_SIZE]; /* keys decoded by the hook, drained by next_key */ + int kq_head, kq_tail; +}; + +static struct platform g_platform; +static struct win_state g_ws; + +void platform_sleep_ms(int ms) +{ + Sleep((DWORD)ms); +} + +void platform_early_init(void) +{ + /* GUI-subsystem binary: launching it (e.g. from a hotkey) creates no + * console window. When started from a terminal, attach to its console + * so --help, --dry-run and --selftest still print. */ + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + (void)freopen("CONOUT$", "w", stdout); + (void)freopen("CONOUT$", "w", stderr); + (void)freopen("CONIN$", "r", stdin); + } +} + +/* ---- key queue ------------------------------------------------------- */ + +static void kq_push(uint32_t k) +{ + int n = (g_ws.kq_tail + 1) % KQ_SIZE; + if (n == g_ws.kq_head) + return; /* full; drop */ + g_ws.kq[g_ws.kq_tail] = k; + g_ws.kq_tail = n; +} + +/* ---- keyboard hook --------------------------------------------------- */ + +/* Translate a virtual-key + scancode to an MB_KEY. The few editing keys are + * mapped directly; everything else goes through the active keyboard layout so + * labels follow the user's layout, same as the Wayland xkb path. */ +static uint32_t map_key(DWORD vk, DWORD scan) +{ + switch (vk) { + case VK_ESCAPE: + return MB_KEY_ESC; + case VK_BACK: + return MB_KEY_BACKSPACE; + case VK_RETURN: + return MB_KEY_ENTER; + } + /* GetKeyboardState is stale on a thread that pumps no keyboard input - + * keys arrive only through the LL hook - so sample the modifiers that + * affect translation directly. */ + BYTE ks[256]; + memset(ks, 0, sizeof ks); + if (GetAsyncKeyState(VK_SHIFT) & 0x8000) + ks[VK_SHIFT] = 0x80; + if (GetKeyState(VK_CAPITAL) & 1) + ks[VK_CAPITAL] = 1; + WCHAR buf[8]; + HWND foreground = GetForegroundWindow(); + DWORD tid = foreground ? GetWindowThreadProcessId(foreground, NULL) : 0; + int n = ToUnicodeEx(vk, scan, ks, buf, 8, 0, GetKeyboardLayout(tid)); + if (n == 1 && buf[0] >= 0x20 && buf[0] < 0x7f) + return (uint32_t)buf[0]; + return MB_KEY_NONE; +} + +/* Bare modifiers do nothing on their own, and swallowing their key-ups would + * leave the OS thinking e.g. the launch hotkey's Win key is still held after + * exit - so they pass through the hook untouched. */ +static int is_modifier(DWORD vk) +{ + switch (vk) { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + case VK_CAPITAL: + return 1; + } + return 0; +} + +static LRESULT CALLBACK ll_hook(int code, WPARAM w, LPARAM l) +{ + if (code == HC_ACTION) { + const KBDLLHOOKSTRUCT *k = (const KBDLLHOOKSTRUCT *)l; + if (is_modifier(k->vkCode)) + return CallNextHookEx(NULL, code, w, l); + if (w == WM_KEYDOWN || w == WM_SYSKEYDOWN) { + uint32_t mb = map_key(k->vkCode, k->scanCode); + if (mb != MB_KEY_NONE) { + kq_push(mb); + /* Wake the GetMessage() in next_key; with no + * focused window the raw key posts no message + * of its own. */ + PostThreadMessage(g_ws.tid, WM_NULL, 0, 0); + } + } + /* Swallow every non-modifier key while the overlay is up: + * nothing leaks to the focused application. */ + return 1; + } + return CallNextHookEx(NULL, code, w, l); +} + +static uint32_t win_next_key(struct platform *p) +{ + (void)p; + for (;;) { + if (g_ws.kq_head != g_ws.kq_tail) { + uint32_t k = g_ws.kq[g_ws.kq_head]; + g_ws.kq_head = (g_ws.kq_head + 1) % KQ_SIZE; + return k; + } + MSG m; + int r = GetMessage(&m, NULL, 0, 0); + if (r <= 0) + return MB_KEY_NONE; /* WM_QUIT or error */ + TranslateMessage(&m); + DispatchMessage(&m); + } +} + +/* ---- frame interface ------------------------------------------------- */ + +static struct mb_buffer *win_frame_begin(struct platform *p, int s) +{ + (void)p; + struct win_screen *w = &g_ws.scr[s]; + memset(w->pub.px, 0, (size_t)w->w * w->h * 4); + return &w->pub; +} + +static int win_frame_commit(struct platform *p) +{ + (void)p; + for (int s = 0; s < g_ws.n; s++) { + struct win_screen *w = &g_ws.scr[s]; + POINT src = { 0, 0 }; + POINT dst = { w->x, w->y }; + SIZE size = { w->w, w->h }; + BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; + if (!UpdateLayeredWindow(w->hwnd, NULL, &dst, &size, w->memdc, + &src, 0, &bf, ULW_ALPHA)) { + fprintf(stderr, "mouseboard: UpdateLayeredWindow failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + } + return 0; +} + +/* ---- pointer --------------------------------------------------------- */ + +static int win_pointer_move(struct platform *p, int x, int y) +{ + (void)p; + int vx = GetSystemMetrics(SM_XVIRTUALSCREEN); + int vy = GetSystemMetrics(SM_YVIRTUALSCREEN); + int vw = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int vh = GetSystemMetrics(SM_CYVIRTUALSCREEN); + if (vw < 2) + vw = 2; + if (vh < 2) + vh = 2; + INPUT in; + memset(&in, 0, sizeof in); + in.type = INPUT_MOUSE; + in.mi.dx = (LONG)(((int64_t)(x - vx) * 65535) / (vw - 1)); + in.mi.dy = (LONG)(((int64_t)(y - vy) * 65535) / (vh - 1)); + in.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | + MOUSEEVENTF_VIRTUALDESK; + SetLastError(0); + if (SendInput(1, &in, sizeof in) != 1) { + fprintf(stderr, "mouseboard: SendInput move failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + return 0; +} + +static int win_pointer_button(struct platform *p, enum mb_button b, int press) +{ + (void)p; + DWORD f; + if (b == MB_LEFT) + f = press ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; + else if (b == MB_MIDDLE) + f = press ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP; + else + f = press ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; + INPUT in; + memset(&in, 0, sizeof in); + in.type = INPUT_MOUSE; + in.mi.dwFlags = f; + SetLastError(0); + if (SendInput(1, &in, sizeof in) != 1) { + fprintf(stderr, "mouseboard: SendInput button failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + return 0; +} + +/* ---- window + monitor setup ------------------------------------------ */ + +static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + if (m == WM_DESTROY) { + PostQuitMessage(0); + return 0; + } + return DefWindowProc(h, m, w, l); +} + +static BOOL CALLBACK mon_proc(HMONITOR hm, HDC dc, LPRECT rc, LPARAM lp) +{ + (void)dc; + (void)rc; + (void)lp; + if (g_ws.n >= MB_MAX_SCREENS) + return TRUE; + MONITORINFO mi; + mi.cbSize = sizeof mi; + if (!GetMonitorInfo(hm, &mi)) + return TRUE; + struct win_screen *w = &g_ws.scr[g_ws.n++]; + w->x = mi.rcMonitor.left; + w->y = mi.rcMonitor.top; + w->w = mi.rcMonitor.right - mi.rcMonitor.left; + w->h = mi.rcMonitor.bottom - mi.rcMonitor.top; + return TRUE; +} + +static int screen_setup(struct win_screen *w, HINSTANCE hinst) +{ + w->hwnd = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT | + WS_EX_TOPMOST | WS_EX_TOOLWINDOW | + WS_EX_NOACTIVATE, + L"mouseboard", L"", WS_POPUP, w->x, w->y, w->w, + w->h, NULL, NULL, hinst, NULL); + if (!w->hwnd) + return -1; + + HDC screen = GetDC(NULL); + w->memdc = CreateCompatibleDC(screen); + ReleaseDC(NULL, screen); + if (!w->memdc) + return -1; + + BITMAPINFO bi; + memset(&bi, 0, sizeof bi); + bi.bmiHeader.biSize = sizeof bi.bmiHeader; + bi.bmiHeader.biWidth = w->w; + bi.bmiHeader.biHeight = -w->h; /* top-down: row 0 is the top */ + bi.bmiHeader.biPlanes = 1; + bi.bmiHeader.biBitCount = 32; + bi.bmiHeader.biCompression = BI_RGB; + void *bits = NULL; + w->dib = CreateDIBSection(w->memdc, &bi, DIB_RGB_COLORS, &bits, NULL, 0); + if (!w->dib) + return -1; + w->old = SelectObject(w->memdc, w->dib); + + w->pub.px = bits; + w->pub.w = w->w; + w->pub.h = w->h; + w->pub.stride = w->w; /* device pixels per row */ + w->pub.scale = 1; /* physical pixels: per-monitor DPI aware */ + + ShowWindow(w->hwnd, SW_SHOWNOACTIVATE); + return 0; +} + +/* ---- teardown -------------------------------------------------------- */ + +static void win_teardown(struct platform *p) +{ + (void)p; + if (g_ws.hook) { + UnhookWindowsHookEx(g_ws.hook); + g_ws.hook = NULL; + } + for (int s = 0; s < g_ws.n; s++) { + struct win_screen *w = &g_ws.scr[s]; + if (w->memdc) { + if (w->old) + SelectObject(w->memdc, w->old); + DeleteDC(w->memdc); + } + if (w->dib) + DeleteObject(w->dib); + if (w->hwnd) + DestroyWindow(w->hwnd); + } + g_ws.n = 0; +} + +/* ---- init ------------------------------------------------------------ */ + +struct platform *platform_init(void) +{ + /* Physical pixels everywhere: monitor rects, the overlay buffers and + * the SendInput coordinates then share one coordinate space. */ + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + HINSTANCE hinst = GetModuleHandle(NULL); + WNDCLASSEXW wc; + memset(&wc, 0, sizeof wc); + wc.cbSize = sizeof wc; + wc.lpfnWndProc = wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszClassName = L"mouseboard"; + if (!RegisterClassExW(&wc)) { + fprintf(stderr, "mouseboard: RegisterClassEx failed\n"); + return NULL; + } + + EnumDisplayMonitors(NULL, NULL, mon_proc, 0); + if (g_ws.n == 0) { + fprintf(stderr, "mouseboard: no monitors found\n"); + return NULL; + } + + for (int s = 0; s < g_ws.n; s++) { + if (screen_setup(&g_ws.scr[s], hinst) < 0) { + fprintf(stderr, + "mouseboard: overlay window setup failed\n"); + win_teardown(&g_platform); + return NULL; + } + } + + g_ws.tid = GetCurrentThreadId(); + g_ws.hook = SetWindowsHookEx(WH_KEYBOARD_LL, ll_hook, hinst, 0); + if (!g_ws.hook) { + fprintf(stderr, "mouseboard: cannot install keyboard hook\n"); + win_teardown(&g_platform); + return NULL; + } + + g_platform.priv = &g_ws; + g_platform.n_screens = g_ws.n; + for (int s = 0; s < g_ws.n; s++) + g_platform.screens[s] = (struct mb_screen){ + g_ws.scr[s].x, g_ws.scr[s].y, g_ws.scr[s].w, g_ws.scr[s].h + }; + g_platform.frame_begin = win_frame_begin; + g_platform.frame_commit = win_frame_commit; + g_platform.next_key = win_next_key; + g_platform.pointer_move = win_pointer_move; + g_platform.pointer_button = win_pointer_button; + g_platform.teardown = win_teardown; + return &g_platform; +} diff --git a/config.c b/config.c new file mode 100644 index 0000000..4b930df --- /dev/null +++ b/config.c @@ -0,0 +1,273 @@ +/* config.c - defaults, config file parsing, and shared key/value sets. */ +#include "config.h" + +#include +#include +#include +#include +#include + +void config_defaults(struct config *c) +{ + /* Home row first, then the rest of the top/bottom rows: common cells + * get the easiest two-key labels. */ + strcpy(c->alphabet, "fjdkslaghrueiwoqptyvbcnxmz"); + c->cell_size = 100; + c->cols = 0; + c->rows = 0; + c->font_scale = 0; + + c->opacity_bg = 33; /* how dark the screen goes (100 = solid) */ + c->opacity_fg = 65; /* labels, grid and crosshair */ + + /* Full-alpha colours (grid deliberately fainter); opacity_bg/fg + * scale them once at startup. */ + c->color_bg = 0xff101014; /* near-black backdrop */ + c->color_grid = 0x40ffffff; /* faint white grid lines */ + c->color_label = 0xffffffff; /* white labels */ + c->color_typed = 0xff52a8ff; /* blue for the typed prefix */ + c->color_active = 0xffff5252; /* red refine box + crosshair */ + + c->k_tl = 'u'; + c->k_tr = 'i'; + c->k_bl = 'j'; + c->k_br = 'k'; + c->k_left = 'm'; + c->k_middle = ','; + c->k_right = '.'; + c->k_double = ';'; + c->k_drag = 'v'; +} + +static int parse_color(const char *s, uint32_t *out) +{ + if (*s == '#') + s++; + size_t n = strlen(s); + if (n != 6 && n != 8) + return -1; + for (size_t i = 0; i < n; i++) + if (!isxdigit((unsigned char)s[i])) + return -1; + + unsigned long v = strtoul(s, NULL, 16); + uint32_t r, g, b, a; + if (n == 6) { + r = (v >> 16) & 0xff; + g = (v >> 8) & 0xff; + b = v & 0xff; + a = 0xff; + } else { + r = (v >> 24) & 0xff; + g = (v >> 16) & 0xff; + b = (v >> 8) & 0xff; + a = v & 0xff; + } + *out = (a << 24) | (r << 16) | (g << 8) | b; + return 0; +} + +static int parse_int(const char *s, int *out) +{ + char *end; + errno = 0; + long v = strtol(s, &end, 10); + if (errno || end == s || *end || v < 0 || v > 100000) + return -1; + *out = (int)v; + return 0; +} + +/* A character the user can actually type at the grid: printable ASCII, no + * space. Uppercase is rejected because incoming keys are lowercased (so Caps + * Lock cannot break labels) - an uppercase binding would never fire. */ +static int typeable(unsigned char ch) +{ + return ch > 0x20 && ch < 0x7f && !(ch >= 'A' && ch <= 'Z'); +} + +int config_set(struct config *c, const char *key, const char *val) +{ + if (!strcmp(key, "alphabet")) { + if (strlen(val) < 2 || strlen(val) >= sizeof c->alphabet) + return -2; + for (size_t i = 0; val[i]; i++) { + if (!typeable((unsigned char)val[i])) + return -2; + if (strchr(val + i + 1, val[i])) + return -2; /* duplicate char breaks labels */ + } + strcpy(c->alphabet, val); + return 0; + } + + if (!strcmp(key, "opacity_bg")) { + int v; + if (parse_int(val, &v) || v > 100) + return -2; + c->opacity_bg = v; + return 0; + } + if (!strcmp(key, "opacity_fg")) { + int v; + if (parse_int(val, &v) || v > 100) + return -2; + c->opacity_fg = v; + return 0; + } + + if (!strcmp(key, "cell_size")) + return parse_int(val, &c->cell_size) ? -2 : 0; + if (!strcmp(key, "cols")) + return parse_int(val, &c->cols) ? -2 : 0; + if (!strcmp(key, "rows")) + return parse_int(val, &c->rows) ? -2 : 0; + if (!strcmp(key, "font_scale")) + return parse_int(val, &c->font_scale) ? -2 : 0; + + if (!strcmp(key, "color_bg")) + return parse_color(val, &c->color_bg) ? -2 : 0; + if (!strcmp(key, "color_grid")) + return parse_color(val, &c->color_grid) ? -2 : 0; + if (!strcmp(key, "color_label")) + return parse_color(val, &c->color_label) ? -2 : 0; + if (!strcmp(key, "color_typed")) + return parse_color(val, &c->color_typed) ? -2 : 0; + if (!strcmp(key, "color_active")) + return parse_color(val, &c->color_active) ? -2 : 0; + + /* Single-character key bindings. */ + struct { const char *name; char *slot; } keys[] = { + { "refine_tl", &c->k_tl }, { "refine_tr", &c->k_tr }, + { "refine_bl", &c->k_bl }, { "refine_br", &c->k_br }, + { "click_left", &c->k_left }, { "click_middle", &c->k_middle }, + { "click_right", &c->k_right }, { "click_double", &c->k_double }, + { "drag", &c->k_drag }, + }; + for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) { + if (strcmp(key, keys[i].name)) + continue; + if (strlen(val) != 1 || !typeable((unsigned char)val[0])) + return -2; + *keys[i].slot = val[0]; + return 0; + } + + return -1; +} + +const char *config_validate(const struct config *c) +{ + if (c->cell_size < 1) + return "cell_size must be greater than zero"; + if ((c->cols > 0) != (c->rows > 0)) + return "cols and rows must be set together"; + if (c->cols > MB_MAX_GRID_DIM || c->rows > MB_MAX_GRID_DIM) + return "cols and rows must not exceed 200"; + if (c->font_scale > MB_MAX_FONT_SCALE) + return "font_scale must not exceed 64"; + + const char keys[] = { + c->k_tl, c->k_tr, c->k_bl, c->k_br, c->k_left, + c->k_middle, c->k_right, c->k_double, c->k_drag, + }; + for (size_t i = 0; i < sizeof keys; i++) + for (size_t j = i + 1; j < sizeof keys; j++) + if (keys[i] == keys[j]) + return "refine and action keys must be unique"; + return NULL; +} + +void config_apply_opacity(struct config *c) +{ + uint32_t *fg[] = { &c->color_grid, &c->color_label, + &c->color_typed, &c->color_active }; + uint32_t a = (c->color_bg >> 24) * (uint32_t)c->opacity_bg / 100; + c->color_bg = (a << 24) | (c->color_bg & 0x00ffffff); + for (size_t i = 0; i < sizeof fg / sizeof fg[0]; i++) { + a = (*fg[i] >> 24) * (uint32_t)c->opacity_fg / 100; + *fg[i] = (a << 24) | (*fg[i] & 0x00ffffff); + } +} + +static char *trim(char *s) +{ + while (*s && isspace((unsigned char)*s)) + s++; + char *e = s + strlen(s); + while (e > s && isspace((unsigned char)e[-1])) + *--e = 0; + return s; +} + +int config_load_file(struct config *c, const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) { + if (errno == ENOENT) + return -1; + fprintf(stderr, "%s: %s\n", path, strerror(errno)); + return -2; + } + + char line[256]; + int lineno = 0, rc = 0; + while (fgets(line, sizeof line, f)) { + lineno++; + size_t len = strlen(line); + if (len && line[len - 1] != '\n' && !feof(f)) { + int ch; + while ((ch = fgetc(f)) != '\n' && ch != EOF) + ; + fprintf(stderr, "%s:%d: line too long\n", path, lineno); + rc = -2; + continue; + } + /* Comments are whole lines starting with '#'; this keeps '#' + * usable inside values (colours are #RRGGBB). */ + char *body = trim(line); + if (!*body || *body == '#') + continue; + char *eq = strchr(body, '='); + if (!eq) { + fprintf(stderr, "%s:%d: missing '='\n", path, lineno); + rc = -2; + continue; + } + *eq = 0; + char *key = trim(body); + char *val = trim(eq + 1); + int r = config_set(c, key, val); + if (r == -1) + fprintf(stderr, "%s:%d: unknown key '%s'\n", path, + lineno, key); + else if (r == -2) + fprintf(stderr, "%s:%d: bad value for '%s'\n", path, + lineno, key); + if (r) + rc = -2; + } + fclose(f); + return rc; +} + +const char *config_default_path(void) +{ + static char path[512]; + const char *xdg = getenv("XDG_CONFIG_HOME"); + if (xdg && *xdg) { + snprintf(path, sizeof path, "%s/mouseboard/config", xdg); + return path; + } + /* Windows sets APPDATA, not HOME; fopen accepts the mixed slashes. */ + const char *appdata = getenv("APPDATA"); + if (appdata && *appdata) { + snprintf(path, sizeof path, "%s/mouseboard/config", appdata); + return path; + } + const char *home = getenv("HOME"); + if (!home) + home = ""; + snprintf(path, sizeof path, "%s/.config/mouseboard/config", home); + return path; +} diff --git a/config.example b/config.example new file mode 100644 index 0000000..378c269 --- /dev/null +++ b/config.example @@ -0,0 +1,44 @@ +# mouseboard configuration. Copy to $XDG_CONFIG_HOME/mouseboard/config +# (default ~/.config/mouseboard/config; on Windows %APPDATA%\mouseboard\config). +# Lines are "key = value"; lines starting with # are comments (no inline +# comments). Command-line flags override this file. Values below are the +# defaults. + +# Label characters, most-preferred first. Common cells get the easiest labels. +# alphabet = fjdkslaghrueiwoqptyvbcnxmz + +# Coarse grid. Either a target cell size in pixels (auto cols/rows per monitor), +# or an explicit cols/rows pair (set both, 1..200; they override cell_size). +# cell_size = 100 +# cols = 0 +# rows = 0 + +# Label glyph scale. 0 means auto-fit each cell; the maximum is 64. +# font_scale = 0 + +# Opacity, 0..100 each. opacity_bg is the backdrop (0 = no dim, 100 = solid); +# opacity_fg covers labels, grid lines and the refine crosshair. The flags +# --opacity and --opacity-fg override them. +# opacity_bg = 33 +# opacity_fg = 65 + +# Colours, #RRGGBB or #RRGGBBAA. An explicit alpha sets an element's weight +# relative to the others; opacity_bg/opacity_fg then scale them. +# color_bg = #101014 +# color_grid = #ffffff40 +# color_label = #ffffff +# color_typed = #52a8ff +# color_active = #ff5252 + +# Refine (bisection) keys: keep the named quadrant of the active region. +# refine_tl = u +# refine_tr = i +# refine_bl = j +# refine_br = k + +# Click keys. The final key decides the button. +# click_left = m +# click_middle = , +# click_right = . +# click_double = ; +# drag = v diff --git a/config.h b/config.h new file mode 100644 index 0000000..efbd74f --- /dev/null +++ b/config.h @@ -0,0 +1,60 @@ +/* + * config.h - configuration: defaults, file parsing, and CLI overrides all flow + * through config_set(), so the file and the command line share one vocabulary. + * + * Colours are stored 0xAARRGGBB and written in the config as #RRGGBB or + * #RRGGBBAA. Key bindings are single ASCII characters; cancel (Esc) and undo + * (Backspace) are fixed. + */ +#ifndef MOUSEBOARD_CONFIG_H +#define MOUSEBOARD_CONFIG_H + +#include + +#define MB_MAX_GRID_DIM 200 +#define MB_MAX_FONT_SCALE 64 + +struct config { + char alphabet[64]; /* label characters, most-preferred first */ + int cell_size; /* target coarse-cell size in px (if cols/rows 0) */ + int cols, rows; /* explicit grid; 0 means derive from cell_size */ + int font_scale; /* label scale; 0 means auto-fit per cell */ + + int opacity_bg; /* backdrop opacity 0..100 (100 = solid) */ + int opacity_fg; /* labels/grid/crosshair opacity 0..100 */ + uint32_t color_bg; /* full-screen backdrop */ + uint32_t color_grid; /* grid lines */ + uint32_t color_label; /* label glyphs */ + uint32_t color_typed; /* already-typed portion of a label */ + uint32_t color_active; /* refine box and crosshair */ + + char k_tl, k_tr, k_bl, k_br; /* refine: keep TL/TR/BL/BR quadrant */ + char k_left, k_middle, k_right; /* click buttons */ + char k_double, k_drag; /* double-click; start/finish drag */ +}; + +void config_defaults(struct config *c); + +/* Apply one "key value" pair. Returns 0 on success, -1 unknown key, + * -2 bad value. */ +int config_set(struct config *c, const char *key, const char *val); + +/* Return NULL if the complete configuration is usable, otherwise a short + * diagnostic. Call after file and command-line overrides have been applied. */ +const char *config_validate(const struct config *c); + +/* Scale color_bg's alpha by opacity_bg/100 and the other colours' by + * opacity_fg/100. Call exactly once, after all file and command-line + * overrides. The default colours carry full alpha, so 100 means solid. */ +void config_apply_opacity(struct config *c); + +/* Parse a config file. Returns 0 on success, -1 if it does not exist (not an + * error), -2 on a parse error (message printed to stderr). */ +int config_load_file(struct config *c, const char *path); + +/* Default config path: $XDG_CONFIG_HOME/mouseboard/config, falling back to + * %APPDATA%/mouseboard/config (Windows), then ~/.config/mouseboard/config. + * Static buffer. */ +const char *config_default_path(void); + +#endif /* MOUSEBOARD_CONFIG_H */ diff --git a/core.c b/core.c new file mode 100644 index 0000000..aa72e8e --- /dev/null +++ b/core.c @@ -0,0 +1,755 @@ +/* core.c - the interaction state machine, independent of any backend. */ +#include "core.h" +#include "draw.h" +#include "font.h" + +#include +#include +#include + +#define MAX_LABEL 24 + +struct rect { int x, y, w, h; }; + +struct cell { + struct rect r; /* global coordinates */ + int screen; + char label[MAX_LABEL]; +}; + +enum action { ACT_NONE, ACT_LEFT, ACT_MIDDLE, ACT_RIGHT, ACT_DOUBLE, ACT_DRAG }; + +/* ---- labels ---------------------------------------------------------- */ + +static int label_len(int base, int n) +{ + int len = 1; + long cap = base; + while (cap < n) { + cap *= base; + len++; + } + return len; +} + +static void make_label(char *out, const char *alpha, int base, int len, int idx) +{ + out[len] = 0; + for (int i = len - 1; i >= 0; i--) { + out[i] = alpha[idx % base]; + idx /= base; + } +} + +/* ---- geometry -------------------------------------------------------- */ + +static int fit_scale(int cw, int ch, int len) +{ + int wunit = len * FONT_W + (len - 1); /* label width at scale 1 */ + int sw = (cw * 8 / 10) / wunit; /* fit 80% of cell width */ + int sh = (ch * 6 / 10) / FONT_H; /* fit 60% of cell height */ + int s = sw < sh ? sw : sh; + if (s < 1) + s = 1; + if (s > 8) + s = 8; + return s; +} + +static int screen_at(struct platform *p, int x, int y) +{ + for (int s = 0; s < p->n_screens; s++) { + struct mb_screen c = p->screens[s]; + if (x >= c.x && x < c.x + c.w && y >= c.y && y < c.y + c.h) + return s; + } + return -1; +} + +static int build_cells(const struct config *cfg, struct platform *p, + struct cell **out, int *out_len) +{ + int base = (int)strlen(cfg->alphabet); + int total = 0; + int cols[MB_MAX_SCREENS], rows[MB_MAX_SCREENS]; + + for (int s = 0; s < p->n_screens; s++) { + struct mb_screen sc = p->screens[s]; + int c, r; + if (cfg->cols > 0 && cfg->rows > 0) { + c = cfg->cols; + r = cfg->rows; + } else { + int cs = cfg->cell_size > 0 ? cfg->cell_size : 100; + c = (sc.w + cs / 2) / cs; + r = (sc.h + cs / 2) / cs; + } + if (c < 1) + c = 1; + if (r < 1) + r = 1; + if (c > MB_MAX_GRID_DIM) + c = MB_MAX_GRID_DIM; + if (r > MB_MAX_GRID_DIM) + r = MB_MAX_GRID_DIM; + cols[s] = c; + rows[s] = r; + total += c * r; + } + if (total <= 0) + return 0; + + int len = label_len(base, total); + if (len >= MAX_LABEL) + len = MAX_LABEL - 1; + + struct cell *cells = calloc((size_t)total, sizeof *cells); + if (!cells) + return 0; + + int idx = 0; + for (int s = 0; s < p->n_screens; s++) { + struct mb_screen sc = p->screens[s]; + for (int cy = 0; cy < rows[s]; cy++) { + for (int cx = 0; cx < cols[s]; cx++) { + int x0 = sc.x + cx * sc.w / cols[s]; + int x1 = sc.x + (cx + 1) * sc.w / cols[s]; + int y0 = sc.y + cy * sc.h / rows[s]; + int y1 = sc.y + (cy + 1) * sc.h / rows[s]; + struct cell *cl = &cells[idx]; + cl->r = (struct rect){ x0, y0, x1 - x0, + y1 - y0 }; + cl->screen = s; + make_label(cl->label, cfg->alphabet, base, len, + idx); + idx++; + } + } + } + + *out = cells; + *out_len = len; + return total; +} + +/* ---- rendering ------------------------------------------------------- */ + +/* Begin every screen's overlay and paint the dim backdrop, filling bufs[]. */ +static int frame_begin_dim(struct platform *p, const struct config *cfg, + struct mb_buffer **bufs) +{ + for (int s = 0; s < p->n_screens; s++) { + bufs[s] = p->frame_begin(p, s); + if (!bufs[s]) + return -1; + draw_fill(bufs[s], 0, 0, bufs[s]->w, bufs[s]->h, cfg->color_bg); + } + return 0; +} + +static int render_grid(struct platform *p, const struct config *cfg, + struct cell *cells, int ncells, int len, + const char *typed) +{ + struct mb_buffer *bufs[MB_MAX_SCREENS]; + if (frame_begin_dim(p, cfg, bufs)) + return -1; + + int tlen = (int)strlen(typed); + for (int i = 0; i < ncells; i++) { + struct cell *cl = &cells[i]; + struct mb_buffer *b = bufs[cl->screen]; + struct mb_screen sc = p->screens[cl->screen]; + int lx = cl->r.x - sc.x, ly = cl->r.y - sc.y; + + draw_rect(b, lx, ly, cl->r.w, cl->r.h, 1, cfg->color_grid); + if (tlen && strncmp(cl->label, typed, (size_t)tlen) != 0) + continue; + + int scale = cfg->font_scale > 0 ? cfg->font_scale + : fit_scale(cl->r.w, cl->r.h, len); + const char *lab = cl->label; + int tx = lx + (cl->r.w - text_width(lab, scale)) / 2; + int ty = ly + (cl->r.h - text_height(scale)) / 2; + + if (tlen) { + char pre[MAX_LABEL]; + memcpy(pre, lab, (size_t)tlen); + pre[tlen] = 0; + draw_text(b, tx, ty, pre, scale, cfg->color_typed); + int off = text_width(pre, scale) + scale; + draw_text(b, tx + off, ty, lab + tlen, scale, + cfg->color_label); + } else { + draw_text(b, tx, ty, lab, scale, cfg->color_label); + } + } + return p->frame_commit(p); +} + +/* Draw a refine key centred in its quadrant, while the quadrant is large enough + * for the glyph to be legible; it shrinks away as the region is bisected. */ +static void draw_quad_key(struct mb_buffer *b, int qx, int qy, int qw, int qh, + char key, uint32_t fg) +{ + int smin = qw < qh ? qw : qh; + int scale = (smin / 2) / FONT_W; + if (scale < 1) + return; + if (scale > 6) + scale = 6; + char s[2] = { key, 0 }; + int tx = qx + (qw - text_width(s, scale)) / 2; + int ty = qy + (qh - text_height(scale)) / 2; + draw_text(b, tx, ty, s, scale, fg); +} + +static int render_refine(struct platform *p, const struct config *cfg, + struct rect cur) +{ + struct mb_buffer *bufs[MB_MAX_SCREENS]; + if (frame_begin_dim(p, cfg, bufs)) + return -1; + + int cx = cur.x + cur.w / 2, cy = cur.y + cur.h / 2; + int s = screen_at(p, cx, cy); + if (s < 0) + s = 0; + struct mb_buffer *b = bufs[s]; + struct mb_screen sc = p->screens[s]; + int lx = cur.x - sc.x, ly = cur.y - sc.y; + uint32_t ac = cfg->color_active; + /* Guide lines at ~38% of the active alpha, so they track opacity. */ + uint32_t guide = (ac & 0x00ffffff) | (((ac >> 24) * 0x60 / 0xff) << 24); + + draw_rect(b, lx, ly, cur.w, cur.h, 2, ac); + draw_vline(b, lx + cur.w / 2, ly, cur.h, 1, guide); + draw_hline(b, lx, ly + cur.h / 2, cur.w, 1, guide); + + int hw = cur.w / 2, hh = cur.h / 2; + int rw = cur.w - hw, rh = cur.h - hh; + draw_quad_key(b, lx, ly, hw, hh, cfg->k_tl, cfg->color_label); + draw_quad_key(b, lx + hw, ly, rw, hh, cfg->k_tr, cfg->color_label); + draw_quad_key(b, lx, ly + hh, hw, rh, cfg->k_bl, cfg->color_label); + draw_quad_key(b, lx + hw, ly + hh, rw, rh, cfg->k_br, cfg->color_label); + + int gx = lx + cur.w / 2, gy = ly + cur.h / 2; + draw_hline(b, gx - 12, gy - 1, 25, 2, ac); + draw_vline(b, gx - 1, gy - 12, 25, 2, ac); + return p->frame_commit(p); +} + +/* ---- actions --------------------------------------------------------- */ + +static enum action action_for(const struct config *cfg, uint32_t k) +{ + if (k == MB_KEY_ENTER) + return ACT_LEFT; + if (k > 0x7e) + return ACT_NONE; + char c = (char)k; + if (c == cfg->k_left) + return ACT_LEFT; + if (c == cfg->k_middle) + return ACT_MIDDLE; + if (c == cfg->k_right) + return ACT_RIGHT; + if (c == cfg->k_double) + return ACT_DOUBLE; + if (c == cfg->k_drag) + return ACT_DRAG; + return ACT_NONE; +} + +#define CLICK_MS 12 /* down->up and inter-click gap; 0ms clicks get missed */ + +static int do_click(struct platform *p, enum mb_button b, int times) +{ + for (int i = 0; i < times; i++) { + if (i) + platform_sleep_ms(CLICK_MS); + if (p->pointer_button(p, b, 1)) { + p->pointer_button(p, b, 0); + return -1; + } + platform_sleep_ms(CLICK_MS); + if (p->pointer_button(p, b, 0)) + return -1; + } + return 0; +} + +static int emit(struct platform *p, int dry, int x, int y, enum action a) +{ + static const char *name[] = { "none", "left", "middle", + "right", "double", "drag" }; + if (dry) { + printf("%d %d %s\n", x, y, name[a]); + return 0; + } + if (p->pointer_move(p, x, y)) + return -1; + switch (a) { + case ACT_LEFT: + return do_click(p, MB_LEFT, 1); + case ACT_MIDDLE: + return do_click(p, MB_MIDDLE, 1); + case ACT_RIGHT: + return do_click(p, MB_RIGHT, 1); + case ACT_DOUBLE: + return do_click(p, MB_LEFT, 2); + default: + return 0; + } +} + +/* ---- main flow ------------------------------------------------------- */ + +int core_run(struct platform *p, const struct config *cfg, int dry) +{ + if (cfg->cols > 0 && cfg->rows > 0) { + for (int s = 0; s < p->n_screens; s++) { + if (cfg->cols > p->screens[s].w || + cfg->rows > p->screens[s].h) { + fprintf(stderr, + "mouseboard: grid exceeds screen dimensions\n"); + return 2; + } + } + } + struct cell *cells; + int len; + int ncells = build_cells(cfg, p, &cells, &len); + if (ncells <= 0) { + fprintf(stderr, "mouseboard: no screen cells\n"); + return 2; + } + + enum { GRID, REFINE } state = GRID; + char typed[MAX_LABEL] = ""; + int tn = 0; + struct rect stack[64]; + int sp = 0; + int drag = 0; + int rc = 2; + + if (render_grid(p, cfg, cells, ncells, len, typed)) + goto out; + + for (;;) { + uint32_t k = p->next_key(p); + if (k >= 'A' && k <= 'Z') + k += 'a' - 'A'; /* labels and bindings are lowercase; + makes Caps Lock harmless */ + if (k == MB_KEY_NONE) { + rc = 2; + break; + } + if (k == MB_KEY_ESC) { + rc = 1; + break; + } + + if (state == GRID) { + if (k == MB_KEY_BACKSPACE) { + if (tn) { + typed[--tn] = 0; + if (render_grid(p, cfg, cells, ncells, len, + typed)) + break; + } + continue; + } + if (k < 0x20 || k > 0x7e || tn >= MAX_LABEL - 1) + continue; + + typed[tn] = (char)k; + typed[tn + 1] = 0; + int matches = 0, exact = -1; + for (int i = 0; i < ncells; i++) { + if (strncmp(cells[i].label, typed, + (size_t)(tn + 1))) + continue; + matches++; + if ((int)strlen(cells[i].label) == tn + 1) + exact = i; + } + if (matches == 0) { + typed[tn] = 0; /* reject, keep prompt */ + continue; + } + tn++; + if (exact >= 0 && tn == len) { + stack[0] = cells[exact].r; + sp = 1; + state = REFINE; + struct rect c = stack[0]; + if (!dry) { + if (p->pointer_move(p, c.x + c.w / 2, + c.y + c.h / 2)) + break; + } + if (render_refine(p, cfg, c)) + break; + } else { + if (render_grid(p, cfg, cells, ncells, len, typed)) + break; + } + continue; + } + + /* REFINE */ + struct rect cur = stack[sp - 1]; + if (k == MB_KEY_BACKSPACE) { + if (sp > 1) { + sp--; + cur = stack[sp - 1]; + if (!dry) { + if (p->pointer_move(p, cur.x + cur.w / 2, + cur.y + cur.h / 2)) + break; + } + if (render_refine(p, cfg, cur)) + break; + } else { + state = GRID; + tn = 0; + typed[0] = 0; + if (render_grid(p, cfg, cells, ncells, len, typed)) + break; + } + continue; + } + + char c = (k <= 0x7e) ? (char)k : 0; + int hw = cur.w / 2, hh = cur.h / 2; + int rw = cur.w - hw, rh = cur.h - hh; + struct rect nr; + int split = 1; + if (c && c == cfg->k_tl) + nr = (struct rect){ cur.x, cur.y, hw, hh }; + else if (c && c == cfg->k_tr) + nr = (struct rect){ cur.x + hw, cur.y, rw, hh }; + else if (c && c == cfg->k_bl) + nr = (struct rect){ cur.x, cur.y + hh, hw, rh }; + else if (c && c == cfg->k_br) + nr = (struct rect){ cur.x + hw, cur.y + hh, rw, rh }; + else + split = 0; + + if (split) { + if (sp >= (int)(sizeof stack / sizeof stack[0])) + continue; /* depth limit; the region has been + 1x1 for dozens of splits by now */ + if (nr.w < 1) + nr.w = 1; + if (nr.h < 1) + nr.h = 1; + stack[sp++] = nr; + cur = nr; + if (!dry) { + if (p->pointer_move(p, cur.x + cur.w / 2, + cur.y + cur.h / 2)) + break; + } + if (render_refine(p, cfg, cur)) + break; + continue; + } + + enum action a = action_for(cfg, k); + if (a == ACT_NONE) + continue; + + int tx = cur.x + cur.w / 2, ty = cur.y + cur.h / 2; + if (a == ACT_DRAG && !drag) { + if (dry) + printf("%d %d drag-start\n", tx, ty); + else { + if (p->pointer_move(p, tx, ty)) + break; + if (p->pointer_button(p, MB_LEFT, 1)) { + p->pointer_button(p, MB_LEFT, 0); + break; + } + } + drag = 1; + state = GRID; + tn = 0; + typed[0] = 0; + if (render_grid(p, cfg, cells, ncells, len, typed)) + break; + continue; + } + if (drag) { + if (dry) + printf("%d %d drag-end\n", tx, ty); + else { + if (p->pointer_move(p, tx, ty) || + p->pointer_button(p, MB_LEFT, 0)) + break; + } + drag = 0; + rc = 0; + break; + } + if (emit(p, dry, tx, ty, a)) + break; + rc = 0; + break; + } + +out: + if (drag && !dry && p->pointer_button(p, MB_LEFT, 0)) + rc = 2; + free(cells); + return rc; +} + +/* ---- offline self-test ---------------------------------------------- */ + +static int st_fail; + +static void check(int cond, const char *what) +{ + if (!cond) { + st_fail++; + printf("FAIL: %s\n", what); + } else { + printf("ok: %s\n", what); + } +} + +struct test_platform { + uint32_t keys[8]; + int key_count; + int key_pos; + int commits; + int moves; + int buttons; + int last_x, last_y; + int fail_commit; + int fail_move; + int fail_button_down; + struct mb_buffer buffer; + uint32_t pixels[100 * 80]; +}; + +static struct mb_buffer *test_frame_begin(struct platform *p, int screen) +{ + (void)screen; + return &((struct test_platform *)p->priv)->buffer; +} + +static int test_frame_commit(struct platform *p) +{ + struct test_platform *t = p->priv; + t->commits++; + return t->fail_commit ? -1 : 0; +} + +static uint32_t test_next_key(struct platform *p) +{ + struct test_platform *t = p->priv; + if (t->key_pos >= t->key_count) + return MB_KEY_NONE; + return t->keys[t->key_pos++]; +} + +static int test_pointer_move(struct platform *p, int x, int y) +{ + struct test_platform *t = p->priv; + t->moves++; + t->last_x = x; + t->last_y = y; + return t->fail_move ? -1 : 0; +} + +static int test_pointer_button(struct platform *p, enum mb_button b, int press) +{ + (void)b; + struct test_platform *t = p->priv; + t->buttons++; + if (press && t->fail_button_down) + return -1; + return 0; +} + +static void test_platform_init(struct platform *p, struct test_platform *t) +{ + memset(t, 0, sizeof *t); + *p = (struct platform){ 0 }; + p->n_screens = 1; + p->screens[0] = (struct mb_screen){ 0, 0, 100, 80 }; + p->frame_begin = test_frame_begin; + p->frame_commit = test_frame_commit; + p->next_key = test_next_key; + p->pointer_move = test_pointer_move; + p->pointer_button = test_pointer_button; + p->priv = t; + t->buffer = (struct mb_buffer){ t->pixels, 100, 80, 100, 1 }; +} + +int core_selftest(void) +{ + st_fail = 0; + + /* label length and base conversion */ + check(label_len(2, 4) == 2, "label_len(2,4)==2"); + check(label_len(26, 26) == 1, "label_len(26,26)==1"); + check(label_len(26, 27) == 2, "label_len(26,27)==2"); + + const char *alpha = "abcdef"; + int base = 6, len = label_len(base, 36); + check(len == 2, "len for 36 over base 6 == 2"); + char l0[8], l1[8], l35[8]; + make_label(l0, alpha, base, len, 0); + make_label(l1, alpha, base, len, 1); + make_label(l35, alpha, base, len, 35); + check(!strcmp(l0, "aa"), "label 0 == aa"); + check(!strcmp(l1, "ab"), "label 1 == ab"); + check(!strcmp(l35, "ff"), "label 35 == ff"); + + /* labels are unique across a range */ + int dup = 0; + char seen[36][8]; + for (int i = 0; i < 36; i++) + make_label(seen[i], alpha, base, len, i); + for (int i = 0; i < 36 && !dup; i++) + for (int j = i + 1; j < 36; j++) + if (!strcmp(seen[i], seen[j])) + dup = 1; + check(!dup, "labels unique over full range"); + + /* bisection converges and stays inside the cell */ + struct rect r = { 0, 0, 1000, 800 }; + struct rect cur = r; + for (int i = 0; i < 12; i++) { + int hw = cur.w / 2, hh = cur.h / 2; + cur = (struct rect){ cur.x, cur.y, hw, hh }; /* keep TL */ + } + check(cur.w <= 1 && cur.h <= 1, "bisection shrinks to a point"); + check(cur.x >= r.x && cur.y >= r.y, "bisection stays inside cell"); + + /* config parsing */ + struct config cfg; + config_defaults(&cfg); + check(config_set(&cfg, "color_active", "#112233") == 0, + "parse #RRGGBB"); + check(cfg.color_active == 0xff112233, "color #RRGGBB -> 0xAARRGGBB"); + check(config_set(&cfg, "color_bg", "#11223344") == 0, + "parse #RRGGBBAA"); + check(cfg.color_bg == 0x44112233, "color #RRGGBBAA -> 0xAARRGGBB"); + check(config_set(&cfg, "click_left", "z") == 0, "parse binding"); + check(cfg.k_left == 'z', "binding applied"); + check(config_set(&cfg, "bogus", "x") == -1, "unknown key rejected"); + check(config_set(&cfg, "cell_size", "abc") == -2, "bad int rejected"); + check(config_set(&cfg, "click_left", "Z") == -2, + "uppercase binding rejected"); + check(config_set(&cfg, "alphabet", "abC") == -2, + "uppercase alphabet rejected"); + check(config_set(&cfg, "opacity_bg", "50") == 0, "parse opacity_bg"); + check(config_set(&cfg, "opacity_fg", "150") == -2, + "opacity >100 rejected"); + check(config_validate(&cfg) == NULL, "default config is valid"); + struct config bad = cfg; + bad.rows = 2; + check(config_validate(&bad) != NULL, "incomplete grid rejected"); + bad = cfg; + bad.k_drag = bad.k_tl; + check(config_validate(&bad) != NULL, "binding collision rejected"); + bad = cfg; + bad.font_scale = MB_MAX_FONT_SCALE + 1; + check(config_validate(&bad) != NULL, "excessive font scale rejected"); + + /* opacity_bg drives the backdrop, opacity_fg everything else; the + full-alpha default colours make 100 mean solid */ + struct config oc; + config_defaults(&oc); + config_apply_opacity(&oc); + check((oc.color_bg >> 24) == 0xff * 33 / 100, + "opacity_bg scales backdrop alpha"); + check((oc.color_label >> 24) == 0xff * 65 / 100, + "opacity_fg scales label alpha"); + struct config os; + config_defaults(&os); + os.opacity_bg = 100; + config_apply_opacity(&os); + check((os.color_bg >> 24) == 0xff, "opacity_bg 100 is solid"); + + /* build_cells tiles every screen with uniquely labelled cells */ + struct platform pl = { 0 }; + pl.n_screens = 2; + pl.screens[0] = (struct mb_screen){ 0, 0, 1000, 800 }; + pl.screens[1] = (struct mb_screen){ 1000, 0, 1920, 1080 }; + struct config c2; + config_defaults(&c2); + struct cell *cells; + int clen; + int n = build_cells(&c2, &pl, &cells, &clen); + check(n > 0, "build_cells returns cells"); + long area[2] = { 0, 0 }; + int inside = 1, unique = 1; + for (int i = 0; i < n; i++) { + struct mb_screen sc = pl.screens[cells[i].screen]; + struct rect cr = cells[i].r; + if (cr.x < sc.x || cr.y < sc.y || cr.x + cr.w > sc.x + sc.w || + cr.y + cr.h > sc.y + sc.h) + inside = 0; + area[cells[i].screen] += (long)cr.w * cr.h; + for (int j = i + 1; j < n && unique; j++) + if (!strcmp(cells[i].label, cells[j].label)) + unique = 0; + } + check(inside, "cells stay inside their screen"); + check(area[0] == 1000L * 800 && area[1] == 1920L * 1080, + "cells tile each screen exactly"); + check(unique, "cell labels unique across screens"); + check(n > 0 && (int)strlen(cells[0].label) == clen, + "label length matches build_cells"); + free(cells); + + /* Complete state-machine flows through a fake dependency-free backend. */ + struct platform tp; + struct test_platform ts; + struct config tc; + config_defaults(&tc); + tc.cols = tc.rows = 1; + test_platform_init(&tp, &ts); + ts.keys[0] = (uint32_t)tc.alphabet[0]; + ts.keys[1] = MB_KEY_ENTER; + ts.key_count = 2; + check(core_run(&tp, &tc, 0) == 0, "core click flow succeeds"); + check(ts.moves == 2 && ts.buttons == 2, "click emits move, down and up"); + check(ts.last_x == 50 && ts.last_y == 40, "click targets cell centre"); + + test_platform_init(&tp, &ts); + ts.keys[0] = (uint32_t)tc.alphabet[0]; + ts.keys[1] = (uint32_t)tc.k_drag; + ts.keys[2] = MB_KEY_ESC; + ts.key_count = 3; + check(core_run(&tp, &tc, 0) == 1, "drag cancellation reports cancel"); + check(ts.buttons == 2, "drag cancellation releases held button"); + + test_platform_init(&tp, &ts); + ts.keys[0] = (uint32_t)tc.alphabet[0]; + ts.key_count = 1; + ts.fail_move = 1; + check(core_run(&tp, &tc, 0) == 2, "pointer failure reaches caller"); + check(ts.buttons == 0, "pointer failure emits no click"); + + test_platform_init(&tp, &ts); + ts.fail_commit = 1; + check(core_run(&tp, &tc, 0) == 2, "presentation failure reaches caller"); + + test_platform_init(&tp, &ts); + ts.keys[0] = (uint32_t)tc.alphabet[0]; + ts.keys[1] = MB_KEY_ENTER; + ts.key_count = 2; + ts.fail_button_down = 1; + check(core_run(&tp, &tc, 0) == 2, "button failure reaches caller"); + check(ts.buttons == 2, "failed button press gets release attempt"); + + printf(st_fail ? "\n%d FAILED\n" : "\nall passed\n", st_fail); + return st_fail ? 1 : 0; +} diff --git a/core.h b/core.h new file mode 100644 index 0000000..d6047f7 --- /dev/null +++ b/core.h @@ -0,0 +1,21 @@ +/* core.h - platform-independent interaction logic. */ +#ifndef MOUSEBOARD_CORE_H +#define MOUSEBOARD_CORE_H + +#include "config.h" +#include "platform.h" + +/* + * Run the full flow: draw the grid, read keys, refine by bisection, and click. + * If dry_run is set, the chosen target and action are printed to stdout instead + * of moving/clicking the pointer. + * + * Returns 0 if an action was taken, 1 if the user cancelled, 2 on error. + */ +int core_run(struct platform *p, const struct config *cfg, int dry_run); + +/* Offline checks of the pure logic (labels, bisection, config). Prints results; + * returns 0 if all pass. Needs no display. */ +int core_selftest(void); + +#endif /* MOUSEBOARD_CORE_H */ diff --git a/draw.c b/draw.c new file mode 100644 index 0000000..297f5d0 --- /dev/null +++ b/draw.c @@ -0,0 +1,111 @@ +/* draw.c - software rendering onto an ARGB8888 mb_buffer. */ +#include "draw.h" +#include "font.h" + +#include + +/* wl_shm ARGB8888 and Win32 layered windows both hold premultiplied alpha. */ +static uint32_t premul(uint32_t argb) +{ + uint32_t a = argb >> 24; + uint32_t r = (argb >> 16) & 0xff; + uint32_t g = (argb >> 8) & 0xff; + uint32_t b = argb & 0xff; + r = r * a / 255; + g = g * a / 255; + b = b * a / 255; + return (a << 24) | (r << 16) | (g << 8) | b; +} + +/* Fill a logical rectangle, expanding each logical pixel to a scale*scale block + * of device pixels. Clipping is in logical space; scale=1 is the identity. */ +static void fill_premul(struct mb_buffer *b, int x, int y, int w, int h, + uint32_t pm) +{ + int s = b->scale; + int x1 = x + w, y1 = y + h; + if (x < 0) + x = 0; + if (y < 0) + y = 0; + if (x1 > b->w) + x1 = b->w; + if (y1 > b->h) + y1 = b->h; + int n = (x1 - x) * s; + for (int row = y; row < y1; row++) { + for (int sy = 0; sy < s; sy++) { + uint32_t *p = b->px + + (size_t)(row * s + sy) * b->stride + + (size_t)x * s; + for (int i = 0; i < n; i++) + p[i] = pm; + } + } +} + +void draw_fill(struct mb_buffer *b, int x, int y, int w, int h, uint32_t argb) +{ + fill_premul(b, x, y, w, h, premul(argb)); +} + +void draw_hline(struct mb_buffer *b, int x, int y, int len, int thick, + uint32_t argb) +{ + fill_premul(b, x, y, len, thick, premul(argb)); +} + +void draw_vline(struct mb_buffer *b, int x, int y, int len, int thick, + uint32_t argb) +{ + fill_premul(b, x, y, thick, len, premul(argb)); +} + +void draw_rect(struct mb_buffer *b, int x, int y, int w, int h, int thick, + uint32_t argb) +{ + uint32_t pm = premul(argb); + fill_premul(b, x, y, w, thick, pm); /* top */ + fill_premul(b, x, y + h - thick, w, thick, pm); /* bottom */ + fill_premul(b, x, y, thick, h, pm); /* left */ + fill_premul(b, x + w - thick, y, thick, h, pm); /* right */ +} + +int text_height(int scale) +{ + return FONT_H * scale; +} + +int text_width(const char *s, int scale) +{ + int n = 0; + while (s[n]) + n++; + if (n == 0) + return 0; + return n * (FONT_W * scale) + (n - 1) * scale; /* scale-px gaps */ +} + +int draw_text(struct mb_buffer *b, int x, int y, const char *s, int scale, + uint32_t fg) +{ + int advance = FONT_W * scale + scale; + int w = text_width(s, scale); + + uint32_t fpm = premul(fg); + int pen = x; + for (const char *c = s; *c; c++) { + const unsigned char *g = font_glyph(*c); + for (int gy = 0; gy < FONT_H; gy++) { + unsigned char bits = g[gy]; + for (int gx = 0; gx < FONT_W; gx++) { + if (!(bits & (1u << gx))) + continue; + fill_premul(b, pen + gx * scale, + y + gy * scale, scale, scale, fpm); + } + } + pen += advance; + } + return w; +} diff --git a/draw.h b/draw.h new file mode 100644 index 0000000..945c0f7 --- /dev/null +++ b/draw.h @@ -0,0 +1,28 @@ +/* + * draw.h - software rendering onto an ARGB8888 mb_buffer. + * + * Colours are straight-alpha 0xAARRGGBB; they are premultiplied on the way in + * and overwrite the destination (the compositor blends the finished overlay + * over the desktop). All primitives clip to the buffer. + */ +#ifndef MOUSEBOARD_DRAW_H +#define MOUSEBOARD_DRAW_H + +#include "platform.h" + +void draw_fill(struct mb_buffer *b, int x, int y, int w, int h, uint32_t argb); +void draw_rect(struct mb_buffer *b, int x, int y, int w, int h, int thick, + uint32_t argb); +void draw_hline(struct mb_buffer *b, int x, int y, int len, int thick, + uint32_t argb); +void draw_vline(struct mb_buffer *b, int x, int y, int len, int thick, + uint32_t argb); + +/* Render text at scale (integer multiple of the 8x8 cell). Returns the pixel + * width drawn. */ +int draw_text(struct mb_buffer *b, int x, int y, const char *s, int scale, + uint32_t fg); +int text_width(const char *s, int scale); +int text_height(int scale); + +#endif /* MOUSEBOARD_DRAW_H */ diff --git a/font.c b/font.c new file mode 100644 index 0000000..035d330 --- /dev/null +++ b/font.c @@ -0,0 +1,16 @@ +/* font.c - the embedded font table. */ +#include "font.h" + +/* Public-domain 8x8 VGA font by Daniel Hepper / Marcel Sondaar / IBM. + * Vendored verbatim; included here only, so the table has one definition. */ +#include "font8x8_basic.h" + +static const unsigned char blank[FONT_H] = { 0 }; + +const unsigned char *font_glyph(char c) +{ + unsigned char u = (unsigned char)c; + if (u >= 128) + return blank; + return (const unsigned char *)font8x8_basic[u]; +} diff --git a/font.h b/font.h new file mode 100644 index 0000000..62ef4bf --- /dev/null +++ b/font.h @@ -0,0 +1,15 @@ +/* font.h - access to the embedded 8x8 bitmap font. */ +#ifndef MOUSEBOARD_FONT_H +#define MOUSEBOARD_FONT_H + +#define FONT_W 8 +#define FONT_H 8 + +/* + * Return the 8-byte glyph for ASCII character c. Each byte is one row, top to + * bottom; within a row bit 0 (LSB) is the leftmost pixel. Non-printable codes + * return the blank glyph. + */ +const unsigned char *font_glyph(char c); + +#endif /* MOUSEBOARD_FONT_H */ diff --git a/font8x8_basic.h b/font8x8_basic.h new file mode 100644 index 0000000..125cf16 --- /dev/null +++ b/font8x8_basic.h @@ -0,0 +1,152 @@ +/** + * 8x8 monochrome bitmap fonts for rendering + * Author: Daniel Hepper + * + * License: Public Domain + * + * Based on: + * // Summary: font8x8.h + * // 8x8 monochrome bitmap fonts for rendering + * // + * // Author: + * // Marcel Sondaar + * // International Business Machines (public domain VGA fonts) + * // + * // License: + * // Public Domain + * + * Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm + **/ + +// Constant: font8x8_basic +// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin) +char font8x8_basic[128][8] = { + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space) + { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!) + { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (") + { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#) + { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($) + { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%) + { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&) + { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (') + { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (() + { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ()) + { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*) + { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,) + { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.) + { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/) + { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0) + { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1) + { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2) + { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3) + { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4) + { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5) + { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6) + { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7) + { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8) + { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9) + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:) + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;) + { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<) + { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=) + { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>) + { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?) + { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@) + { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A) + { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B) + { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C) + { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F) + { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G) + { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H) + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I) + { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J) + { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K) + { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L) + { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M) + { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N) + { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O) + { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P) + { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q) + { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R) + { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S) + { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V) + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W) + { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X) + { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y) + { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z) + { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([) + { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\) + { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (]) + { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_) + { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`) + { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a) + { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b) + { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c) + { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d) + { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e) + { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g) + { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h) + { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i) + { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j) + { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k) + { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l) + { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m) + { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n) + { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o) + { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q) + { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r) + { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s) + { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v) + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w) + { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y) + { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z) + { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({) + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|) + { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (}) + { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F +}; diff --git a/main.c b/main.c new file mode 100644 index 0000000..4f2b491 --- /dev/null +++ b/main.c @@ -0,0 +1,131 @@ +/* main.c - argument handling and wire-up. */ +#include "config.h" +#include "core.h" +#include "platform.h" + +#include +#include + +/* Canonical version lives in the Makefile (VERSION), injected via -D; a + * release build stamps the git tag in that way. */ +#ifndef MOUSEBOARD_VERSION +#define MOUSEBOARD_VERSION "0-dev" +#endif + +static const char usage[] = + "usage: mouseboard [options]\n" + "\n" + "Draw a labelled grid, type a label to pick a cell, refine by\n" + "bisection (u/i/j/k), then click (m=left , =middle .=right ;=double\n" + "v=drag). Esc cancels, Backspace undoes.\n" + "\n" + " --config PATH config file (default $XDG_CONFIG_HOME/mouseboard/config)\n" + " --cell-size N target coarse-cell size in pixels\n" + " --cols N --rows N explicit grid instead of cell-size\n" + " --alphabet STR label characters\n" + " --opacity N backdrop opacity 0..100 (100 = solid)\n" + " --opacity-fg N label/grid/crosshair opacity 0..100\n" + " --dry-run print 'x y action' instead of clicking\n" + " --selftest run offline logic checks and exit\n" + " -h, --help this help\n" + " -V, --version version\n"; + +/* Flags taking a value; each one sets a config key. */ +static const struct { const char *flag, *key; } opts[] = { + { "--cell-size", "cell_size" }, + { "--cols", "cols" }, + { "--rows", "rows" }, + { "--alphabet", "alphabet" }, + { "--opacity", "opacity_bg" }, + { "--opacity-fg", "opacity_fg" }, +}; + +int main(int argc, char **argv) +{ + platform_early_init(); + + struct config cfg; + config_defaults(&cfg); + + const char *cfgpath = NULL; /* set only by --config */ + int dry = 0; + + /* Pass 1: handle exit-early flags and locate the config path. */ + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (!strcmp(a, "-h") || !strcmp(a, "--help")) { + fputs(usage, stdout); + return 0; + } + if (!strcmp(a, "-V") || !strcmp(a, "--version")) { + printf("mouseboard %s\n", MOUSEBOARD_VERSION); + return 0; + } + if (!strcmp(a, "--selftest")) + return core_selftest(); + if (!strcmp(a, "--config")) { + if (++i >= argc) { + fprintf(stderr, + "mouseboard: %s needs an argument\n", a); + return 2; + } + cfgpath = argv[i]; + } + } + + /* Load the file (CLI flags below override it). */ + int lr = config_load_file(&cfg, + cfgpath ? cfgpath : config_default_path()); + if (lr == -1 && cfgpath) { + fprintf(stderr, "mouseboard: %s: not found\n", cfgpath); + return 2; + } + if (lr == -2) + return 2; + + /* Pass 2: apply CLI overrides. */ + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (!strcmp(a, "--config")) { + i++; + continue; + } + if (!strcmp(a, "--dry-run")) { + dry = 1; + continue; + } + const char *key = NULL; + for (size_t o = 0; o < sizeof opts / sizeof opts[0]; o++) + if (!strcmp(a, opts[o].flag)) + key = opts[o].key; + if (!key) { + fprintf(stderr, "mouseboard: unknown option %s\n", a); + fputs(usage, stderr); + return 2; + } + if (++i >= argc) { + fprintf(stderr, "mouseboard: %s needs an argument\n", a); + return 2; + } + if (config_set(&cfg, key, argv[i])) { + fprintf(stderr, "mouseboard: %s: bad value '%s'\n", a, + argv[i]); + return 2; + } + } + + const char *invalid = config_validate(&cfg); + if (invalid) { + fprintf(stderr, "mouseboard: invalid configuration: %s\n", invalid); + return 2; + } + config_apply_opacity(&cfg); + + struct platform *p = platform_init(); + if (!p) + return 2; + + int rc = core_run(p, &cfg, dry); + p->teardown(p); + return rc; +} diff --git a/mouseboard.1 b/mouseboard.1 new file mode 100644 index 0000000..40d57b8 --- /dev/null +++ b/mouseboard.1 @@ -0,0 +1,104 @@ +.TH MOUSEBOARD 1 "2026" "mouseboard" "User Commands" +.SH NAME +mouseboard \- keyboard-driven virtual pointer for Wayland +.SH SYNOPSIS +.B mouseboard +.RI [ options ] +.SH DESCRIPTION +.B mouseboard +draws a labelled grid over every monitor. Type a cell's label to warp the +pointer there, refine the position by repeatedly bisecting the cell, then press +a key to click. It performs one action and exits. +.PP +Wayland does not allow a client to grab a global hotkey, so bind +.B mouseboard +to a key inside your compositor. It requires a wlroots-based compositor that +implements the +.B wlr-layer-shell +and +.B wlr-virtual-pointer +protocols. +.SH OPTIONS +.TP +.BI \-\-config " PATH" +Read configuration from +.IR PATH +instead of the default +.IR $XDG_CONFIG_HOME/mouseboard/config . +.TP +.BI \-\-cell\-size " N" +Target coarse-cell size in pixels (cols/rows are derived per monitor). +.TP +.BI \-\-cols " N" "\fR, \fP" \-\-rows " N" +Use an explicit grid instead of +.BR \-\-cell\-size . +.TP +.BI \-\-alphabet " STR" +Characters used to build cell labels. +.TP +.BI \-\-opacity " N" +Backdrop opacity, 0 to 100; 100 is a solid backdrop. Default 33. +.TP +.BI \-\-opacity\-fg " N" +Opacity of the labels, grid and crosshair, 0 to 100. Default 65. +.TP +.B \-\-dry\-run +Print "x y action" to standard output instead of moving or clicking. +.TP +.B \-\-selftest +Run offline checks of the label, bisection and config logic, then exit. +.TP +.BR \-h ", " \-\-help +Print usage and exit. +.TP +.BR \-V ", " \-\-version +Print the version and exit. +.SH KEYS +Defaults; see +.B config.example +for how to change them. +.TP +.B labels +type a cell label to select it +.TP +.B u i j k +keep the top-left, top-right, bottom-left, bottom-right quadrant +.TP +.B "m , . ; v" +left, middle, right, double-click, drag +.TP +.B Enter +left click (once a cell is selected) +.TP +.B Backspace +undo the last split, or return to the grid +.TP +.B Esc +cancel without clicking +.SH EXIT STATUS +.TP +.B 0 +an action was performed (or printed, with +.BR \-\-dry\-run ). +.TP +.B 1 +cancelled by the user. +.TP +.B 2 +error (no display, unsupported compositor, bad arguments). +.SH FILES +.TP +.I $XDG_CONFIG_HOME/mouseboard/config +Per-user configuration. +.SH EXAMPLES +Bind it in sway: +.PP +.RS +bindsym $mod+g exec mouseboard +.RE +.SH SEE ALSO +.BR warpd (1), +.BR wl-kbptr (1) +.SH BUGS +On Linux only the wlroots Wayland backend exists; GNOME, KDE and X11 are not +yet supported. Windows is covered by a native Win32 backend. diff --git a/platform.h b/platform.h new file mode 100644 index 0000000..ecddb28 --- /dev/null +++ b/platform.h @@ -0,0 +1,79 @@ +/* + * platform.h - the backend interface. + * + * The core talks only to this interface and never includes an OS header. + * Exactly one backend is linked into the binary, so there is a single + * platform_init() symbol and main.c needs no #ifdef. + * + * All coordinates are global, logical pixels: the union of every screen laid + * out in the display server's coordinate space. + */ +#ifndef MOUSEBOARD_PLATFORM_H +#define MOUSEBOARD_PLATFORM_H + +#include + +#define MB_MAX_SCREENS 16 + +/* One monitor, positioned in the global coordinate space. */ +struct mb_screen { + int x, y, w, h; +}; + +/* A drawable overlay surface for one screen, ARGB8888, native byte order + * (0xAARRGGBB). w,h are LOGICAL pixels - the core draws in these and stays + * scale-agnostic. The backing store is w*scale by h*scale device pixels; + * stride is device pixels per row. The renderer multiplies by scale. */ +struct mb_buffer { + uint32_t *px; + int w, h; /* logical size */ + int stride; /* device pixels per row */ + int scale; /* integer output scale (1 = non-HiDPI) */ +}; + +enum mb_button { MB_LEFT = 0, MB_MIDDLE = 1, MB_RIGHT = 2 }; + +/* Keys handed to the core. Printable input arrives as its ASCII code + * (0x20..0x7e); nothing else reaches the core except these. */ +enum { + MB_KEY_NONE = 0, /* overlay closed / connection lost */ + MB_KEY_ESC = 0x01000000, + MB_KEY_BACKSPACE, + MB_KEY_ENTER, +}; + +struct platform { + int n_screens; + struct mb_screen screens[MB_MAX_SCREENS]; + + /* Return screen i's overlay buffer, cleared to fully transparent. */ + struct mb_buffer *(*frame_begin)(struct platform *, int screen); + /* Present every buffer drawn since the last commit. */ + int (*frame_commit)(struct platform *); + /* Block until the next key; returns an ASCII code or MB_KEY_*. */ + uint32_t (*next_key)(struct platform *); + /* Warp the pointer to a global position. */ + int (*pointer_move)(struct platform *, int x, int y); + /* Press (press=1) or release (press=0) a button. */ + int (*pointer_button)(struct platform *, enum mb_button, int press); + /* Tear down the overlay and disconnect. */ + void (*teardown)(struct platform *); + + void *priv; +}; + +/* Run once at process start, before any argument parsing or output: the + * Windows backend attaches to a parent console there so --help and --dry-run + * still print from a terminal. A no-op elsewhere. */ +void platform_early_init(void); + +/* Bring up the backend: connect to the display server, create the fullscreen + * overlay surfaces, and grab the keyboard. Returns NULL on failure (prints why + * to stderr). */ +struct platform *platform_init(void); + +/* Sleep for ms milliseconds; used only to space out synthesized button + * presses. Provided by the backend so the core needs no OS header. */ +void platform_sleep_ms(int ms); + +#endif /* MOUSEBOARD_PLATFORM_H */ diff --git a/proto/SOURCES b/proto/SOURCES new file mode 100644 index 0000000..fe455a9 --- /dev/null +++ b/proto/SOURCES @@ -0,0 +1,29 @@ +Vendored Wayland protocol descriptions. These are committed verbatim so the +build is self-contained and reproducible; update them deliberately, not by +re-fetching a moving branch. + +xdg-shell.xml + upstream: wayland-protocols, stable/xdg-shell/xdg-shell.xml + version: 1.44 (xdg_wm_base version 7) + license: MIT/Expat + note: only needed because wlr-layer-shell references xdg_popup. + +xdg-output-unstable-v1.xml + upstream: wayland-protocols, unstable/xdg-output/xdg-output-unstable-v1.xml + version: 1.44 (zxdg_output_manager_v1 version 3) + license: MIT/Expat + note: authoritative logical output geometry for multi-monitor. + +wlr-layer-shell-unstable-v1.xml + upstream: https://gitlab.freedesktop.org/wlroots/wlr-protocols + path: unstable/wlr-layer-shell-unstable-v1.xml + commit: bf4fc79abc359eea5a0edec0ac6d4a2b2955f82a (2026-03-09) + version: zwlr_layer_shell_v1 version 5 + license: MIT/Expat + +wlr-virtual-pointer-unstable-v1.xml + upstream: https://gitlab.freedesktop.org/wlroots/wlr-protocols + path: unstable/wlr-virtual-pointer-unstable-v1.xml + commit: bf4fc79abc359eea5a0edec0ac6d4a2b2955f82a (2026-03-09) + version: zwlr_virtual_pointer_v1 version 2 + license: MIT/Expat diff --git a/proto/wlr-layer-shell-unstable-v1.xml b/proto/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..e9f27e4 --- /dev/null +++ b/proto/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,407 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/proto/wlr-virtual-pointer-unstable-v1.xml b/proto/wlr-virtual-pointer-unstable-v1.xml new file mode 100644 index 0000000..ea243e7 --- /dev/null +++ b/proto/wlr-virtual-pointer-unstable-v1.xml @@ -0,0 +1,152 @@ + + + + Copyright © 2019 Josef Gajdusek + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This protocol allows clients to emulate a physical pointer device. The + requests are mostly mirror opposites of those specified in wl_pointer. + + + + + + + + + + The pointer has moved by a relative amount to the previous request. + + Values are in the global compositor space. + + + + + + + + + The pointer has moved in an absolute coordinate frame. + + Value of x can range from 0 to x_extent, value of y can range from 0 + to y_extent. + + + + + + + + + + + A button was pressed or released. + + + + + + + + + Scroll and other axis requests. + + + + + + + + + Indicates the set of events that logically belong together. + + + + + + Source information for scroll and other axis. + + + + + + + Stop notification for scroll and other axes. + + + + + + + + Discrete step information for scroll and other axes. + + This event allows the client to extend data normally sent using the axis + event with discrete value. + + + + + + + + + + + + + + + This object allows clients to create individual virtual pointer objects. + + + + + Creates a new virtual pointer. The optional seat is a suggestion to the + compositor. + + + + + + + + + + + + + Creates a new virtual pointer. The seat and the output arguments are + optional. If the seat argument is set, the compositor should assign the + input device to the requested seat. If the output argument is set, the + compositor should map the input device to the requested output. + + + + + + + diff --git a/proto/xdg-output-unstable-v1.xml b/proto/xdg-output-unstable-v1.xml new file mode 100644 index 0000000..a7306e4 --- /dev/null +++ b/proto/xdg-output-unstable-v1.xml @@ -0,0 +1,222 @@ + + + + + Copyright © 2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol aims at describing outputs in a way which is more in line + with the concept of an output on desktop oriented systems. + + Some information are more specific to the concept of an output for + a desktop oriented system and may not make sense in other applications, + such as IVI systems for example. + + Typically, the global compositor space on a desktop system is made of + a contiguous or overlapping set of rectangular regions. + + The logical_position and logical_size events defined in this protocol + might provide information identical to their counterparts already + available from wl_output, in which case the information provided by this + protocol should be preferred to their equivalent in wl_output. The goal is + to move the desktop specific concepts (such as output location within the + global compositor space, etc.) out of the core wl_output protocol. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + A global factory interface for xdg_output objects. + + + + + Using this request a client can tell the server that it is not + going to use the xdg_output_manager object anymore. + + Any objects already created through this instance are not affected. + + + + + + This creates a new xdg_output object for the given wl_output. + + + + + + + + + An xdg_output describes part of the compositor geometry. + + This typically corresponds to a monitor that displays part of the + compositor space. + + For objects version 3 onwards, after all xdg_output properties have been + sent (when the object is created and when properties are updated), a + wl_output.done event is sent. This allows changes to the output + properties to be seen as atomic, even if they happen via multiple events. + + + + + Using this request a client can tell the server that it is not + going to use the xdg_output object anymore. + + + + + + The position event describes the location of the wl_output within + the global compositor space. + + The logical_position event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the location + of the output changes within the global compositor space. + + + + + + + + The logical_size event describes the size of the output in the + global compositor space. + + Most regular Wayland clients should not pay attention to the + logical size and would rather rely on xdg_shell interfaces. + + Some clients such as Xwayland, however, need this to configure + their surfaces in the global compositor space as the compositor + may apply a different scale from what is advertised by the output + scaling property (to achieve fractional scaling, for example). + + For example, for a wl_output mode 3840×2160 and a scale factor 2: + + - A compositor not scaling the monitor viewport in its compositing space + will advertise a logical size of 3840×2160, + + - A compositor scaling the monitor viewport with scale factor 2 will + advertise a logical size of 1920×1080, + + - A compositor scaling the monitor viewport using a fractional scale of + 1.5 will advertise a logical size of 2560×1440. + + For example, for a wl_output mode 1920×1080 and a 90 degree rotation, + the compositor will advertise a logical size of 1080x1920. + + The logical_size event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the logical + size of the output changes, either as a result of a change in the + applied scale or because of a change in the corresponding output + mode(see wl_output.mode) or transform (see wl_output.transform). + + + + + + + + This event is sent after all other properties of an xdg_output + have been sent. + + This allows changes to the xdg_output properties to be seen as + atomic, even if they happen via multiple events. + + For objects version 3 onwards, this event is deprecated. Compositors + are not required to send it anymore and must send wl_output.done + instead. + + + + + + + + Many compositors will assign names to their outputs, show them to the + user, allow them to be configured by name, etc. The client may wish to + know this name as well to offer the user similar behaviors. + + The naming convention is compositor defined, but limited to + alphanumeric characters and dashes (-). Each name is unique among all + wl_output globals, but if a wl_output global is destroyed the same name + may be reused later. The names will also remain consistent across + sessions with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + The name event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output). This event is only sent once per + xdg_output, and the name does not change over the lifetime of the + wl_output global. + + This event is deprecated, instead clients should use wl_output.name. + Compositors must still support this event. + + + + + + + Many compositors can produce human-readable descriptions of their + outputs. The client may wish to know this description as well, to + communicate the user for various purposes. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. + + The description event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output) and whenever the description + changes. The description is optional, and may not be sent at all. + + For objects of version 2 and lower, this event is only sent once per + xdg_output, and the description does not change over the lifetime of + the wl_output global. + + This event is deprecated, instead clients should use + wl_output.description. Compositors must still support this event. + + + + + + diff --git a/proto/xdg-shell.xml b/proto/xdg-shell.xml new file mode 100644 index 0000000..c4d4685 --- /dev/null +++ b/proto/xdg-shell.xml @@ -0,0 +1,1415 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. While xdg_surface + itself is not a role, the corresponding surface may only be assigned + a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + illegal to create an xdg_surface for a wl_surface which already has an + assigned role and this will result in a role error. + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up (e.g. by sending + the title, app ID, size constraints, parent, etc), the client must + perform an initial commit without any buffer attached. The compositor + will reply with initial wl_surface state such as + wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + event. The client must acknowledge it and is then allowed to attach a + buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. + + The window geometry is double-buffered state, see wl_surface.commit. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface, and may extend outside + of the wl_surface itself to mark parts of the subsurface tree as part of + the window geometry. + + When applied, the effective window geometry will be the set window + geometry clamped to the bounding rectangle of the combined + geometry of the surface of the xdg_surface and the associated + subsurfaces. + + The effective geometry will not be recalculated unless a new call to + set_window_geometry is done and the new pending surface state is + subsequently applied. + + The width and height of the effective window geometry must be + greater than zero. Setting an invalid size will raise an + invalid_size error. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + A xdg_toplevel by default is responsible for providing the full intended + visual representation of the toplevel, which depending on the window + state, may mean things like a title bar, window controls and drop shadow. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by performing a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause the invalid_resize_edge protocol error. + The compositor may use this information to update the surface position + for example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered, see wl_surface.commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client, or the xdg_wm_base.invalid_surface_state + error is raised. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the left edge. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the right edge. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the top edge. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the bottom edge. + + + + + The surface is currently not ordinarily being repainted; for + example because its content is occluded by another window, or its + outputs are switched off due to screen locking. + + + + + The left edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The right edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The top edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The bottom edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in a + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + with border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, the + xdg_wm_base.not_the_topmost_popup protocol error will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + diff --git a/provision-static.sh b/provision-static.sh new file mode 100755 index 0000000..c2125ee --- /dev/null +++ b/provision-static.sh @@ -0,0 +1,190 @@ +#!/bin/sh +# provision-static.sh - build the static libraries mouseboard needs. +# +# Usage: ./provision-static.sh +# +# Debian (and friends) ship no static libwayland-client or libxkbcommon, so +# this fetches and builds them (plus libffi) with a musl toolchain into +# musl-sysroot/, ready for `make static`. Run it once per machine; an unchanged +# completed sysroot is reused, while changed inputs trigger a clean rebuild. +# +# Prerequisites (Debian): +# apt install musl-tools meson ninja-build bison pkgconf curl libexpat1-dev +set -eu + +ROOT="$(cd "$(dirname "$0")" && pwd)" +SYSROOT="$ROOT/musl-sysroot" +WORK="$ROOT/musl-build" +HOSTCC="${HOSTCC:-cc}" + +# Pick the musl target compiler. An explicit CC always wins. Otherwise, if the +# default cc already targets musl (Alpine, Void-musl) use it directly; on a +# glibc host (Debian, Arch) fall back to the musl-gcc wrapper from musl-tools. +if [ -n "${CC:-}" ]; then + BASECC="$CC" +elif command -v cc >/dev/null 2>&1 && cc -dumpmachine 2>/dev/null | grep -q musl; then + BASECC="cc" +else + BASECC="musl-gcc" +fi + +# Pinned versions and checksums. Override a version only together with its sha. +FFI_VER="${FFI_VER:-3.4.6}" +FFI_SHA="${FFI_SHA:-b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e}" +WL_VER="${WL_VER:-1.23.1}" +WL_SHA="${WL_SHA:-864fb2a8399e2d0ec39d56e9d9b753c093775beadc6022ce81f441929a81e5ed}" +XKB_VER="${XKB_VER:-1.7.0}" +XKB_SHA="${XKB_SHA:-65782f0a10a4b455af9c6baab7040e2f537520caa2ec2092805cdfd36863b247}" + +command -v "$BASECC" >/dev/null 2>&1 || { + echo "missing musl compiler: $BASECC" >&2 + echo " glibc host (Debian, Arch): install musl-tools for musl-gcc" >&2 + echo " or point CC at your musl compiler: CC=... $0" >&2 + exit 1 +} +for t in "$HOSTCC" meson ninja bison curl pkg-config sha256sum \ + tar make nproc sed; do + command -v "$t" >/dev/null 2>&1 || { echo "missing tool: $t" >&2; exit 1; } +done +JOBS="$(nproc)" + +# wayland-scanner is built from source below and parses XML with expat. +pkg-config --exists expat || { + echo "missing dev library: expat (Debian: libexpat1-dev)" >&2; exit 1; } + +BASECC_PATH="$(command -v "$BASECC")" +HOSTCC_PATH="$(command -v "$HOSTCC")" +BASECC_VERSION="$("$BASECC" --version | sed -n '1p')" +HOSTCC_VERSION="$("$HOSTCC" --version | sed -n '1p')" +MULTIARCH="$("$BASECC" -print-multiarch 2>/dev/null || echo x86_64-linux-gnu)" +SCRIPT_SHA="$(sha256sum "$0" | sed 's/ .*//')" +STAMP="$SYSROOT/.provisioned" +INPUTS="recipe=$SCRIPT_SHA +ffi=$FFI_VER $FFI_SHA +wayland=$WL_VER $WL_SHA +xkbcommon=$XKB_VER $XKB_SHA +target-cc=$BASECC_PATH +target-cc-version=$BASECC_VERSION +host-cc=$HOSTCC_PATH +host-cc-version=$HOSTCC_VERSION +multiarch=$MULTIARCH +cflags=${CFLAGS:-} +cppflags=${CPPFLAGS:-} +ldflags=${LDFLAGS:-}" + +sysroot_complete() { + [ -x "$SYSROOT/bin/cc" ] && + [ -x "$SYSROOT/bin/wayland-scanner" ] && + [ -f "$SYSROOT/lib/libffi.a" ] && + [ -f "$SYSROOT/lib/libwayland-client.a" ] && + [ -f "$SYSROOT/lib/libxkbcommon.a" ] && + [ -f "$SYSROOT/lib/pkgconfig/libffi.pc" ] && + [ -f "$SYSROOT/lib/pkgconfig/wayland-client.pc" ] && + [ -f "$SYSROOT/lib/pkgconfig/xkbcommon.pc" ] && + [ -f "$SYSROOT/include/wayland-client.h" ] && + [ -f "$SYSROOT/include/xkbcommon/xkbcommon.h" ] +} + +if [ -d "$SYSROOT" ]; then + OLD_INPUTS="" + [ ! -f "$STAMP" ] || OLD_INPUTS="$(cat "$STAMP")" + if [ "$OLD_INPUTS" != "$INPUTS" ] || ! sysroot_complete; then + echo "static sysroot stale or incomplete; rebuilding $SYSROOT" + rm -rf "$SYSROOT" + fi +fi + +# Fetch a file if absent, then verify its checksum (fails loud on mismatch). +fetch() { # url sha + file="$(basename "$1")" + [ -f "$file" ] || curl -fsSLO "$1" + echo "$2 $file" | sha256sum -c - || { rm -f "$file"; exit 1; } +} + +# Sysroot libs resolve first while building the deps themselves (wayland +# needs the sysroot's libffi.pc). +export PKG_CONFIG_PATH="$SYSROOT/lib/pkgconfig:$SYSROOT/share/pkgconfig:${PKG_CONFIG_PATH:-}" + +mkdir -p "$WORK" "$SYSROOT/bin" + +# musl-gcc only exposes musl's libc headers, not the Linux kernel uapi headers +# (linux/*, asm/*) from linux-libc-dev. Wrap it to add them at lowest priority +# so musl's libc headers still win. The wrapper lands in musl-sysroot/bin/cc; +# `make static` compiles with it too. On musl-native distros the extra +# -idirafter flags are harmless. +MCC="$SYSROOT/bin/cc" +printf '#!/bin/sh\nexec "%s" -idirafter /usr/include -idirafter /usr/include/%s "$@"\n' \ + "$BASECC_PATH" "$MULTIARCH" > "$MCC" +chmod +x "$MCC" + +cd "$WORK" + +# ---- libffi (autotools): wayland's marshalling needs it ---- +if [ ! -f "$SYSROOT/lib/pkgconfig/libffi.pc" ]; then + fetch "https://github.com/libffi/libffi/releases/download/v$FFI_VER/libffi-$FFI_VER.tar.gz" "$FFI_SHA" + rm -rf "libffi-$FFI_VER" + tar xf "libffi-$FFI_VER.tar.gz" + cd "libffi-$FFI_VER" + ./configure CC="$MCC" --prefix="$SYSROOT" --libdir="$SYSROOT/lib" \ + --disable-shared --enable-static --disable-docs + make -j"$JOBS" + make install + cd "$WORK" +fi + +# ---- wayland-scanner (native host tool, pinned to WL_VER) ---- +# wayland's library build needs a wayland-scanner at least as new as the +# library, but distros ship older ones (CI had 1.22.0 vs 1.23.1). Build it +# from the pinned source with the host compiler (glibc + host expat) into the +# sysroot, so the version always matches and nothing depends on the host copy. +# The static mouseboard build uses this scanner too (see Makefile). +if [ ! -x "$SYSROOT/bin/wayland-scanner" ]; then + fetch "https://gitlab.freedesktop.org/wayland/wayland/-/releases/$WL_VER/downloads/wayland-$WL_VER.tar.xz" "$WL_SHA" + rm -rf "wayland-$WL_VER" wl-scanner-build + tar xf "wayland-$WL_VER.tar.xz" + CC="$HOSTCC_PATH" meson setup "wayland-$WL_VER" wl-scanner-build \ + --prefix="$SYSROOT" --libdir=lib --buildtype=release \ + -Dscanner=true -Dlibraries=false -Dtests=false \ + -Ddocumentation=false -Ddtd_validation=false + ninja -C wl-scanner-build + ninja -C wl-scanner-build install +fi + +# ---- wayland (libraries, static/musl; uses the scanner built above) ---- +if [ ! -f "$SYSROOT/lib/pkgconfig/wayland-client.pc" ]; then + fetch "https://gitlab.freedesktop.org/wayland/wayland/-/releases/$WL_VER/downloads/wayland-$WL_VER.tar.xz" "$WL_SHA" + rm -rf "wayland-$WL_VER" wl-build + tar xf "wayland-$WL_VER.tar.xz" + CC="$MCC" meson setup "wayland-$WL_VER" wl-build \ + --prefix="$SYSROOT" --libdir=lib -Ddefault_library=static \ + --buildtype=release \ + -Dscanner=false -Dlibraries=true -Dtests=false \ + -Ddocumentation=false -Ddtd_validation=false + ninja -C wl-build + ninja -C wl-build install +fi + +# ---- libxkbcommon (no x11, registry, tools, docs -> no extra deps) ---- +if [ ! -f "$SYSROOT/lib/pkgconfig/xkbcommon.pc" ]; then + fetch "https://xkbcommon.org/download/libxkbcommon-$XKB_VER.tar.xz" "$XKB_SHA" + rm -rf "libxkbcommon-$XKB_VER" xkb-build + tar xf "libxkbcommon-$XKB_VER.tar.xz" + CC="$MCC" meson setup "libxkbcommon-$XKB_VER" xkb-build \ + --prefix="$SYSROOT" --libdir=lib -Ddefault_library=static \ + --buildtype=release \ + -Denable-x11=false -Denable-docs=false -Denable-xkbregistry=false \ + -Denable-tools=false -Denable-bash-completion=false + ninja -C xkb-build + ninja -C xkb-build install +fi + +if ! sysroot_complete; then + echo "static sysroot incomplete after provisioning" >&2 + exit 1 +fi +printf '%s\n' "$INPUTS" >"$STAMP.tmp" +mv "$STAMP.tmp" "$STAMP" + +echo +echo "sysroot ready: $SYSROOT" +echo "build the static binary with: make static" diff --git a/selftest.c b/selftest.c new file mode 100644 index 0000000..906f3f6 --- /dev/null +++ b/selftest.c @@ -0,0 +1,12 @@ +/* selftest.c - dependency-free entry point for the shared core checks. */ +#include "core.h" + +void platform_sleep_ms(int ms) +{ + (void)ms; +} + +int main(void) +{ + return core_selftest(); +} -- cgit v1.2.3