1use std::time::{Duration, Instant};
6
7use a7n_proto::v1::{InputEvent, KeyEvent};
8use tokio::sync::mpsc;
9
10#[cfg(all(unix, not(target_os = "macos")))]
11mod accumulate;
12#[cfg(all(unix, not(target_os = "macos")))]
13mod evdev_keymap;
14#[cfg(target_os = "macos")]
15pub(crate) mod macos;
16#[cfg(all(test, not(target_os = "macos")))]
17#[path = "input/macos/keymap.rs"]
18mod macos_keymap;
19#[cfg(all(test, not(target_os = "macos")))]
20#[path = "input/macos/capture/swipe.rs"]
22mod macos_swipe;
23mod scroll;
24#[cfg(all(unix, not(target_os = "macos")))]
25mod wayland;
26#[cfg(windows)]
27pub(crate) mod windows;
28#[cfg(all(unix, not(target_os = "macos")))]
29mod x11;
30
31pub const GESTURE_CHORDS_SUPPORTED: bool = cfg!(windows);
33pub const GESTURES_SUPPORTED: bool = cfg!(any(windows, target_os = "macos"));
35
36pub const KEEPALIVE_INTERVAL: Duration = Duration::from_millis(500);
37pub const DEADMAN_TIMEOUT: Duration = Duration::from_millis(1500);
38
39#[cfg(all(unix, not(target_os = "macos")))]
41const CAPTURE_EVENT_CAPACITY: usize = 1024;
42#[cfg(all(unix, not(target_os = "macos")))]
43const CAPTURE_CONTROL_CAPACITY: usize = 16;
44#[cfg(all(unix, not(target_os = "macos")))]
45const INJECT_COMMAND_CAPACITY: usize = 1024;
46#[cfg(all(unix, not(target_os = "macos")))]
48const INJECT_POINTER_CAPACITY: usize = 64;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Bounds {
52 pub x: i32,
53 pub y: i32,
54 pub width: u32,
55 pub height: u32,
56}
57
58#[derive(Debug)]
59pub enum Captured {
60 Pointer { x: i32, y: i32, dx: i32, dy: i32 },
61 Input(InputEvent),
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CaptureControl {
66 Grab(bool),
67 Keepalive,
68}
69
70#[derive(Debug)]
72pub struct Capture {
73 pub bounds: Bounds,
74 pub events: mpsc::Receiver<Captured>,
75 pub control: mpsc::Sender<CaptureControl>,
76}
77
78#[derive(Debug)]
79pub enum Inject {
80 Chord(Vec<KeyEvent>),
81 Event(InputEvent),
82 Motion { dx: i32, dy: i32 },
83 Warp { x: i32, y: i32 },
84}
85
86#[derive(Debug)]
87pub struct Injector {
88 pub bounds: Bounds,
89 pub commands: mpsc::Sender<Inject>,
90 pub pointer: mpsc::Receiver<Captured>,
92}
93
94#[derive(Debug, thiserror::Error)]
95pub enum InputError {
96 #[error("input capture and injection are not supported on this platform")]
97 Unsupported,
98 #[error("{0} permission missing")]
99 MissingPermission(&'static str),
100 #[cfg(all(unix, not(target_os = "macos")))]
101 #[error("Wayland portal failed: {0}")]
102 Portal(String),
103 #[cfg(target_os = "macos")]
104 #[error("macOS input failed: {0}")]
105 Macos(#[from] std::io::Error),
106 #[cfg(all(unix, not(target_os = "macos")))]
107 #[error("cannot connect to the X server: {0}")]
108 X11Connect(#[from] x11rb::errors::ConnectError),
109 #[cfg(all(unix, not(target_os = "macos")))]
110 #[error("X connection failed: {0}")]
111 X11Connection(#[from] x11rb::errors::ConnectionError),
112 #[cfg(all(unix, not(target_os = "macos")))]
113 #[error("X request failed: {0}")]
114 X11Reply(#[from] x11rb::errors::ReplyError),
115 #[cfg(all(unix, not(target_os = "macos")))]
116 #[error("the X server lacks the {0} extension")]
117 MissingX11Extension(&'static str),
118 #[error("cannot start an input thread: {0}")]
119 Thread(std::io::Error),
120 #[cfg(windows)]
121 #[error("Windows input failed: {0}")]
122 Windows(#[from] std::io::Error),
123}
124
125pub fn start() -> Result<(Capture, Injector), InputError> {
130 start_platform()
131}
132
133#[cfg(all(unix, not(target_os = "macos")))]
134fn start_platform() -> Result<(Capture, Injector), InputError> {
135 match detect_backend() {
136 Backend::Wayland => wayland::start(),
137 Backend::X11 => x11::start(),
138 Backend::None => Err(InputError::Unsupported),
139 }
140}
141
142#[cfg(all(unix, not(target_os = "macos")))]
144pub(crate) fn detect_backend() -> Backend {
145 let session_type = std::env::var("XDG_SESSION_TYPE").ok();
146 select_backend(
147 session_type.as_deref(),
148 env_set("DISPLAY"),
149 env_set("WAYLAND_DISPLAY"),
150 )
151}
152
153#[cfg(all(unix, not(target_os = "macos")))]
154fn env_set(name: &str) -> bool {
155 std::env::var_os(name).is_some_and(|value| !value.is_empty())
156}
157
158#[cfg(windows)]
159fn start_platform() -> Result<(Capture, Injector), InputError> {
160 windows::start()
161}
162
163#[cfg(target_os = "macos")]
164fn start_platform() -> Result<(Capture, Injector), InputError> {
165 macos::start()
166}
167
168#[cfg(not(any(windows, unix)))]
169fn start_platform() -> Result<(Capture, Injector), InputError> {
170 Err(InputError::Unsupported)
171}
172
173#[cfg(all(unix, not(target_os = "macos")))]
174#[derive(Debug, PartialEq, Eq)]
175pub(crate) enum Backend {
176 Wayland,
177 X11,
178 None,
179}
180
181#[cfg(all(unix, not(target_os = "macos")))]
184pub(crate) fn select_backend(
185 session_type: Option<&str>,
186 display_set: bool,
187 wayland_display_set: bool,
188) -> Backend {
189 if session_type == Some("wayland") {
190 return if wayland_display_set {
191 Backend::Wayland
192 } else {
193 Backend::None
194 };
195 }
196 if display_set {
197 Backend::X11
198 } else {
199 Backend::None
200 }
201}
202
203#[derive(Debug)]
205pub struct DeadMan {
206 last: Instant,
207}
208
209impl DeadMan {
210 #[must_use]
211 pub fn new(now: Instant) -> Self {
212 Self { last: now }
213 }
214
215 pub fn keepalive(&mut self, now: Instant) {
216 self.last = now;
217 }
218
219 #[must_use]
220 pub fn expired(&self, now: Instant) -> bool {
221 now.saturating_duration_since(self.last) >= DEADMAN_TIMEOUT
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[cfg(all(unix, not(target_os = "macos")))]
230 #[test]
231 fn select_backend_session_type_and_displays_pick_backend() {
232 let cases = [
234 (Some("wayland"), true, true, Backend::Wayland),
235 (Some("wayland"), false, true, Backend::Wayland),
236 (Some("wayland"), true, false, Backend::None),
237 (Some("x11"), false, false, Backend::None),
238 (Some("x11"), true, false, Backend::X11),
239 (Some("x11"), true, true, Backend::X11),
240 (None, true, false, Backend::X11),
241 (None, false, true, Backend::None),
242 ];
243
244 for (session_type, display_set, wayland_display_set, expected) in cases {
245 let backend = select_backend(session_type, display_set, wayland_display_set);
247
248 assert_eq!(
250 backend, expected,
251 "{session_type:?} display={display_set} wayland={wayland_display_set}"
252 );
253 }
254 }
255
256 #[test]
257 fn expired_keepalive_missing_releases_at_timeout() {
258 let now = Instant::now();
260 let mut deadman = DeadMan::new(now);
261 deadman.keepalive(now + KEEPALIVE_INTERVAL);
262
263 let expired = [
265 DEADMAN_TIMEOUT.saturating_sub(Duration::from_nanos(1)),
266 DEADMAN_TIMEOUT,
267 ]
268 .map(|elapsed| deadman.expired(now + KEEPALIVE_INTERVAL + elapsed));
269
270 assert_eq!(expired, [false, true]);
272 }
273}