Rust / macOS accessibility
AXUIElement::system_wide() in Rust, and the trap the docs leave out
If you searched the accessibility crate on docs.rs you found a one-line signature and no example. This is the part the reference page cannot tell you: what system_wide() is actually for, when to reach for application(pid) instead, and why the crate's own tree walker will quietly skip an app's windows.
Direct answer · verified 2026-06-15
In the accessibility crate (v0.2.0), AXUIElement::system_wide() is an associated function:
pub fn system_wide() -> SelfIt returns the global system-wide accessibility root, wrapping the C function AXUIElementCreateSystemWide() from accessibility-sys. Use it to read the system focused element. To walk a single app's UI, use AXUIElement::application(pid) instead.
The constructors, and which one you actually want
The struct gives you four ways to get an AXUIElement to start from: system_wide(), application(pid), application_with_bundle(bundle_id), and application_with_bundle_timeout(bundle_id, timeout). People reach for system_wide() first because it is the one that needs no arguments. That is usually the wrong move. Here is the difference that matters:
| Feature | application(pid) | system_wide() |
|---|---|---|
| What it returns | The single global system-wide accessibility root | The accessibility root of one running application |
| Signature | pub fn system_wide() -> Self | pub fn application(pid: pid_t) -> Self |
| Underlying C call | AXUIElementCreateSystemWide() | AXUIElementCreateApplication(pid) |
| Good for | Reading the system-wide focused element (AXFocusedUIElement) and global attributes | Walking a specific app's window and control tree |
| windows() on it | Returns nothing useful, the system root has no windows | Returns the app's AXWindow elements you can descend into |
| Can fail | No, it is infallible, returns Self | No on construction, but attribute reads fail without permission |
Column 1 is system_wide(); column 2 is application(pid). Both return AXUIElement; only the second is a useful starting point for walking one app's tree.
The single legitimate reason to start from system_wide() is the focused-element query. Reading the AXFocusedUIElement attribute on the system root tells you which control has keyboard focus right now, across every app on the machine. That is the global hook editors and clipboard tools use. For anything scoped to a known process, take the pid and call application(pid).
Why system_wide() “works” but reads nothing
The first surprise is that system_wide() cannot fail. Its return type is Self, not Result<Self, Error>. So the handle always looks fine. The failure shows up one call later, when you read an attribute and macOS refuses because your process was never granted Accessibility permission. The element is real; the data behind it is gated.
use accessibility::{AXUIElement, AXAttribute};
// Infallible: you always get an element back.
let system = AXUIElement::system_wide();
// Fallible: this is where missing permission bites.
match system.attribute(&AXAttribute::focused_uielement()) {
Ok(focused) => { /* the control with keyboard focus */ }
Err(e) => {
// Not a panic. An Err you must handle.
eprintln!("AX read failed (permission?): {e:?}");
}
}Check the grant up front so you can prompt the user cleanly instead of failing deep in a tree walk. These are the preconditions before any attribute() call returns real data:
Preconditions for a useful AXUIElement
- The host process is granted Accessibility permission in System Settings, Privacy and Security, Accessibility.
- You check it at runtime with AXIsProcessTrusted() before relying on any attribute read.
- You handle the error path: without trust, system_wide() still returns an element, but attribute() calls come back as an Err, not a panic.
- For a single app you have the target's pid, so you can call AXUIElement::application(pid) instead of filtering the whole system.
The trap: the stock TreeWalker does not traverse windows
This is the part no reference page mentions, and the reason this guide exists. The accessibility crate ships a TreeWalker. If you point it at an application's AXUIElement and expect it to enumerate the windows and controls, you will get an almost-empty tree. The reason: on macOS an application's windows are reached through dedicated accessors, not reliably through the generic AXChildren attribute the stock walker follows.
Terminator hit this and left the explanation as the literal first line of its walker file, crates/terminator/src/platforms/tree_search.rs:
/// TLDR: default TreeWalker does not traverse windows,
/// so we need to traverse windows manually
use accessibility::{AXAttribute, AXUIElement, AXUIElementAttributes, Error};The fix is a custom walker, TreeWalkerWithWindows, that visits things in a deliberate order. For each element it tries the windows first, then the main window, then the ordinary children:
How TreeWalkerWithWindows descends one element
root.windows()
Ask the application element for its AXWindows array. The stock TreeWalker never does this, which is the whole reason a custom walker exists.
root.main_window()
Some apps expose a main window that is not in the windows() array. Walk it too, guarding against the duplicate.
attribute(children)
Only after the windows are handled, descend into AXChildren the ordinary way.
dedupe via CFEqual / CFHash
AX trees contain cycles. Every visited element goes into a HashSet keyed on Core Foundation identity so the walk terminates.
// crates/terminator/src/platforms/tree_search.rs (abridged)
if flow == TreeWalkerFlow::Continue {
// 1. windows() - the step the stock walker skips
if let Ok(windows) = &root.windows() {
for window in windows.iter() {
self.walk_one(&window, visitor);
}
}
// 2. main_window() - some apps expose it outside windows()
if let Ok(main_window) = root.main_window() {
self.walk_one(&main_window, visitor);
}
// 3. only now, the generic children attribute
if let Ok(children) = root.attribute(&self.attr_children) {
for child in children.into_iter() {
self.walk_one(&child, visitor);
}
}
}And it is a graph, so it has cycles
Once you walk windows manually you hit the second surprise: the accessibility tree is not a tree. The same element can show up under more than one parent, so a naive recursive descent loops forever. Terminator dedupes by wrapping every AXUIElement in an AXUIElementWrapper and keeping a HashSet of what it has already seen. The wrapper's identity is the underlying Core Foundation object, compared with CFEqual and hashed with CFHash, not a Rust pointer:
impl PartialEq for AXUIElementWrapper {
fn eq(&self, other: &Self) -> bool {
unsafe {
let a = self.element.as_concrete_TypeRef();
let b = other.element.as_concrete_TypeRef();
core_foundation::base::CFEqual(a as _, b as _) != 0
}
}
}
impl Hash for AXUIElementWrapper {
fn hash<H: Hasher>(&self, state: &mut H) {
unsafe {
let r = self.element.as_concrete_TypeRef();
state.write_u64(core_foundation::base::CFHash(r as _) as u64);
}
}
}Before recursing, the walker checks the set; a repeat returns SkipSubtree and increments a cycle counter you can log. There is also a hard MAX_DEPTH of 100 as a backstop, and the element finder polls on an implicit-wait deadline, sleeping in 250 ms slices until the target appears or time runs out. None of this is in the crate docs; all of it is in tree_search.rs, MIT licensed, ready to copy.
If you do not want to maintain this yourself
Terminator is an open-source desktop automation framework that wraps all of this behind a Playwright-shaped API for the whole OS. It uses the same accessibility crate on macOS and the native UIAutomation layer on Windows, so the windows-aware walk, the cycle dedup, the permission handling, and the cross-platform selector grammar are already solved. You write locator("name:Save").click() instead of hand-rolling another tree walker. The source for the macOS path is exactly the file quoted above.
Building on the macOS accessibility API?
Talk through your AXUIElement tree-walk, permission, or cross-platform pain with the people who wrote the walker.
Frequently asked questions
Frequently asked questions
What does AXUIElement::system_wide() return in the accessibility crate?
In the accessibility crate version 0.2.0 it is an associated function with the signature pub fn system_wide() -> Self. It returns an AXUIElement that represents the single global system-wide accessibility root. Under the hood it calls AXUIElementCreateSystemWide() from the accessibility-sys crate, whose C signature is pub unsafe extern "C" fn AXUIElementCreateSystemWide() -> AXUIElementRef. The system-wide element is the right handle when you want the system focused element (the AXFocusedUIElement attribute, which points at whatever control currently has keyboard focus, regardless of which app owns it). It is not a good handle for walking a particular application, because it does not expose that application's windows.
Should I use system_wide() or application(pid) to automate an app?
Almost always application(pid). AXUIElement::application(pid: pid_t) -> Self returns the accessibility root of one process, and crucially its windows() method returns that app's AXWindow elements, which you then descend into. system_wide() is for global queries like 'what has focus right now.' If you start from system_wide() and try to find a button inside a specific app, you have to go system root, find the running application, then its windows, then the tree. Starting from application(pid) skips the first two hops. There is also application_with_bundle(bundle_id: &str) -> Result<Self, Error> if you only know the bundle identifier rather than the pid.
Why do attribute reads fail even though system_wide() succeeds?
Because system_wide() is infallible by signature, it returns Self, not a Result. The permission gate shows up later. macOS only lets a process read the accessibility tree if the user has granted it Accessibility permission in System Settings. If that permission is missing, you still get a valid-looking AXUIElement back, but the first attribute() call returns an Err (an AXError that surfaces as the crate's Error type). Check AXIsProcessTrusted() at startup so you can show a clear permission prompt instead of failing deep in a tree walk.
Does the accessibility crate's built-in TreeWalker work out of the box?
Not for application trees. The very first line of Terminator's tree_search.rs is a comment that reads: 'TLDR: default TreeWalker does not traverse windows, so we need to traverse windows manually.' The stock TreeWalker descends into AXChildren, but an application's windows are reached through the windows() and main_window() accessors, not always through AXChildren. So Terminator ships TreeWalkerWithWindows, which calls root.windows() first, then root.main_window(), then falls back to the children attribute. If you use the stock walker on an application element you will often see an empty or truncated tree.
How do you avoid infinite loops when walking the AX tree?
The macOS accessibility tree is a graph, not a strict tree: an element can appear as a child of more than one parent, which creates cycles. Terminator's walker wraps each AXUIElement in an AXUIElementWrapper and stores visited elements in a HashSet. The wrapper implements PartialEq using Core Foundation's CFEqual and Hash using CFHash, so identity is based on the underlying CF object, not on a Rust pointer. Before recursing into an element the walker checks the set; if the element was already seen it returns SkipSubtree and bumps a cycle counter. There is also a MAX_DEPTH constant of 100 as a hard backstop.
Where can I read a real implementation that uses AXUIElement?
crates/terminator/src/platforms/tree_search.rs in the open-source Terminator repo. It imports use accessibility::{AXAttribute, AXUIElement, AXUIElementAttributes, Error}, which is the same accessibility crate published on docs.rs. The file contains TreeWalkerWithWindows (the windows-aware walker), ElementFinderWithWindows (a walker that polls with an implicit wait and sleeps in 250 ms slices until a deadline), and the AXUIElementWrapper cycle-detection type. It is MIT licensed, so you can copy the pattern. The repo is at github.com/mediar-ai/terminator.
Keep reading
The macOS accessibility automation API
How the AX layer exposes the macOS UI as a queryable tree, and what that buys you over screenshots.
Accessibility API desktop automation
The write path: firing control patterns and actions on elements without moving the mouse.
Walking the macOS accessibility UI tree
Roles, attributes, and how a structural tree walk beats pixel matching for finding controls.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.