trycua--cua
91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
711 行
33 KiB
Python
711 行
33 KiB
Python
#!/usr/bin/env python3
|
|
# Transforms wlroots' upstream `tinywl/tinywl.c` (0.19) into `cua-compositor.c`:
|
|
# a minimal headless wlroots compositor that cua-driver spawns as its nested
|
|
# session (CUA_WAYLAND_NEST_COMPOSITOR=cua-compositor) to perform what stock
|
|
# Wayland forbids a client from doing —
|
|
#
|
|
# * focus-FREE per-surface KEYBOARD injection (type into an unfocused window),
|
|
# * MULTI-cursor pointer injection (N independent cursors on one window),
|
|
#
|
|
# both routed by stable process identity (with app_id fallback), driven over a tiny line
|
|
# protocol on a unix control socket ($CUA_INJECT_SOCKET). It also exposes
|
|
# foreign-toplevel-management (so cua-driver's list_windows enumerates windows)
|
|
# and screencopy (so grim captures the output) — the same protocols labwc gives
|
|
# the non-nested cells — so the nested-injection cells keep working end to end.
|
|
#
|
|
# The injection primitives (cua_motion/cua_button/cua_kbd_*) deliver wl_pointer/
|
|
# wl_keyboard straight to a target client's resources, bypassing seat focus —
|
|
# routed by process identity rather than a connection-local object id and transported over a plain
|
|
# socket rather than libei/EIS, since cua owns both ends and the portal/libei
|
|
# layer buys nothing here. The socket speaks a versioned v1 line protocol: the
|
|
# client sends the `cua-inject v1` banner (echoed back on match), then every
|
|
# command line is answered by exactly one `ok` / `err <reason>` acknowledgement.
|
|
#
|
|
# Usage: cua_compositor_patch.py <tinywl.c in> <cua-compositor.c out>
|
|
import sys, io
|
|
|
|
inp = sys.argv[1] if len(sys.argv) > 1 else "tinywl.c"
|
|
out = sys.argv[2] if len(sys.argv) > 2 else "cua-compositor.c"
|
|
src = io.open(inp, encoding="utf-8").read()
|
|
|
|
# memfd_create + SOCK_CLOEXEC etc. need _GNU_SOURCE before any system header.
|
|
src = "#define _GNU_SOURCE\n" + src
|
|
|
|
INCLUDES = r"""#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <linux/input-event-codes.h>
|
|
#include <wayland-server-protocol.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <wlr/types/wlr_foreign_toplevel_management_v1.h>
|
|
#include <wlr/types/wlr_screencopy_v1.h>
|
|
#include <wlr/types/wlr_xdg_output_v1.h>
|
|
|
|
#define CUA_MAXDEV 64
|
|
/* Per-cursor / per-keyboard enter bookkeeping (idx = logical device). */
|
|
struct cua_devstate { struct wlr_surface *entered; };
|
|
static struct cua_devstate cua_ptr[CUA_MAXDEV];
|
|
static struct cua_devstate cua_kbd_state[CUA_MAXDEV];
|
|
static int g_keymap_fd = -1;
|
|
static size_t g_keymap_size = 0;
|
|
static xkb_mod_mask_t g_shift_mask = 1;
|
|
static xkb_mod_mask_t g_ctrl_mask = 0;
|
|
static xkb_mod_mask_t g_alt_mask = 0;
|
|
static xkb_mod_mask_t g_logo_mask = 0;
|
|
struct cua_keyent { uint32_t keycode; int shift; int valid; };
|
|
static struct cua_keyent g_chartab[128];
|
|
static struct wlr_foreign_toplevel_manager_v1 *g_ftl_mgr = NULL;
|
|
static void cua_ftl_request_activate(struct wl_listener *listener, void *data);
|
|
|
|
"""
|
|
|
|
# A foreign-toplevel handle pointer on each toplevel (for list_windows).
|
|
STRUCT_FIELD = (
|
|
"\tstruct wlr_xdg_toplevel *xdg_toplevel;\n"
|
|
"\tstruct wlr_foreign_toplevel_handle_v1 *ftl;\n"
|
|
"\tstruct wl_listener ftl_request_activate;\n"
|
|
)
|
|
|
|
FUNCS = r"""
|
|
/* v1 control-protocol banner: the client sends this line, the compositor echoes
|
|
* it to confirm both speak v1. Any other first line is refused. */
|
|
#define CUA_PROTO_HELLO "cua-inject v1"
|
|
static void cua_ftl_request_activate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct tinywl_toplevel *t = wl_container_of(listener, t, ftl_request_activate);
|
|
focus_toplevel(t);
|
|
}
|
|
static uint32_t cua_now_ms(void) {
|
|
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
|
}
|
|
/* Write a single acknowledgement line back to the control client. Best-effort:
|
|
* a dead peer is torn down by the read side on the next loop iteration. */
|
|
static void cua_reply(int fd, const char *line) {
|
|
char buf[256];
|
|
int n = snprintf(buf, sizeof buf, "%s\n", line);
|
|
if (n < 0) return;
|
|
if (n >= (int)sizeof buf) n = (int)sizeof buf - 1;
|
|
ssize_t off = 0;
|
|
while (off < n) {
|
|
ssize_t w = write(fd, buf + off, (size_t)n - (size_t)off);
|
|
if (w <= 0) break;
|
|
off += w;
|
|
}
|
|
}
|
|
static void cua_pframe(struct wl_resource *res) {
|
|
if (wl_resource_get_version(res) >= WL_POINTER_FRAME_SINCE_VERSION)
|
|
wl_pointer_send_frame(res);
|
|
}
|
|
static pid_t cua_toplevel_pid(struct tinywl_toplevel *t);
|
|
/* Resolve a target window by its xdg app_id, refusing missing and ambiguous
|
|
* matches so a command never silently drives the wrong window. In v1 duplicate
|
|
* app_ids are simply not addressable. On failure returns NULL and points *err
|
|
* at a stable reason token; on success *err is left untouched. */
|
|
static struct tinywl_toplevel *cua_resolve_target(struct tinywl_server *server, const char *app_id, const char **err) {
|
|
struct tinywl_toplevel *t, *found = NULL;
|
|
int matches = 0;
|
|
if (!strncmp(app_id, "pid:", 4)) {
|
|
char *end = NULL;
|
|
long pid = strtol(app_id + 4, &end, 10);
|
|
if (pid <= 0 || !end || *end) { *err = "bad-pid"; return NULL; }
|
|
wl_list_for_each(t, &server->toplevels, link) {
|
|
if (cua_toplevel_pid(t) == (pid_t)pid) { found = t; matches++; }
|
|
}
|
|
if (matches == 0) { *err = "unknown-pid"; return NULL; }
|
|
if (matches > 1) { *err = "ambiguous-pid"; return NULL; }
|
|
return found;
|
|
}
|
|
wl_list_for_each(t, &server->toplevels, link) {
|
|
const char *a = t->xdg_toplevel ? t->xdg_toplevel->app_id : NULL;
|
|
if (a && strcmp(a, app_id) == 0) { found = t; matches++; }
|
|
}
|
|
if (matches == 0) { *err = "unknown-app-id"; return NULL; }
|
|
if (matches > 1) { *err = "ambiguous-app-id"; return NULL; }
|
|
return found;
|
|
}
|
|
static pid_t cua_toplevel_pid(struct tinywl_toplevel *t) {
|
|
if (!t || !t->xdg_toplevel || !t->xdg_toplevel->base->surface) return 0;
|
|
struct wl_client *client = wl_resource_get_client(t->xdg_toplevel->base->surface->resource);
|
|
pid_t pid = 0; uid_t uid = 0; gid_t gid = 0;
|
|
wl_client_get_credentials(client, &pid, &uid, &gid);
|
|
return pid;
|
|
}
|
|
/* Focus exactly one mapped toplevel owned by `target_pid`. Refuse ambiguity:
|
|
* process-scoped activation is only safe when the process owns one window. */
|
|
static const char *cua_activate_pid(struct tinywl_server *server, pid_t target_pid) {
|
|
struct tinywl_toplevel *t, *found = NULL;
|
|
int matches = 0;
|
|
wl_list_for_each(t, &server->toplevels, link) {
|
|
if (target_pid > 0 && cua_toplevel_pid(t) == target_pid) {
|
|
found = t;
|
|
matches++;
|
|
}
|
|
}
|
|
if (matches == 0) return "unknown-pid";
|
|
if (matches > 1) return "ambiguous-pid";
|
|
focus_toplevel(found);
|
|
/* tinywl only notifies seat keyboard focus when a physical wlr_keyboard is
|
|
* attached. Headless CI has none, so establish the logical focus explicitly
|
|
* for observer truth and client activation semantics. */
|
|
struct wlr_surface *surface = found->xdg_toplevel->base->surface;
|
|
if (server->seat->keyboard_state.focused_surface != surface) {
|
|
struct wlr_keyboard_modifiers modifiers = {0};
|
|
wlr_seat_keyboard_notify_enter(server->seat, surface, NULL, 0, &modifiers);
|
|
}
|
|
return NULL;
|
|
}
|
|
/* Independent observer query used only by the Rust E2E testkit. The target is
|
|
* selected by the Wayland client's process credentials, not by driver-owned
|
|
* object ids. In this minimal compositor every mapped toplevel shares origin;
|
|
* a non-focused target beneath another focused surface is therefore occluded. */
|
|
static void cua_query_state(struct tinywl_server *server, pid_t target_pid, char *out, size_t out_len) {
|
|
struct wlr_surface *focused = server->seat->keyboard_state.focused_surface;
|
|
struct wlr_surface *focused_root = focused ? wlr_surface_get_root_surface(focused) : NULL;
|
|
struct tinywl_toplevel *t, *target = NULL, *focused_toplevel = NULL;
|
|
wl_list_for_each(t, &server->toplevels, link) {
|
|
struct wlr_surface *surface = t->xdg_toplevel ? t->xdg_toplevel->base->surface : NULL;
|
|
if (surface == focused_root) focused_toplevel = t;
|
|
if (target_pid > 0 && cua_toplevel_pid(t) == target_pid) target = t;
|
|
}
|
|
pid_t focused_pid = cua_toplevel_pid(focused_toplevel);
|
|
const char *state = !target ? "not_found" :
|
|
(target == focused_toplevel ? "foreground" :
|
|
(focused_toplevel ? "background_occluded" : "background_visible"));
|
|
snprintf(out, out_len, "state %d %s", (int)focused_pid, state);
|
|
}
|
|
static const char *cua_query_geometry(struct tinywl_server *server, pid_t target_pid, char *out, size_t out_len) {
|
|
struct tinywl_toplevel *t, *target = NULL;
|
|
int matches = 0;
|
|
wl_list_for_each(t, &server->toplevels, link) {
|
|
if (cua_toplevel_pid(t) == target_pid) { target = t; matches++; }
|
|
}
|
|
if (!matches) return "target-not-found";
|
|
if (matches > 1) return "ambiguous-pid";
|
|
int x = 0, y = 0;
|
|
if (!wlr_scene_node_coords(&target->scene_tree->node, &x, &y)) return "unmapped-target";
|
|
/* AT-SPI Window coordinates for native GTK are rooted at the xdg window
|
|
* geometry, while scene coordinates and screencopy include the complete root
|
|
* surface (including client-side decorations). Rebase into that coordinate
|
|
* system so `origin + accessible_window_xy` lands on the captured pixel. */
|
|
int scene_x = x, scene_y = y;
|
|
struct wlr_box geo = target->xdg_toplevel->base->geometry;
|
|
x -= geo.x; y -= geo.y;
|
|
snprintf(out, out_len, "geometry %d %d %d %d", x, y, scene_x, scene_y);
|
|
return NULL;
|
|
}
|
|
static void cua_ptr_leave(struct wlr_seat *seat, struct wlr_surface *surf) {
|
|
if (!surf) return;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(seat, wl_resource_get_client(surf->resource));
|
|
if (!sc) return;
|
|
uint32_t serial = wlr_seat_client_next_serial(sc);
|
|
struct wl_resource *res;
|
|
wl_resource_for_each(res, &sc->pointers) { wl_pointer_send_leave(res, serial, surf->resource); cua_pframe(res); }
|
|
}
|
|
/* Inject pointer motion from logical cursor `idx` into window `t` at window-
|
|
* local (x,y). enter/leave is tracked per idx, so several idx values can drive
|
|
* independent cursors against the same or different surfaces. Focus-free: we
|
|
* write straight to the client's wl_pointer resources, never touching the seat
|
|
* focus or any real cursor. */
|
|
static bool cua_motion(struct tinywl_server *server, struct tinywl_toplevel *t, int idx, double x, double y) {
|
|
if (!t || idx < 0 || idx >= CUA_MAXDEV) return false;
|
|
/* Public PX coordinates come from the cropped root-surface screenshot. Hit
|
|
* test that point through the scene so Chromium/WebKit child surfaces receive
|
|
* enter/motion in their own local coordinates instead of the top-level root. */
|
|
int scene_x = 0, scene_y = 0;
|
|
if (!wlr_scene_node_coords(&t->scene_tree->node, &scene_x, &scene_y)) return false;
|
|
double local_x = 0, local_y = 0;
|
|
/* Search only the requested toplevel's scene subtree. A global hit test
|
|
* would select the foreground sentinel when this target is occluded. */
|
|
struct wlr_scene_node *node = wlr_scene_node_at(&t->scene_tree->node,
|
|
scene_x + x, scene_y + y, &local_x, &local_y);
|
|
if (!node || node->type != WLR_SCENE_NODE_BUFFER) return false;
|
|
struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node);
|
|
struct wlr_scene_surface *scene_surface = wlr_scene_surface_try_from_buffer(buffer);
|
|
if (!scene_surface) return false;
|
|
struct wlr_surface *surface = scene_surface->surface;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->pointers)) {
|
|
/* Chromium can compose a renderer-owned child surface whose wl_client
|
|
* never bound wl_pointer while the owning toplevel client did. Use that
|
|
* target root while retaining the caller's screenshot-local point. */
|
|
surface = t->xdg_toplevel->base->surface;
|
|
local_x = x; local_y = y;
|
|
sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->pointers)) return false;
|
|
}
|
|
wl_fixed_t sx = wl_fixed_from_double(local_x), sy = wl_fixed_from_double(local_y);
|
|
struct wl_resource *res;
|
|
if (cua_ptr[idx].entered != surface) {
|
|
if (cua_ptr[idx].entered) cua_ptr_leave(server->seat, cua_ptr[idx].entered);
|
|
uint32_t es = wlr_seat_client_next_serial(sc);
|
|
wl_resource_for_each(res, &sc->pointers) { wl_pointer_send_enter(res, es, surface->resource, sx, sy); cua_pframe(res); }
|
|
cua_ptr[idx].entered = surface;
|
|
}
|
|
uint32_t tm = cua_now_ms();
|
|
wl_resource_for_each(res, &sc->pointers) { wl_pointer_send_motion(res, tm, sx, sy); cua_pframe(res); }
|
|
return true;
|
|
}
|
|
/* Resolve an output-layout point through the compositor scene, preserving
|
|
* subsurface offsets and output scaling. This is the desktop-scope path. */
|
|
static struct tinywl_toplevel *cua_desktop_motion(struct tinywl_server *server, double x, double y) {
|
|
double sx = 0, sy = 0;
|
|
struct wlr_surface *surface = NULL;
|
|
struct tinywl_toplevel *t = desktop_toplevel_at(server, x, y, &surface, &sx, &sy);
|
|
if (!t || !surface) return NULL;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->pointers)) return NULL;
|
|
struct wl_resource *res;
|
|
if (cua_ptr[0].entered != surface) {
|
|
if (cua_ptr[0].entered) cua_ptr_leave(server->seat, cua_ptr[0].entered);
|
|
uint32_t serial = wlr_seat_client_next_serial(sc);
|
|
wl_resource_for_each(res, &sc->pointers) {
|
|
wl_pointer_send_enter(res, serial, surface->resource, wl_fixed_from_double(sx), wl_fixed_from_double(sy));
|
|
cua_pframe(res);
|
|
}
|
|
cua_ptr[0].entered = surface;
|
|
}
|
|
uint32_t tm = cua_now_ms();
|
|
wl_resource_for_each(res, &sc->pointers) {
|
|
wl_pointer_send_motion(res, tm, wl_fixed_from_double(sx), wl_fixed_from_double(sy));
|
|
cua_pframe(res);
|
|
}
|
|
return t;
|
|
}
|
|
static bool cua_button(struct tinywl_server *server, struct tinywl_toplevel *t, int idx, uint32_t button, bool pressed) {
|
|
if (!t || idx < 0 || idx >= CUA_MAXDEV) return false;
|
|
/* `cua_motion` establishes the exact child or root surface for this logical
|
|
* pointer. Button and axis events must use that same wl_pointer resource. */
|
|
struct wlr_surface *surface = cua_ptr[idx].entered ? cua_ptr[idx].entered : t->xdg_toplevel->base->surface;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->pointers)) return false;
|
|
uint32_t tm = cua_now_ms(), bs = wlr_seat_client_next_serial(sc);
|
|
struct wl_resource *res;
|
|
wl_resource_for_each(res, &sc->pointers) {
|
|
wl_pointer_send_button(res, bs, tm, button, pressed ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED);
|
|
cua_pframe(res);
|
|
}
|
|
return true;
|
|
}
|
|
static bool cua_axis(struct tinywl_server *server, struct tinywl_toplevel *t, int idx, uint32_t axis, double value) {
|
|
if (!t || idx < 0 || idx >= CUA_MAXDEV) return false;
|
|
struct wlr_surface *surface = cua_ptr[idx].entered ? cua_ptr[idx].entered : t->xdg_toplevel->base->surface;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->pointers)) return false;
|
|
uint32_t tm = cua_now_ms();
|
|
struct wl_resource *res;
|
|
wl_resource_for_each(res, &sc->pointers) {
|
|
int32_t step = value < 0 ? -1 : 1;
|
|
if (wl_resource_get_version(res) >= WL_POINTER_AXIS_SOURCE_SINCE_VERSION)
|
|
wl_pointer_send_axis_source(res, WL_POINTER_AXIS_SOURCE_WHEEL);
|
|
if (wl_resource_get_version(res) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
|
|
wl_pointer_send_axis_value120(res, axis, step * 120);
|
|
else if (wl_resource_get_version(res) >= WL_POINTER_AXIS_DISCRETE_SINCE_VERSION)
|
|
wl_pointer_send_axis_discrete(res, axis, step);
|
|
wl_pointer_send_axis(res, tm, axis, wl_fixed_from_double(value));
|
|
cua_pframe(res);
|
|
}
|
|
return true;
|
|
}
|
|
static void cua_init_keymap(void) {
|
|
struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
struct xkb_rule_names names = {0};
|
|
struct xkb_keymap *km = xkb_keymap_new_from_names(ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
char *s = xkb_keymap_get_as_string(km, XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
g_keymap_size = strlen(s) + 1;
|
|
g_keymap_fd = memfd_create("cua-keymap", MFD_CLOEXEC);
|
|
if (ftruncate(g_keymap_fd, g_keymap_size) == 0) {
|
|
void *p = mmap(NULL, g_keymap_size, PROT_READ | PROT_WRITE, MAP_SHARED, g_keymap_fd, 0);
|
|
memcpy(p, s, g_keymap_size); munmap(p, g_keymap_size);
|
|
}
|
|
free(s);
|
|
/* Build a US-layout char -> (evdev keycode, shift-level) table from the
|
|
* keymap so the `t` command can type arbitrary ASCII. xkb keycodes are
|
|
* evdev+8; wl_keyboard.key wants the evdev code. */
|
|
xkb_mod_index_t shift = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_SHIFT);
|
|
if (shift != XKB_MOD_INVALID) g_shift_mask = (xkb_mod_mask_t)1 << shift;
|
|
xkb_mod_index_t ctrl = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_CTRL);
|
|
if (ctrl != XKB_MOD_INVALID) g_ctrl_mask = (xkb_mod_mask_t)1 << ctrl;
|
|
xkb_mod_index_t alt = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_ALT);
|
|
if (alt != XKB_MOD_INVALID) g_alt_mask = (xkb_mod_mask_t)1 << alt;
|
|
xkb_mod_index_t logo = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_LOGO);
|
|
if (logo != XKB_MOD_INVALID) g_logo_mask = (xkb_mod_mask_t)1 << logo;
|
|
for (xkb_keycode_t kc = 9; kc < 256; kc++) {
|
|
for (int lvl = 0; lvl < 2; lvl++) {
|
|
const xkb_keysym_t *syms;
|
|
int n = xkb_keymap_key_get_syms_by_level(km, kc, 0, lvl, &syms);
|
|
if (n != 1) continue;
|
|
uint32_t cp = xkb_keysym_to_utf32(syms[0]);
|
|
if (cp > 0 && cp < 128 && !g_chartab[cp].valid) {
|
|
g_chartab[cp].keycode = (uint32_t)kc - 8;
|
|
g_chartab[cp].shift = lvl;
|
|
g_chartab[cp].valid = 1;
|
|
}
|
|
}
|
|
}
|
|
/* Control characters used by the protocol map to dedicated keys. */
|
|
g_chartab['\n'] = (struct cua_keyent){ KEY_ENTER, 0, 1 };
|
|
g_chartab['\t'] = (struct cua_keyent){ KEY_TAB, 0, 1 };
|
|
xkb_keymap_unref(km); xkb_context_unref(ctx);
|
|
wlr_log(WLR_INFO, "[cua] xkb keymap + chartab ready (%zu bytes)", g_keymap_size);
|
|
}
|
|
/* Ensure the target client's keyboard has had keymap + enter sent once (focus
|
|
* free). idx 0 is the typing keyboard. */
|
|
static struct wlr_seat_client *cua_kbd_enter(struct tinywl_server *server, struct tinywl_toplevel *t) {
|
|
struct wlr_surface *surface = t->xdg_toplevel->base->surface;
|
|
struct wlr_seat_client *sc = wlr_seat_client_for_wl_client(server->seat, wl_resource_get_client(surface->resource));
|
|
if (!sc || wl_list_empty(&sc->keyboards)) return NULL;
|
|
if (cua_kbd_state[0].entered != surface) {
|
|
struct wl_resource *res; struct wl_array keys; wl_array_init(&keys);
|
|
wl_resource_for_each(res, &sc->keyboards) {
|
|
wl_keyboard_send_keymap(res, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, g_keymap_fd, g_keymap_size);
|
|
wl_keyboard_send_enter(res, wlr_seat_client_next_serial(sc), surface->resource, &keys);
|
|
wl_keyboard_send_modifiers(res, wlr_seat_client_next_serial(sc), 0, 0, 0, 0);
|
|
}
|
|
wl_array_release(&keys);
|
|
cua_kbd_state[0].entered = surface;
|
|
}
|
|
return sc;
|
|
}
|
|
static void cua_kbd_mods(struct wlr_seat_client *sc, uint32_t depressed) {
|
|
struct wl_resource *res;
|
|
wl_resource_for_each(res, &sc->keyboards)
|
|
wl_keyboard_send_modifiers(res, wlr_seat_client_next_serial(sc), depressed, 0, 0, 0);
|
|
}
|
|
static void cua_kbd_key(struct wlr_seat_client *sc, uint32_t keycode, bool pressed) {
|
|
uint32_t tm = cua_now_ms();
|
|
struct wl_resource *res;
|
|
wl_resource_for_each(res, &sc->keyboards)
|
|
wl_keyboard_send_key(res, wlr_seat_client_next_serial(sc), tm, keycode,
|
|
pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
|
|
}
|
|
static bool cua_type_cp(struct tinywl_server *server, struct tinywl_toplevel *t, uint32_t cp) {
|
|
if (cp >= 128 || !g_chartab[cp].valid) return false;
|
|
struct wlr_seat_client *sc = cua_kbd_enter(server, t);
|
|
if (!sc) return false;
|
|
struct cua_keyent e = g_chartab[cp];
|
|
if (e.shift) cua_kbd_mods(sc, g_shift_mask);
|
|
cua_kbd_key(sc, e.keycode, true);
|
|
cua_kbd_key(sc, e.keycode, false);
|
|
if (e.shift) cua_kbd_mods(sc, 0);
|
|
return true;
|
|
}
|
|
/* Decode a hex-encoded ASCII string and type it focus-free into `t`. */
|
|
static bool cua_type_hex(struct tinywl_server *server, struct tinywl_toplevel *t, const char *hex) {
|
|
if (!t) return false;
|
|
for (const char *p = hex; p[0] && p[1]; p += 2) {
|
|
int hi = (p[0] <= '9') ? p[0] - '0' : (p[0] | 0x20) - 'a' + 10;
|
|
int lo = (p[1] <= '9') ? p[1] - '0' : (p[1] | 0x20) - 'a' + 10;
|
|
if (!cua_type_cp(server, t, (uint32_t)((hi << 4) | lo))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
static uint32_t cua_named_keycode(const char *name) {
|
|
uint32_t kc = 0;
|
|
if (!strcasecmp(name, "enter") || !strcasecmp(name, "return")) kc = KEY_ENTER;
|
|
else if (!strcasecmp(name, "tab")) kc = KEY_TAB;
|
|
else if (!strcasecmp(name, "escape") || !strcasecmp(name, "esc")) kc = KEY_ESC;
|
|
else if (!strcasecmp(name, "backspace")) kc = KEY_BACKSPACE;
|
|
else if (!strcasecmp(name, "space")) kc = KEY_SPACE;
|
|
else if (!strcasecmp(name, "up")) kc = KEY_UP;
|
|
else if (!strcasecmp(name, "down")) kc = KEY_DOWN;
|
|
else if (!strcasecmp(name, "left")) kc = KEY_LEFT;
|
|
else if (!strcasecmp(name, "right")) kc = KEY_RIGHT;
|
|
else if (!strncasecmp(name, "f", 1)) {
|
|
char *end = NULL; long fn = strtol(name + 1, &end, 10);
|
|
if (end && !*end && fn >= 1 && fn <= 10) kc = KEY_F1 + (uint32_t)fn - 1;
|
|
else if (end && !*end && fn == 11) kc = KEY_F11;
|
|
else if (end && !*end && fn == 12) kc = KEY_F12;
|
|
}
|
|
return kc;
|
|
}
|
|
/* Returns 1 when `name` is a recognised key (delivered if the target had a
|
|
* keyboard bound), 0 when it is outside the whitelist so the caller can NAK. */
|
|
static int cua_key_named(struct tinywl_server *server, struct tinywl_toplevel *t, const char *name) {
|
|
if (!t) return 0;
|
|
uint32_t kc = cua_named_keycode(name);
|
|
if (!kc) return 0;
|
|
struct wlr_seat_client *sc = cua_kbd_enter(server, t);
|
|
if (!sc) return -1;
|
|
cua_kbd_key(sc, kc, true);
|
|
cua_kbd_key(sc, kc, false);
|
|
return 1;
|
|
}
|
|
static int cua_hotkey(struct tinywl_server *server, struct tinywl_toplevel *t, const char *mods, const char *key) {
|
|
if (!t) return 0;
|
|
uint32_t kc = cua_named_keycode(key);
|
|
if (!kc && key[0] && !key[1]) {
|
|
unsigned char cp = (unsigned char)key[0];
|
|
if (cp < 128 && g_chartab[cp].valid) kc = g_chartab[cp].keycode;
|
|
}
|
|
if (!kc) return 0;
|
|
xkb_mod_mask_t mask = 0;
|
|
char copy[128]; snprintf(copy, sizeof copy, "%s", mods);
|
|
char *save = NULL;
|
|
for (char *mod = strtok_r(copy, ",", &save); mod; mod = strtok_r(NULL, ",", &save)) {
|
|
if (!strcasecmp(mod, "ctrl") || !strcasecmp(mod, "control")) mask |= g_ctrl_mask;
|
|
else if (!strcasecmp(mod, "shift")) mask |= g_shift_mask;
|
|
else if (!strcasecmp(mod, "alt") || !strcasecmp(mod, "option")) mask |= g_alt_mask;
|
|
else if (!strcasecmp(mod, "meta") || !strcasecmp(mod, "super") || !strcasecmp(mod, "win") || !strcasecmp(mod, "cmd")) mask |= g_logo_mask;
|
|
else return 0;
|
|
}
|
|
struct wlr_seat_client *sc = cua_kbd_enter(server, t);
|
|
if (!sc) return -1;
|
|
cua_kbd_mods(sc, mask);
|
|
cua_kbd_key(sc, kc, true);
|
|
cua_kbd_key(sc, kc, false);
|
|
cua_kbd_mods(sc, 0);
|
|
return 1;
|
|
}
|
|
/* ── control socket: one line per command, routed by app_id ───────────────── */
|
|
/* Process one command line. Returns NULL on success, else a stable error token
|
|
* the caller sends back as `err <token>`. The command is only acknowledged
|
|
* after it has been resolved and applied — never before. */
|
|
static const char *cua_handle_cmd(struct tinywl_server *server, char *line) {
|
|
char cmd[8], app[128];
|
|
if (sscanf(line, "%7s", cmd) != 1) return "empty";
|
|
const char *err = NULL;
|
|
struct tinywl_toplevel *t;
|
|
if (!strcmp(cmd, "d")) {
|
|
double x, y; unsigned count, btn;
|
|
if (sscanf(line, "d %lf %lf %u %u", &x, &y, &count, &btn) != 4) return "bad-args";
|
|
if (!(t = cua_desktop_motion(server, x, y))) return "no-surface-at-point";
|
|
for (unsigned i = 0; i < (count ? count : 1); i++) {
|
|
if (!cua_button(server, t, 0, btn, true)) return "no-pointer-resource";
|
|
if (!cua_button(server, t, 0, btn, false)) return "no-pointer-resource";
|
|
}
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "m")) {
|
|
int idx; double x, y;
|
|
if (sscanf(line, "m %127s %d %lf %lf", app, &idx, &x, &y) != 4) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
if (!cua_motion(server, t, idx, x, y)) return "no-pointer-resource";
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "b")) {
|
|
int idx; unsigned btn, pr;
|
|
if (sscanf(line, "b %127s %d %u %u", app, &idx, &btn, &pr) != 4) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
if (!cua_button(server, t, idx, btn, pr != 0)) return "no-pointer-resource";
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "t")) {
|
|
char hex[8192];
|
|
if (sscanf(line, "t %127s %8191s", app, hex) != 2) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
if (!cua_type_hex(server, t, hex)) return "no-keyboard-resource";
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "k")) {
|
|
char key[32];
|
|
if (sscanf(line, "k %127s %31s", app, key) != 2) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
int result = cua_key_named(server, t, key);
|
|
if (result < 0) return "no-keyboard-resource";
|
|
if (!result) return "unknown-key";
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "h")) {
|
|
char mods[128], key[32];
|
|
if (sscanf(line, "h %127s %127s %31s", app, mods, key) != 3) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
int result = cua_hotkey(server, t, mods, key);
|
|
if (result < 0) return "no-keyboard-resource";
|
|
if (!result) return "unknown-hotkey";
|
|
return NULL;
|
|
} else if (!strcmp(cmd, "a")) {
|
|
int idx; unsigned axis; double value;
|
|
if (sscanf(line, "a %127s %d %u %lf", app, &idx, &axis, &value) != 4) return "bad-args";
|
|
if (!(t = cua_resolve_target(server, app, &err))) return err;
|
|
if (!cua_axis(server, t, idx, axis, value)) return "no-pointer-resource";
|
|
return NULL;
|
|
}
|
|
return "unknown-command";
|
|
}
|
|
struct cua_conn { struct tinywl_server *server; struct wl_event_source *src; int hello; char buf[16384]; size_t len; };
|
|
/* Tear a connection down once: remove its event source (else the loop fires it
|
|
* again on freed data -> double free), close the fd, free the state. */
|
|
static int cua_conn_drop(struct cua_conn *c, int fd) {
|
|
if (c->src) wl_event_source_remove(c->src);
|
|
close(fd); free(c);
|
|
return 0;
|
|
}
|
|
static int cua_conn_readable(int fd, uint32_t mask, void *data) {
|
|
struct cua_conn *c = data;
|
|
if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) return cua_conn_drop(c, fd);
|
|
ssize_t n = read(fd, c->buf + c->len, sizeof(c->buf) - c->len - 1);
|
|
if (n <= 0) return cua_conn_drop(c, fd);
|
|
c->len += (size_t)n; c->buf[c->len] = 0;
|
|
char *p = c->buf, *nl;
|
|
while ((nl = memchr(p, '\n', (size_t)(c->buf + c->len - p)))) {
|
|
*nl = 0;
|
|
/* Tolerate CRLF clients by trimming a trailing carriage return. */
|
|
if (nl > p && nl[-1] == '\r') nl[-1] = 0;
|
|
if (!c->hello) {
|
|
/* The first line must be the versioned v1 handshake. */
|
|
if (!strcmp(p, CUA_PROTO_HELLO)) {
|
|
c->hello = 1;
|
|
cua_reply(fd, CUA_PROTO_HELLO);
|
|
} else {
|
|
cua_reply(fd, "err unsupported-version");
|
|
return cua_conn_drop(c, fd);
|
|
}
|
|
} else {
|
|
int query_pid, geometry_pid, activate_pid;
|
|
if (sscanf(p, "q %d", &query_pid) == 1) {
|
|
char msg[128]; cua_query_state(c->server, (pid_t)query_pid, msg, sizeof msg);
|
|
cua_reply(fd, msg);
|
|
} else if (sscanf(p, "g %d", &geometry_pid) == 1) {
|
|
char msg[128];
|
|
const char *err = cua_query_geometry(c->server, (pid_t)geometry_pid, msg, sizeof msg);
|
|
if (err) {
|
|
char reply[128]; snprintf(reply, sizeof reply, "err %s", err); cua_reply(fd, reply);
|
|
} else {
|
|
cua_reply(fd, msg);
|
|
}
|
|
} else if (sscanf(p, "f %d", &activate_pid) == 1) {
|
|
const char *err = cua_activate_pid(c->server, (pid_t)activate_pid);
|
|
wl_display_flush_clients(c->server->wl_display);
|
|
if (err) {
|
|
char msg[128]; snprintf(msg, sizeof msg, "err %s", err); cua_reply(fd, msg);
|
|
} else {
|
|
cua_reply(fd, "ok");
|
|
}
|
|
} else {
|
|
const char *err = cua_handle_cmd(c->server, p);
|
|
/* Deliver injected events before acking so `ok` means "processed",
|
|
* never merely "parsed". */
|
|
wl_display_flush_clients(c->server->wl_display);
|
|
if (err) {
|
|
char msg[128];
|
|
snprintf(msg, sizeof msg, "err %s", err);
|
|
cua_reply(fd, msg);
|
|
} else {
|
|
cua_reply(fd, "ok");
|
|
}
|
|
}
|
|
}
|
|
p = nl + 1;
|
|
}
|
|
size_t rem = (size_t)(c->buf + c->len - p);
|
|
memmove(c->buf, p, rem); c->len = rem;
|
|
wl_display_flush_clients(c->server->wl_display);
|
|
return 0;
|
|
}
|
|
static int cua_listen_cb(int fd, uint32_t mask, void *data) {
|
|
struct tinywl_server *server = data;
|
|
int cfd = accept(fd, NULL, NULL);
|
|
if (cfd < 0) return 0;
|
|
struct cua_conn *c = calloc(1, sizeof *c); c->server = server;
|
|
c->src = wl_event_loop_add_fd(wl_display_get_event_loop(server->wl_display), cfd,
|
|
WL_EVENT_READABLE, cua_conn_readable, c);
|
|
return 0;
|
|
}
|
|
static void cua_setup_control_socket(struct tinywl_server *server) {
|
|
const char *path = getenv("CUA_INJECT_SOCKET");
|
|
if (!path) { wlr_log(WLR_INFO, "[cua] no CUA_INJECT_SOCKET; injection disabled"); return; }
|
|
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) { wlr_log(WLR_ERROR, "[cua] socket(): %s", strerror(errno)); return; }
|
|
struct sockaddr_un addr = {0}; addr.sun_family = AF_UNIX;
|
|
snprintf(addr.sun_path, sizeof addr.sun_path, "%s", path);
|
|
unlink(path);
|
|
if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(fd, 4) < 0) {
|
|
wlr_log(WLR_ERROR, "[cua] bind/listen %s: %s", path, strerror(errno)); close(fd); return;
|
|
}
|
|
cua_init_keymap();
|
|
wl_event_loop_add_fd(wl_display_get_event_loop(server->wl_display), fd,
|
|
WL_EVENT_READABLE, cua_listen_cb, server);
|
|
wlr_log(WLR_INFO, "[cua] injection control socket on %s", path);
|
|
}
|
|
|
|
"""
|
|
|
|
def repl(s, old, new, label, count=1):
|
|
if s.count(old) != count:
|
|
sys.stderr.write("ANCHOR FAIL [%s]: found %d (want %d)\n" % (label, s.count(old), count))
|
|
sys.exit(1)
|
|
return s.replace(old, new, count)
|
|
|
|
# 1) includes + globals + a foreign-toplevel handle field on the toplevel.
|
|
src = repl(src, "struct tinywl_server {", INCLUDES + "struct tinywl_server {", "includes")
|
|
src = repl(src,
|
|
"\tstruct wlr_xdg_toplevel *xdg_toplevel;\n",
|
|
STRUCT_FIELD, "ftl-field")
|
|
|
|
# 2) injection helpers + control socket, just before main().
|
|
src = repl(src, "int main(int argc, char *argv[]) {", FUNCS + "int main(int argc, char *argv[]) {", "funcs")
|
|
|
|
# 3) On map: register a foreign-toplevel handle (title/app_id) for list_windows.
|
|
src = repl(src,
|
|
"\twl_list_insert(&toplevel->server->toplevels, &toplevel->link);\n\n\tfocus_toplevel(toplevel);",
|
|
"\twl_list_insert(&toplevel->server->toplevels, &toplevel->link);\n"
|
|
"\tif (g_ftl_mgr) {\n"
|
|
"\t\ttoplevel->ftl = wlr_foreign_toplevel_handle_v1_create(g_ftl_mgr);\n"
|
|
"\t\tif (toplevel->xdg_toplevel->title)\n"
|
|
"\t\t\twlr_foreign_toplevel_handle_v1_set_title(toplevel->ftl, toplevel->xdg_toplevel->title);\n"
|
|
"\t\tif (toplevel->xdg_toplevel->app_id)\n"
|
|
"\t\t\twlr_foreign_toplevel_handle_v1_set_app_id(toplevel->ftl, toplevel->xdg_toplevel->app_id);\n"
|
|
"\t\ttoplevel->ftl_request_activate.notify = cua_ftl_request_activate;\n"
|
|
"\t\twl_signal_add(&toplevel->ftl->events.request_activate, &toplevel->ftl_request_activate);\n"
|
|
"\t}\n\n\tfocus_toplevel(toplevel);",
|
|
"ftl-on-map")
|
|
|
|
# 4) On unmap: drop the foreign-toplevel handle.
|
|
src = repl(src,
|
|
"\twl_list_remove(&toplevel->link);\n}",
|
|
"\tstruct wlr_surface *cua_surface = toplevel->xdg_toplevel->base->surface;\n"
|
|
"\tfor (int i = 0; i < CUA_MAXDEV; i++) {\n"
|
|
"\t\tif (cua_ptr[i].entered && wlr_surface_get_root_surface(cua_ptr[i].entered) == cua_surface) cua_ptr[i].entered = NULL;\n"
|
|
"\t\tif (cua_kbd_state[i].entered && wlr_surface_get_root_surface(cua_kbd_state[i].entered) == cua_surface) cua_kbd_state[i].entered = NULL;\n"
|
|
"\t}\n"
|
|
"\tif (toplevel->ftl) { wl_list_remove(&toplevel->ftl_request_activate.link); wlr_foreign_toplevel_handle_v1_destroy(toplevel->ftl); toplevel->ftl = NULL; }\n"
|
|
"\twl_list_remove(&toplevel->link);\n}",
|
|
"ftl-on-unmap")
|
|
|
|
# 5) On commit: keep title/app_id fresh (they often arrive after the map).
|
|
src = repl(src,
|
|
"\t\twlr_xdg_toplevel_set_size(toplevel->xdg_toplevel, 0, 0);\n\t}",
|
|
"\t\twlr_xdg_toplevel_set_size(toplevel->xdg_toplevel, 0, 0);\n\t}\n"
|
|
"\tif (toplevel->ftl) {\n"
|
|
"\t\tif (toplevel->xdg_toplevel->title)\n"
|
|
"\t\t\twlr_foreign_toplevel_handle_v1_set_title(toplevel->ftl, toplevel->xdg_toplevel->title);\n"
|
|
"\t\tif (toplevel->xdg_toplevel->app_id)\n"
|
|
"\t\t\twlr_foreign_toplevel_handle_v1_set_app_id(toplevel->ftl, toplevel->xdg_toplevel->app_id);\n"
|
|
"\t}",
|
|
"ftl-on-commit")
|
|
|
|
# 6) Headless backend has no preferred mode -> set a real custom mode so the
|
|
# scene has a non-zero output for screencopy/grim.
|
|
src = repl(src,
|
|
"\tstruct wlr_output_mode *mode = wlr_output_preferred_mode(wlr_output);\n\tif (mode != NULL) {\n\t\twlr_output_state_set_mode(&state, mode);\n\t}",
|
|
"\tstruct wlr_output_mode *mode = wlr_output_preferred_mode(wlr_output);\n\tif (mode != NULL) {\n\t\twlr_output_state_set_mode(&state, mode);\n\t} else {\n"
|
|
"\t\tint ow = getenv(\"CUA_OUTW\") ? atoi(getenv(\"CUA_OUTW\")) : 1280;\n"
|
|
"\t\tint oh = getenv(\"CUA_OUTH\") ? atoi(getenv(\"CUA_OUTH\")) : 1024;\n"
|
|
"\t\twlr_output_state_set_custom_mode(&state, ow, oh, 0);\n\t}",
|
|
"headless-custom-mode")
|
|
|
|
# 7) Create the extra managers + the seat caps + the control socket in main.
|
|
src = repl(src,
|
|
"\tserver.output_layout = wlr_output_layout_create(server.wl_display);",
|
|
"\tserver.output_layout = wlr_output_layout_create(server.wl_display);\n"
|
|
"\twlr_xdg_output_manager_v1_create(server.wl_display, server.output_layout);\n"
|
|
"\twlr_screencopy_manager_v1_create(server.wl_display);\n"
|
|
"\tg_ftl_mgr = wlr_foreign_toplevel_manager_v1_create(server.wl_display);",
|
|
"managers")
|
|
src = repl(src,
|
|
'server.seat = wlr_seat_create(server.wl_display, "seat0");',
|
|
'server.seat = wlr_seat_create(server.wl_display, "seat0");\n'
|
|
"\twlr_seat_set_capabilities(server.seat, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD);",
|
|
"seat-caps")
|
|
src = repl(src,
|
|
"\twl_display_run(server.wl_display);",
|
|
"\tcua_setup_control_socket(&server);\n\twl_display_run(server.wl_display);",
|
|
"control-socket-setup")
|
|
|
|
io.open(out, "w", encoding="utf-8").write(src)
|
|
sys.stderr.write("cua-compositor.c written (%d bytes)\n" % len(src))
|