use std::sync::OnceLock;
use x11rb::connection::Connection as _;
use x11rb::protocol::xproto::Window;
use x11rb::protocol::xtest::ConnectionExt as _;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
use x11rb::CURRENT_TIME;
use crate::engine::{EngineError, EngineResult};
#[derive(Debug, Clone, Copy)]
pub(crate) struct ScreenPoint {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Button {
Left,
#[allow(dead_code)]
Middle,
#[allow(dead_code)]
Right,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum InputEvent {
Move(ScreenPoint),
Press(Button),
Release(Button),
}
pub(crate) trait InputDispatcher: Send + Sync {
fn backend_name(&self) -> &'static str;
fn dispatch(&self, ev: InputEvent) -> EngineResult<()>;
fn flush(&self) -> EngineResult<()>;
}
pub(crate) fn detect() -> Option<Box<dyn InputDispatcher>> {
if is_wayland_session() {
if let Some(b) = LibeiDispatcher::try_new() {
return Some(Box::new(b));
}
}
if let Some(b) = XtestDispatcher::try_new() {
return Some(Box::new(b));
}
None
}
fn is_wayland_session() -> bool {
std::env::var_os("WAYLAND_DISPLAY").is_some()
&& std::env::var("XDG_SESSION_TYPE").as_deref() == Ok("wayland")
}
static DISPATCHER: OnceLock<Option<Box<dyn InputDispatcher + 'static>>> = OnceLock::new();
pub(crate) fn dispatcher() -> EngineResult<&'static (dyn InputDispatcher + 'static)> {
let cell = DISPATCHER.get_or_init(detect);
match cell.as_deref() {
Some(d) => Ok(d),
None => Err(EngineError::Unsupported {
engine: "wpe",
primitive: "cursor_op",
}),
}
}
#[allow(dead_code)]
pub(crate) fn active_backend_name() -> Option<&'static str> {
DISPATCHER
.get_or_init(detect)
.as_deref()
.map(InputDispatcher::backend_name)
}
const XT_MOTION_NOTIFY: u8 = 6;
const XT_BUTTON_PRESS: u8 = 4;
const XT_BUTTON_RELEASE: u8 = 5;
struct XtestDispatcher {
conn: RustConnection,
root: Window,
}
impl XtestDispatcher {
fn try_new() -> Option<Self> {
let (conn, screen_num) = RustConnection::connect(None).ok()?;
let root = conn.setup().roots.get(screen_num)?.root;
Some(Self { conn, root })
}
}
impl InputDispatcher for XtestDispatcher {
fn backend_name(&self) -> &'static str {
"xtest"
}
fn dispatch(&self, ev: InputEvent) -> EngineResult<()> {
let (type_, detail, root_x, root_y) = match ev {
InputEvent::Move(p) => (
XT_MOTION_NOTIFY,
0_u8,
clamp_i16(p.x),
clamp_i16(p.y),
),
InputEvent::Press(b) => (XT_BUTTON_PRESS, button_code(b), 0, 0),
InputEvent::Release(b) => (XT_BUTTON_RELEASE, button_code(b), 0, 0),
};
self.conn
.xtest_fake_input(type_, detail, CURRENT_TIME, self.root, root_x, root_y, 0)
.map_err(|e| EngineError::Other(format!("xtest_fake_input: {e}")))?
.ignore_error();
Ok(())
}
fn flush(&self) -> EngineResult<()> {
self.conn
.flush()
.map_err(|e| EngineError::Other(format!("XFlush: {e}")))?;
self.conn
.sync()
.map_err(|e| EngineError::Other(format!("XSync: {e}")))?;
Ok(())
}
}
#[allow(clippy::cast_possible_truncation)]
fn clamp_i16(v: i32) -> i16 {
v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
}
fn button_code(b: Button) -> u8 {
match b {
Button::Left => 1,
Button::Middle => 2,
Button::Right => 3,
}
}
use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
use ashpd::desktop::{PersistMode, Session};
use enumflags2::BitFlags;
use std::sync::mpsc;
struct LibeiDispatcher {
cmd_tx: mpsc::Sender<LibeiCmd>,
}
enum LibeiCmd {
Motion { x: f64, y: f64, ack: mpsc::Sender<EngineResult<()>> },
Button { code: u32, pressed: bool, ack: mpsc::Sender<EngineResult<()>> },
}
impl LibeiDispatcher {
fn try_new() -> Option<Self> {
let (cmd_tx, cmd_rx) = mpsc::channel::<LibeiCmd>();
let (init_tx, init_rx) = mpsc::sync_channel::<Option<()>>(1);
std::thread::Builder::new()
.name("vs-libei".to_string())
.spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
let _ = init_tx.send(None);
return;
};
rt.block_on(async move {
let proxy = match RemoteDesktop::new().await {
Ok(p) => p,
Err(_) => {
let _ = init_tx.send(None);
return;
}
};
let session: Session<'_, RemoteDesktop> = match proxy.create_session().await {
Ok(s) => s,
Err(_) => {
let _ = init_tx.send(None);
return;
}
};
let types: BitFlags<DeviceType> = DeviceType::Pointer.into();
if proxy
.select_devices(&session, types, None, PersistMode::DoNot)
.await
.and_then(|r| r.response())
.is_err()
{
let _ = init_tx.send(None);
return;
}
if proxy
.start(&session, None)
.await
.and_then(|r| r.response())
.is_err()
{
let _ = init_tx.send(None);
return;
}
let _ = init_tx.send(Some(()));
while let Ok(cmd) = cmd_rx.recv() {
match cmd {
LibeiCmd::Motion { x, y, ack } => {
let r = proxy
.notify_pointer_motion_absolute(&session, 0, x, y)
.await
.map_err(|e| EngineError::Other(format!("ei motion: {e}")));
let _ = ack.send(r);
}
LibeiCmd::Button { code, pressed, ack } => {
let state = if pressed {
ashpd::desktop::remote_desktop::KeyState::Pressed
} else {
ashpd::desktop::remote_desktop::KeyState::Released
};
#[allow(clippy::cast_possible_wrap)]
let r = proxy
.notify_pointer_button(&session, code as i32, state)
.await
.map_err(|e| EngineError::Other(format!("ei button: {e}")));
let _ = ack.send(r);
}
}
}
});
})
.ok()?;
init_rx.recv().ok().flatten()?;
Some(Self { cmd_tx })
}
}
impl InputDispatcher for LibeiDispatcher {
fn backend_name(&self) -> &'static str {
"libei"
}
fn dispatch(&self, ev: InputEvent) -> EngineResult<()> {
let (ack_tx, ack_rx) = mpsc::channel();
let cmd = match ev {
InputEvent::Move(p) => LibeiCmd::Motion {
x: f64::from(p.x),
y: f64::from(p.y),
ack: ack_tx,
},
InputEvent::Press(b) => LibeiCmd::Button {
code: linux_button_code(b),
pressed: true,
ack: ack_tx,
},
InputEvent::Release(b) => LibeiCmd::Button {
code: linux_button_code(b),
pressed: false,
ack: ack_tx,
},
};
self.cmd_tx
.send(cmd)
.map_err(|_| EngineError::Other("libei worker thread gone".into()))?;
ack_rx
.recv()
.map_err(|_| EngineError::Other("libei ack channel closed".into()))?
}
fn flush(&self) -> EngineResult<()> {
Ok(())
}
}
fn linux_button_code(b: Button) -> u32 {
match b {
Button::Left => 0x110,
Button::Middle => 0x112,
Button::Right => 0x111,
}
}