use std::time::{Duration, Instant};
use a7n_proto::v1::InputEvent;
use tokio::sync::mpsc;
#[cfg(all(unix, not(target_os = "macos")))]
mod accumulate;
#[cfg(all(unix, not(target_os = "macos")))]
mod evdev_keymap;
#[cfg(target_os = "macos")]
pub(crate) mod macos;
#[cfg(all(test, not(target_os = "macos")))]
#[path = "input/macos/keymap.rs"]
mod macos_keymap;
mod scroll;
#[cfg(all(unix, not(target_os = "macos")))]
mod wayland;
#[cfg(windows)]
pub(crate) mod windows;
#[cfg(all(unix, not(target_os = "macos")))]
mod x11;
pub const KEEPALIVE_INTERVAL: Duration = Duration::from_millis(500);
pub const DEADMAN_TIMEOUT: Duration = Duration::from_millis(1500);
#[cfg(all(unix, not(target_os = "macos")))]
const CAPTURE_EVENT_CAPACITY: usize = 1024;
#[cfg(all(unix, not(target_os = "macos")))]
const CAPTURE_CONTROL_CAPACITY: usize = 16;
#[cfg(all(unix, not(target_os = "macos")))]
const INJECT_COMMAND_CAPACITY: usize = 1024;
#[cfg(all(unix, not(target_os = "macos")))]
const INJECT_POINTER_CAPACITY: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bounds {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
#[derive(Debug)]
pub enum Captured {
Pointer { x: i32, y: i32, dx: i32, dy: i32 },
Input(InputEvent),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureControl {
Grab(bool),
Keepalive,
}
#[derive(Debug)]
pub struct Capture {
pub bounds: Bounds,
pub events: mpsc::Receiver<Captured>,
pub control: mpsc::Sender<CaptureControl>,
}
#[derive(Debug)]
pub enum Inject {
Event(InputEvent),
Motion { dx: i32, dy: i32 },
Warp { x: i32, y: i32 },
}
#[derive(Debug)]
pub struct Injector {
pub bounds: Bounds,
pub commands: mpsc::Sender<Inject>,
pub pointer: mpsc::Receiver<Captured>,
}
#[derive(Debug, thiserror::Error)]
pub enum InputError {
#[error("input capture and injection are not supported on this platform")]
Unsupported,
#[error("{0} permission missing")]
MissingPermission(&'static str),
#[cfg(all(unix, not(target_os = "macos")))]
#[error("Wayland portal failed: {0}")]
Portal(String),
#[cfg(target_os = "macos")]
#[error("macOS input failed: {0}")]
Macos(#[from] std::io::Error),
#[cfg(all(unix, not(target_os = "macos")))]
#[error("cannot connect to the X server: {0}")]
X11Connect(#[from] x11rb::errors::ConnectError),
#[cfg(all(unix, not(target_os = "macos")))]
#[error("X connection failed: {0}")]
X11Connection(#[from] x11rb::errors::ConnectionError),
#[cfg(all(unix, not(target_os = "macos")))]
#[error("X request failed: {0}")]
X11Reply(#[from] x11rb::errors::ReplyError),
#[cfg(all(unix, not(target_os = "macos")))]
#[error("the X server lacks the {0} extension")]
MissingX11Extension(&'static str),
#[error("cannot start an input thread: {0}")]
Thread(std::io::Error),
#[cfg(windows)]
#[error("Windows input failed: {0}")]
Windows(#[from] std::io::Error),
}
pub fn start() -> Result<(Capture, Injector), InputError> {
start_platform()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn start_platform() -> Result<(Capture, Injector), InputError> {
match detect_backend() {
Backend::Wayland => wayland::start(),
Backend::X11 => x11::start(),
Backend::None => Err(InputError::Unsupported),
}
}
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) fn detect_backend() -> Backend {
let session_type = std::env::var("XDG_SESSION_TYPE").ok();
select_backend(
session_type.as_deref(),
env_set("DISPLAY"),
env_set("WAYLAND_DISPLAY"),
)
}
#[cfg(all(unix, not(target_os = "macos")))]
fn env_set(name: &str) -> bool {
std::env::var_os(name).is_some_and(|value| !value.is_empty())
}
#[cfg(windows)]
fn start_platform() -> Result<(Capture, Injector), InputError> {
windows::start()
}
#[cfg(target_os = "macos")]
fn start_platform() -> Result<(Capture, Injector), InputError> {
macos::start()
}
#[cfg(not(any(windows, unix)))]
fn start_platform() -> Result<(Capture, Injector), InputError> {
Err(InputError::Unsupported)
}
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Backend {
Wayland,
X11,
None,
}
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) fn select_backend(
session_type: Option<&str>,
display_set: bool,
wayland_display_set: bool,
) -> Backend {
if session_type == Some("wayland") {
return if wayland_display_set {
Backend::Wayland
} else {
Backend::None
};
}
if display_set {
Backend::X11
} else {
Backend::None
}
}
#[derive(Debug)]
pub struct DeadMan {
last: Instant,
}
impl DeadMan {
#[must_use]
pub fn new(now: Instant) -> Self {
Self { last: now }
}
pub fn keepalive(&mut self, now: Instant) {
self.last = now;
}
#[must_use]
pub fn expired(&self, now: Instant) -> bool {
now.saturating_duration_since(self.last) >= DEADMAN_TIMEOUT
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn select_backend_session_type_and_displays_pick_backend() {
let cases = [
(Some("wayland"), true, true, Backend::Wayland),
(Some("wayland"), false, true, Backend::Wayland),
(Some("wayland"), true, false, Backend::None),
(Some("x11"), false, false, Backend::None),
(Some("x11"), true, false, Backend::X11),
(Some("x11"), true, true, Backend::X11),
(None, true, false, Backend::X11),
(None, false, true, Backend::None),
];
for (session_type, display_set, wayland_display_set, expected) in cases {
let backend = select_backend(session_type, display_set, wayland_display_set);
assert_eq!(
backend, expected,
"{session_type:?} display={display_set} wayland={wayland_display_set}"
);
}
}
#[test]
fn expired_keepalive_missing_releases_at_timeout() {
let now = Instant::now();
let mut deadman = DeadMan::new(now);
deadman.keepalive(now + KEEPALIVE_INTERVAL);
let expired = [
DEADMAN_TIMEOUT.saturating_sub(Duration::from_nanos(1)),
DEADMAN_TIMEOUT,
]
.map(|elapsed| deadman.expired(now + KEEPALIVE_INTERVAL + elapsed));
assert_eq!(expired, [false, true]);
}
}