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