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