Skip to content

Events

Accessibility APIs emit events when the interface changes: elements appearing or disappearing, focus moving, names or values updating. app.subscribe() returns a live stream of those events for one application.

A subscription carries every event for its application, with no filters and no selectors. Filter on the receiving side.

The type name is spelled snake_case in Python and the CLI, camelCase in JavaScript, and is a Rust enum variant of EventKind.

Rust EventKind Python event_type JavaScript Fires when
FocusChanged focus_changed focusChanged Keyboard focus moved to a new element. Target is the element that gained focus.
ValueChanged value_changed valueChanged An element’s value changed. Covers slider position, text contents, checkbox state, spin buttons, and progress.
NameChanged name_changed nameChanged An element’s name or label changed.
StateChanged state_changed stateChanged A boolean state flag changed. Carries flag and value, both always populated.
StructureChanged structure_changed structureChanged Children were added or removed, or the tree was otherwise invalidated. Target is the parent, when known.
WindowOpened window_opened windowOpened A new window was created.
WindowClosed window_closed windowClosed A window was closed. Target is a snapshot taken at destruction time, so some attributes may be absent.
WindowActivated window_activated windowActivated A window became the active window.
WindowDeactivated window_deactivated windowDeactivated A window lost active status.
SelectionChanged selection_changed selectionChanged Selection changed in a list, table, or other container. Target is the container, not the selected items.
MenuOpened menu_opened menuOpened A menu became visible.
MenuClosed menu_closed menuClosed A menu was dismissed.
TextChanged text_changed textChanged Text content changed in an editable element. Re-query the target’s value for the current contents.
Announcement announcement announcement A live region update, alert, or explicit announcement request was posted.

A variant includes extra data only where that data is present on every supporting platform. For everything else, re-query the target element after receipt. TextChanged carries no delta because macOS AXValueChanged supplies none, and Announcement carries no text because Windows UIA_LiveRegionChangedEventId supplies none.

Event Gap
MenuOpened / MenuClosed Not reliably emitted on Linux. These will not fire there.
WindowActivated / WindowDeactivated Not emitted on Windows. UIA has no first-class event, and inferring it from focus changes was tried and removed as lossy.
StateChanged Linux delivers all state bits. Windows delivers IsEnabled, ToggleState, and ExpandCollapseState; per-item selection arrives as SelectionChanged rather than as a state flag. macOS delivers Checked and Busy alone. No public app-level macOS notification reports Enabled, so it will never fire there.

The flag carried by StateChanged:

enabled, visible, focused, checked, selected, expanded, editable, focusable, modal, required, busy.

Field Type Meaning
kind (Rust) / event_type (Python) / type (JavaScript) EventKind / string What happened
target Element snapshot, optional The element that triggered the event. Absent when the element is unavailable or already destroyed.
app_name string Name of the application that produced the event
app_pid integer Process ID of that application
timestamp monotonic instant Time of receipt. Rust only.

Python flattens the state_changed payload onto the event itself as state_flag and state_value, which are None for every other event type.

app.subscribe() returns a Subscription, a pull-based stream.

let calc = App::by_name("Calculator", Duration::from_secs(5))?;
let sub = calc.subscribe()?;
// Block until a specific event arrives
let event = sub.wait_for(
|e| e.kind == EventKind::FocusChanged,
Duration::from_secs(5),
)?;
println!("Focus moved to: {:?}", event.target);
// Or poll without blocking
if let Some(event) = sub.try_recv() {
println!("{:?}", event.kind);
}
// Or iterate (blocks until next event)
for event in sub.iter() {
println!("{:?}: {}", event.kind, event.app_name);
}

Drop the Subscription to stop listening. It is Send and not Clone, so move it to another thread when you need it there.

Method Behaviour
try_recv() Returns Option<Event> without blocking
recv(timeout) Blocks up to timeout for the next event
wait_for(predicate, timeout) Blocks until an event satisfies predicate
iter() Blocking iterator over the stream

xa11y events --app NAME streams the same events to stdout until interrupted. See CLI.

Subscription.wait_for raises Timeout when the timeout expires with no matching event. The diagnosis reports how many non-matching events were seen and says whether the stream stopped delivering or disconnected. See Errors and diagnosis.

A stream that disconnects before the timeout is a different failure: the Python binding raises PlatformError rather than waiting out the clock.