fix no mouse motion in demo generation

This commit is contained in:
Kovid Goyal 2026-08-09 15:16:08 +05:30
parent 99c75d1243
commit 84763e09d0
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C

View file

@ -4,6 +4,7 @@
import glob
import math
import os
import shutil
import signal
import subprocess
import sys
@ -34,26 +35,200 @@ bar {{
_sway_socket: str = ''
_wayland_display: str = ''
_kitty_socket: str = ''
_virt_mouse: 'subprocess.Popen[bytes] | None' = None
# Protocol XML for zwlr_virtual_pointer_manager_v1 (wlr-protocols)
_VIRTUAL_POINTER_XML = """\
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_virtual_pointer_unstable_v1">
<interface name="zwlr_virtual_pointer_v1" version="2">
<enum name="error">
<entry name="invalid_axis" value="0"/>
<entry name="invalid_axis_source" value="1"/>
</enum>
<request name="motion">
<arg name="time" type="uint"/>
<arg name="dx" type="fixed"/>
<arg name="dy" type="fixed"/>
</request>
<request name="motion_absolute">
<arg name="time" type="uint"/>
<arg name="x" type="uint"/>
<arg name="y" type="uint"/>
<arg name="x_extent" type="uint"/>
<arg name="y_extent" type="uint"/>
</request>
<request name="button">
<arg name="time" type="uint"/>
<arg name="button" type="uint"/>
<arg name="state" type="uint"/>
</request>
<request name="axis">
<arg name="time" type="uint"/>
<arg name="axis" type="uint"/>
<arg name="value" type="fixed"/>
</request>
<request name="frame"/>
<request name="axis_source">
<arg name="axis_source" type="uint"/>
</request>
<request name="axis_stop">
<arg name="time" type="uint"/>
<arg name="axis" type="uint"/>
</request>
<request name="axis_discrete">
<arg name="time" type="uint"/>
<arg name="axis" type="uint"/>
<arg name="value" type="fixed"/>
<arg name="discrete" type="int"/>
</request>
<request name="destroy" type="destructor"/>
</interface>
<interface name="zwlr_virtual_pointer_manager_v1" version="2">
<request name="create_virtual_pointer">
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
</request>
<request name="create_virtual_pointer_with_output" since="2">
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
</request>
<request name="destroy" type="destructor"/>
</interface>
</protocol>
"""
# Daemon that holds a virtual pointer Wayland connection and processes commands on stdin.
# Commands: "move X Y EX EY\\n", "press BTN\\n", "release BTN\\n"
_VIRTUAL_POINTER_C = r"""
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <wayland-client-core.h>
#include "virt-pointer-client.h"
static struct zwlr_virtual_pointer_manager_v1 *g_manager;
static struct zwlr_virtual_pointer_v1 *g_pointer;
static void on_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version)
{
if (!strcmp(interface, "zwlr_virtual_pointer_manager_v1"))
g_manager = wl_registry_bind(registry, name,
&zwlr_virtual_pointer_manager_v1_interface,
version > 1 ? 1 : version);
}
static void on_global_remove(void *d, struct wl_registry *r, uint32_t n) { (void)d; (void)r; (void)n; }
static const struct wl_registry_listener reg_listener = { on_global, on_global_remove };
static uint32_t ms_now(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
int main(void) {
struct wl_display *dpy = wl_display_connect(NULL);
if (!dpy) { fputs("Cannot connect to Wayland display\n", stderr); return 1; }
struct wl_registry *reg = wl_display_get_registry(dpy);
wl_registry_add_listener(reg, &reg_listener, NULL);
wl_display_roundtrip(dpy);
if (!g_manager) {
fputs("zwlr_virtual_pointer_manager_v1 not supported by this compositor\n", stderr);
return 1;
}
g_pointer = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(g_manager, NULL);
wl_display_roundtrip(dpy);
char line[128];
while (fgets(line, sizeof(line), stdin)) {
uint32_t t = ms_now();
uint32_t a, b, c, d;
if (sscanf(line, "move %u %u %u %u", &a, &b, &c, &d) == 4) {
zwlr_virtual_pointer_v1_motion_absolute(g_pointer, t, a, b, c, d);
zwlr_virtual_pointer_v1_frame(g_pointer);
} else if (sscanf(line, "press %u", &a) == 1) {
zwlr_virtual_pointer_v1_button(g_pointer, t, a, 1);
zwlr_virtual_pointer_v1_frame(g_pointer);
} else if (sscanf(line, "release %u", &a) == 1) {
zwlr_virtual_pointer_v1_button(g_pointer, t, a, 0);
zwlr_virtual_pointer_v1_frame(g_pointer);
}
wl_display_flush(dpy);
}
zwlr_virtual_pointer_v1_destroy(g_pointer);
zwlr_virtual_pointer_manager_v1_destroy(g_manager);
wl_display_disconnect(dpy);
return 0;
}
"""
def get_runtime_dir() -> str:
return os.environ.get('XDG_RUNTIME_DIR', f'/run/user/{os.getuid()}')
def sway_msg(*args: str) -> None:
subprocess.run(['swaymsg', '-s', _sway_socket] + list(args), check=True, stdout=subprocess.DEVNULL)
def build_virt_mouse(build_dir: str) -> str:
xml_path = os.path.join(build_dir, 'virt-pointer.xml')
header_path = os.path.join(build_dir, 'virt-pointer-client.h')
proto_c_path = os.path.join(build_dir, 'virt-pointer-client.c')
main_c_path = os.path.join(build_dir, 'virt-pointer-main.c')
binary_path = os.path.join(build_dir, 'virt-pointer')
with open(xml_path, 'w') as f:
f.write(_VIRTUAL_POINTER_XML)
with open(main_c_path, 'w') as f:
f.write(_VIRTUAL_POINTER_C)
subprocess.run(['wayland-scanner', 'client-header', xml_path, header_path], check=True, capture_output=True)
subprocess.run(['wayland-scanner', 'private-code', xml_path, proto_c_path], check=True, capture_output=True)
result = subprocess.run(
['gcc', '-O2', '-I', build_dir, proto_c_path, main_c_path, '-lwayland-client', '-o', binary_path],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise SystemExit(f'Failed to compile virtual pointer tool:\n{result.stderr}')
return binary_path
def start_virt_mouse(binary: str, wayland_display: str) -> None:
global _virt_mouse
env = {**os.environ, 'WAYLAND_DISPLAY': wayland_display}
_virt_mouse = subprocess.Popen([binary], stdin=subprocess.PIPE, env=env, text=False)
time.sleep(0.2) # allow the virtual pointer to register with the compositor
def stop_virt_mouse() -> None:
global _virt_mouse
if _virt_mouse is not None:
with suppress(Exception):
assert _virt_mouse.stdin is not None
_virt_mouse.stdin.close()
with suppress(subprocess.TimeoutExpired):
_virt_mouse.wait(timeout=2)
if _virt_mouse.returncode is None:
_virt_mouse.kill()
_virt_mouse.wait()
_virt_mouse = None
def send_virt_mouse(cmd: str) -> None:
if _virt_mouse is not None and _virt_mouse.stdin is not None:
_virt_mouse.stdin.write(cmd.encode())
_virt_mouse.stdin.flush()
def move_mouse(geometry: tuple[int, int, int, int], x: int, y: int) -> None:
abs_x = geometry[0] + x
abs_y = geometry[1] + y
sway_msg('seat', 'seat0', 'cursor', 'set', str(abs_x), str(abs_y))
send_virt_mouse(f'move {abs_x} {abs_y} {SCREEN_WIDTH} {SCREEN_HEIGHT}\n')
def click_mouse() -> None:
sway_msg('seat', 'seat0', 'cursor', 'press', 'button1')
send_virt_mouse('press 272\n') # BTN_LEFT = 0x110 = 272
time.sleep(0.05)
sway_msg('seat', 'seat0', 'cursor', 'release', 'button1')
send_virt_mouse('release 272\n')
def remote_control(*args: str) -> None:
@ -240,14 +415,19 @@ def do_one(which: str) -> None:
f.write(SWAY_CONFIG)
config_file = f.name
build_dir = tempfile.mkdtemp(prefix='kitty-demo-build-')
kitty_socket_path = f'@kitty-custom-shader-demo-{os.getpid()}'
try:
virt_mouse_binary = build_virt_mouse(build_dir)
sway_proc, wayland_display, ipc_socket = start_sway(config_file)
_sway_socket = ipc_socket
_wayland_display = wayland_display
_kitty_socket = kitty_socket_path
wayland_env = {'WAYLAND_DISPLAY': wayland_display}
# Start virtual pointer daemon before kitty so the seat has pointer
# capability when kitty connects and creates its wl_pointer object.
start_virt_mouse(virt_mouse_binary, wayland_display)
try:
kcmd = [
'kitty',
@ -273,6 +453,7 @@ def do_one(which: str) -> None:
kitty.terminate()
kitty.wait()
finally:
stop_virt_mouse()
sway_proc.terminate()
with suppress(subprocess.TimeoutExpired):
sway_proc.wait(timeout=5)
@ -284,6 +465,7 @@ def do_one(which: str) -> None:
os.remove(config_file)
with suppress(FileNotFoundError):
os.remove(kitty_socket_path)
shutil.rmtree(build_dir, ignore_errors=True)
def main() -> None: