//! Drives a virtual pointer on a wlroots compositor (the headless rig's Sway), //! which has no input devices of its own. Reads one command per line on stdin: //! //! move move to output coordinates //! click press and release the left button //! down / up press or release the left button //! scroll scroll vertically (negative scrolls up), in wheel steps //! sleep //! //! echo -e "move 100 200\nclick" | rig-pointer use std::{io::BufRead, thread, time::Duration}; use wayland_client::{ Connection, Dispatch, QueueHandle, globals::{GlobalListContents, registry_queue_init}, protocol::{wl_pointer, wl_registry, wl_seat::WlSeat}, }; use wayland_protocols_wlr::virtual_pointer::v1::client::{ zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1, zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1, }; const BUTTON_LEFT: u32 = 0x110; struct State; impl Dispatch for State { fn event( _: &mut Self, _: &wl_registry::WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle, ) { } } macro_rules! ignore_events { ($($proxy:ty),*) => {$( impl Dispatch<$proxy, ()> for State { fn event(_: &mut Self, _: &$proxy, _: <$proxy as wayland_client::Proxy>::Event, _: &(), _: &Connection, _: &QueueHandle) {} } )*}; } ignore_events!(WlSeat, ZwlrVirtualPointerManagerV1, ZwlrVirtualPointerV1); fn main() -> Result<(), Box> { let args: Vec = std::env::args() .skip(1) .map(|arg| arg.parse()) .collect::>()?; let [width, height] = args[..] else { return Err("usage: rig-pointer < commands".into()); }; let connection = Connection::connect_to_env()?; let (globals, mut queue) = registry_queue_init::(&connection)?; let handle = queue.handle(); let seat: WlSeat = globals.bind(&handle, 1..=7, ())?; let manager: ZwlrVirtualPointerManagerV1 = globals.bind(&handle, 1..=2, ())?; let pointer = manager.create_virtual_pointer(Some(&seat), &handle, ()); queue.roundtrip(&mut State)?; let start = std::time::Instant::now(); let time = || start.elapsed().as_millis() as u32; for line in std::io::stdin().lock().lines() { let line = line?; let words: Vec<&str> = line.split_whitespace().collect(); let button = |state| { pointer.button(time(), BUTTON_LEFT, state); pointer.frame(); }; match words[..] { ["move", x, y] => { pointer.motion_absolute(time(), x.parse()?, y.parse()?, width, height); pointer.frame(); } ["down"] => button(wl_pointer::ButtonState::Pressed), ["up"] => button(wl_pointer::ButtonState::Released), ["click"] => { button(wl_pointer::ButtonState::Pressed); button(wl_pointer::ButtonState::Released); } ["scroll", steps] => { let steps: i32 = steps.parse()?; pointer.axis_source(wl_pointer::AxisSource::Wheel); pointer.axis_discrete( time(), wl_pointer::Axis::VerticalScroll, 15. * steps as f64, steps, ); pointer.frame(); } ["sleep", seconds] => thread::sleep(Duration::from_secs_f64(seconds.parse()?)), [] => continue, _ => return Err(format!("unknown command: {line}").into()), } queue.roundtrip(&mut State)?; } thread::sleep(Duration::from_millis(200)); Ok(()) }