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