srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tools/virtual-pointer-click/src
diff options
context:
space:
mode:
Diffstat (limited to 'tools/virtual-pointer-click/src')
-rw-r--r--tools/virtual-pointer-click/src/main.rs161
1 files changed, 161 insertions, 0 deletions
diff --git a/tools/virtual-pointer-click/src/main.rs b/tools/virtual-pointer-click/src/main.rs
new file mode 100644
index 0000000..993de45
--- /dev/null
+++ b/tools/virtual-pointer-click/src/main.rs
@@ -0,0 +1,161 @@
+// A scriptable zwlr_virtual_pointer_unstable_v1 driver: reads one command
+// per line from stdin and turns it into a virtual-pointer request against
+// whatever compositor WAYLAND_DISPLAY names.
+//
+// WHY THIS EXISTS RATHER THAN ydotool
+//
+// ydotool writes to /dev/uinput. That is a kernel-level device shared by
+// every session on the machine, so a synthetic click from it lands wherever
+// the real seat's focus happens to be - which, when the target is a nested
+// throwaway compositor, is very often the user's real desktop instead. This
+// tool is an ordinary Wayland client of one specific compositor, so its
+// input physically cannot reach any other one. That makes it the safe way
+// to drive a nested test instance, which is the only reason it exists.
+//
+// ../virtual-pointer-pin-test is not this: it is a fixed left-button drag
+// used to verify pinned delivery, and it blocks on stdin exactly once. This
+// one stays alive and takes a stream of commands, so a shell script can
+// interleave a `grim` screenshot between a move and the click that follows
+// it - which is what "never click at a position you have not verified
+// first" actually requires.
+//
+// Usage:
+// vpclick (commands on stdin, one per line)
+//
+// move <x> <y> absolute, as a fraction of EXTENT (5000 5000 = centre)
+// press <button> left | right | middle
+// release <button>
+// click <button> press then release
+// sync round-trip and acknowledge, nothing else
+// quit exit
+//
+// Every command prints "ok <command>" once it has round-tripped, so a
+// driving script can wait for the compositor to have actually seen it
+// rather than sleeping and hoping.
+
+use std::io::{BufRead, Write};
+
+use wayland_client::protocol::wl_pointer::ButtonState;
+use wayland_client::protocol::{wl_registry, wl_seat::WlSeat};
+use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle};
+use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1;
+use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_v1::{self, ZwlrVirtualPointerV1};
+
+const EXTENT: u32 = 10000;
+
+/// Linux `input-event-codes.h` button codes - what the protocol asks for
+/// verbatim, not a wl_pointer enum.
+const BTN_LEFT: u32 = 0x110;
+const BTN_RIGHT: u32 = 0x111;
+const BTN_MIDDLE: u32 = 0x112;
+
+struct State {
+ seat: Option<WlSeat>,
+ manager: Option<ZwlrVirtualPointerManagerV1>,
+}
+
+impl Dispatch<wl_registry::WlRegistry, ()> for State {
+ fn event(state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle<Self>) {
+ if let wl_registry::Event::Global { name, interface, version } = event {
+ if interface == "wl_seat" {
+ state.seat = Some(registry.bind::<WlSeat, _, _>(name, version.min(9), qh, ()));
+ } else if interface == "zwlr_virtual_pointer_manager_v1" {
+ state.manager = Some(registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(name, version.min(2), qh, ()));
+ }
+ }
+ }
+}
+
+impl Dispatch<WlSeat, ()> for State {
+ fn event(_: &mut Self, _: &WlSeat, _: wayland_client::protocol::wl_seat::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {}
+}
+
+impl Dispatch<ZwlrVirtualPointerManagerV1, ()> for State {
+ fn event(_: &mut Self, _: &ZwlrVirtualPointerManagerV1, _: wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_manager_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
+ }
+}
+
+impl Dispatch<ZwlrVirtualPointerV1, ()> for State {
+ fn event(_: &mut Self, _: &ZwlrVirtualPointerV1, _: zwlr_virtual_pointer_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {}
+}
+
+fn button_code(name: &str) -> Option<u32> {
+ match name {
+ "left" => Some(BTN_LEFT),
+ "right" => Some(BTN_RIGHT),
+ "middle" => Some(BTN_MIDDLE),
+ _ => None,
+ }
+}
+
+fn main() {
+ let conn = Connection::connect_to_env().expect("failed to connect to the Wayland compositor - is WAYLAND_DISPLAY set?");
+ let display = conn.display();
+ let mut queue: EventQueue<State> = conn.new_event_queue();
+ let qh = queue.handle();
+ let _registry = display.get_registry(&qh, ());
+
+ let mut state = State { seat: None, manager: None };
+ queue.roundtrip(&mut state).expect("initial roundtrip failed");
+
+ let Some(manager) = state.manager.clone() else {
+ eprintln!("compositor does not advertise zwlr_virtual_pointer_manager_v1");
+ std::process::exit(1);
+ };
+ let Some(seat) = state.seat.clone() else {
+ eprintln!("compositor does not advertise wl_seat");
+ std::process::exit(1);
+ };
+
+ let pointer = manager.create_virtual_pointer(Some(&seat), &qh, ());
+ queue.roundtrip(&mut state).expect("create_virtual_pointer roundtrip failed");
+ println!("ready pid {}", std::process::id());
+ let _ = std::io::stdout().flush();
+
+ // A monotonically rising millisecond stamp. Some compositors ignore
+ // this entirely, but a click whose press and release carry the same
+ // timestamp is indistinguishable from a double-click to anything that
+ // does look, so it is worth stepping.
+ let mut time: u32 = 1;
+ let stdin = std::io::stdin();
+ for line in stdin.lock().lines() {
+ let line = line.expect("failed to read stdin");
+ let parts: Vec<&str> = line.split_whitespace().collect();
+ let Some(&cmd) = parts.first() else { continue };
+ match cmd {
+ "move" if parts.len() == 3 => {
+ let x: u32 = parts[1].parse().expect("x must be a number");
+ let y: u32 = parts[2].parse().expect("y must be a number");
+ pointer.motion_absolute(time, x, y, EXTENT, EXTENT);
+ pointer.frame();
+ }
+ "press" | "release" | "click" if parts.len() == 2 => {
+ let Some(code) = button_code(parts[1]) else {
+ println!("err unknown button {}", parts[1]);
+ let _ = std::io::stdout().flush();
+ continue;
+ };
+ if cmd != "release" {
+ pointer.button(time, code, ButtonState::Pressed);
+ pointer.frame();
+ time += 10;
+ }
+ if cmd != "press" {
+ pointer.button(time, code, ButtonState::Released);
+ pointer.frame();
+ }
+ }
+ "sync" => {}
+ "quit" => break,
+ _ => {
+ println!("err bad command: {line}");
+ let _ = std::io::stdout().flush();
+ continue;
+ }
+ }
+ time += 10;
+ queue.roundtrip(&mut state).expect("roundtrip failed");
+ println!("ok {line}");
+ let _ = std::io::stdout().flush();
+ }
+}