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