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