Skip to main content

xa11y/
lib.rs

1//! xa11y — Cross-Platform Accessibility Client Library
2//!
3//! Provides a unified API for reading and interacting with accessibility trees
4//! across desktop platforms (macOS, Windows, Linux).
5//!
6//! # Quick Start
7//!
8//! ```no_run
9//! use std::time::Duration;
10//! use xa11y::*;
11//!
12//! let app = App::by_name("Safari", Duration::from_secs(5)).expect("App not found");
13//!
14//! for child in app.children().unwrap() {
15//!     println!("{}: {:?}", child.role, child.name);
16//! }
17//!
18//! app.locator(r#"button[name="OK"]"#).press().expect("Failed to press");
19//! ```
20
21use std::sync::{Arc, OnceLock};
22
23// Re-export public types.
24pub use xa11y_core::{
25    App, Element, ElementData, ElementState, Error, Event, EventKind, Locator, RawPlatformData,
26    Rect, Result, Role, StateFlag, StateSet, Subscription, SubscriptionIter, Toggled, TreeNode,
27};
28
29// Re-export input simulation surface.
30pub use xa11y_core::input;
31pub use xa11y_core::{
32    anchor_point, point_for, Anchor, ClickOptions, ClickTarget, DragOptions, InputProvider,
33    InputSim, IntoPoint, Key, Keyboard, Mouse, MouseButton, Point, ScrollDelta,
34};
35
36// Re-export screenshot surface.
37pub use xa11y_core::screenshot;
38pub use xa11y_core::{Screenshot, ScreenshotProvider};
39
40// Re-export bidi text helpers (see `xa11y_core::text`). `name`, `value`, and
41// `description` on `ElementData` are stripped of bidi format controls; these
42// helpers let callers strip ad-hoc strings or check membership.
43pub use xa11y_core::{is_bidi_control, strip_bidi, strip_bidi_opt};
44
45// Implementation details used by platform backends and Python bindings.
46#[doc(hidden)]
47pub use xa11y_core::{CancelHandle, EventReceiver, Provider, RecvStatus, Selector, SelectorGroup};
48
49/// Shared in-memory mock Provider — re-exported from `xa11y-core` when the
50/// `test-support` feature is enabled. Used by language-binding tests so
51/// Python and JS don't each carry their own copy of the fixture.
52#[cfg(feature = "test-support")]
53#[doc(hidden)]
54pub use xa11y_core::mock;
55
56#[doc(hidden)]
57pub mod cli;
58
59// Re-export the extension trait so `use xa11y::*` enables `App::by_name(...)`.
60pub use app_ext::AppExt;
61
62// ── Internal singleton ──────────────────────────────────────────────────────
63
64static PROVIDER: OnceLock<std::result::Result<&'static dyn Provider, String>> = OnceLock::new();
65
66fn get_provider_ref() -> Result<&'static dyn Provider> {
67    PROVIDER
68        .get_or_init(|| {
69            create_provider_boxed()
70                .map(|b| &*Box::leak(b))
71                .map_err(|e| format!("{e}"))
72        })
73        .as_ref()
74        .copied()
75        .map_err(|msg| Error::Platform {
76            code: -1,
77            message: msg.clone(),
78        })
79}
80
81#[doc(hidden)]
82pub fn provider() -> Result<Arc<dyn Provider>> {
83    Ok(Arc::new(get_provider_ref()?))
84}
85
86// ── Platform provider construction (internal) ───────────────────────────────
87
88#[doc(hidden)]
89#[cfg(feature = "testing")]
90pub fn create_provider() -> Result<Arc<dyn Provider>> {
91    create_provider_boxed().map(Arc::from)
92}
93
94/// Build an [`InputSim`] backed by the platform's native input-synthesis API
95/// (CGEvent on macOS, SendInput on Windows, XTest on X11). Returns
96/// [`Error::Unsupported`] on a Wayland-only Linux session and
97/// [`Error::Platform`] on any other platform we don't ship a backend for.
98///
99/// `InputSim` is cheap to clone — construct one and share it.
100pub fn input_sim() -> Result<InputSim> {
101    #[cfg(target_os = "macos")]
102    {
103        let backend = xa11y_macos::MacOSInputProvider::new()?;
104        Ok(InputSim::new(Arc::new(backend)))
105    }
106    #[cfg(target_os = "windows")]
107    {
108        let backend = xa11y_windows::WindowsInputProvider::new()?;
109        Ok(InputSim::new(Arc::new(backend)))
110    }
111    #[cfg(target_os = "linux")]
112    {
113        let backend = xa11y_linux::LinuxInputProvider::new()?;
114        Ok(InputSim::new(Arc::new(backend)))
115    }
116    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
117    {
118        Err(Error::Platform {
119            code: -1,
120            message: format!(
121                "Input simulation not available on platform: {}",
122                std::env::consts::OS
123            ),
124        })
125    }
126}
127
128// ── Screenshot entry points ────────────────────────────────────────────
129//
130// Three bare functions instead of a factory + handle. The platform backend
131// (ScreenCaptureKit on macOS, X11 `GetImage` or xdg-desktop-portal on Linux,
132// GDI on Windows) is initialised lazily on first call and memoized in a
133// `OnceLock`, so repeated captures reuse the same backend without paying
134// construction cost per call.
135//
136// All three return:
137// - [`Error::PermissionDenied`] on macOS if Screen Recording is not granted
138//   (or on Linux if the Wayland portal denies consent).
139// - [`Error::Unsupported`] on Linux if neither `DISPLAY` nor `WAYLAND_DISPLAY`
140//   is set, and on older Windows contexts where `BitBlt` is unavailable.
141
142static SCREENSHOT_BACKEND: OnceLock<std::result::Result<Arc<dyn ScreenshotProvider>, String>> =
143    OnceLock::new();
144
145fn screenshot_backend() -> Result<Arc<dyn ScreenshotProvider>> {
146    SCREENSHOT_BACKEND
147        .get_or_init(create_screenshot_backend)
148        .as_ref()
149        .cloned()
150        .map_err(|msg| Error::Platform {
151            code: -1,
152            message: msg.clone(),
153        })
154}
155
156fn create_screenshot_backend() -> std::result::Result<Arc<dyn ScreenshotProvider>, String> {
157    #[cfg(target_os = "macos")]
158    {
159        xa11y_macos::MacOSScreenshot::new()
160            .map(|b| Arc::new(b) as Arc<dyn ScreenshotProvider>)
161            .map_err(|e| format!("{e}"))
162    }
163    #[cfg(target_os = "windows")]
164    {
165        xa11y_windows::WindowsScreenshot::new()
166            .map(|b| Arc::new(b) as Arc<dyn ScreenshotProvider>)
167            .map_err(|e| format!("{e}"))
168    }
169    #[cfg(target_os = "linux")]
170    {
171        xa11y_linux::LinuxScreenshot::new()
172            .map(|b| Arc::new(b) as Arc<dyn ScreenshotProvider>)
173            .map_err(|e| format!("{e}"))
174    }
175    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
176    {
177        Err(format!(
178            "Screenshot not available on platform: {}",
179            std::env::consts::OS
180        ))
181    }
182}
183
184/// Capture the full primary display.
185pub fn screenshot() -> Result<Screenshot> {
186    screenshot_backend()?.capture_full()
187}
188
189/// Capture an explicit sub-rectangle of the screen.
190pub fn screenshot_region(rect: Rect) -> Result<Screenshot> {
191    screenshot_backend()?.capture_region(rect)
192}
193
194/// Capture the pixels under an element's current bounds.
195///
196/// Returns [`Error::NoElementBounds`] if the element has no bounds. The target
197/// window is **not** raised or activated — see the `screenshot` module docs.
198pub fn screenshot_element(element: &Element) -> Result<Screenshot> {
199    let rect = element.bounds.ok_or(Error::NoElementBounds)?;
200    screenshot_backend()?.capture_region(rect)
201}
202
203fn create_provider_boxed() -> Result<Box<dyn Provider>> {
204    #[cfg(target_os = "macos")]
205    {
206        Ok(Box::new(xa11y_macos::MacOSProvider::new()?))
207    }
208    #[cfg(target_os = "windows")]
209    {
210        Ok(Box::new(xa11y_windows::WindowsProvider::new()?))
211    }
212    #[cfg(target_os = "linux")]
213    {
214        Ok(Box::new(xa11y_linux::LinuxProvider::new()?))
215    }
216    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
217    {
218        Err(Error::Platform {
219            code: -1,
220            message: format!("Unsupported platform: {}", std::env::consts::OS),
221        })
222    }
223}
224
225// ── AppExt extension trait ───────────────────────────────────────────────────
226
227mod app_ext {
228    use std::time::Duration;
229
230    use super::{provider, App, Result};
231
232    /// Extension trait that adds singleton-based constructors to [`App`].
233    ///
234    /// Imported automatically via `use xa11y::*`.
235    ///
236    /// # Example
237    /// ```no_run
238    /// use std::time::Duration;
239    /// use xa11y::*;
240    ///
241    /// let app = App::by_name("Safari", Duration::from_secs(5))?;
242    /// # Ok::<(), xa11y::Error>(())
243    /// ```
244    pub trait AppExt: Sized {
245        /// Find an application by exact name using the global singleton
246        /// provider, polling until it appears or `timeout` elapses. Pass
247        /// `Duration::ZERO` for a single attempt with no waiting. See
248        /// [`App::by_name_with`] for retry semantics.
249        fn by_name(name: &str, timeout: Duration) -> Result<Self>;
250        /// Find an application by process ID using the global singleton
251        /// provider, polling until it appears or `timeout` elapses. See
252        /// [`by_name`](Self::by_name) for retry semantics.
253        fn by_pid(pid: u32, timeout: Duration) -> Result<Self>;
254        /// List all running applications using the global singleton provider.
255        fn list() -> Result<Vec<Self>>;
256    }
257
258    impl AppExt for App {
259        fn by_name(name: &str, timeout: Duration) -> Result<Self> {
260            App::by_name_with(provider()?, name, timeout)
261        }
262
263        fn by_pid(pid: u32, timeout: Duration) -> Result<Self> {
264            App::by_pid_with(provider()?, pid, timeout)
265        }
266
267        fn list() -> Result<Vec<Self>> {
268            App::list_with(provider()?)
269        }
270    }
271}