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