use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use zbus::blocking::fdo::DBusProxy;
use zbus::blocking::{Connection, MessageIterator, Proxy};
use zbus::message::Type as MessageType;
use zbus::zvariant::OwnedValue;
use zbus::MatchRule;
use xa11y_core::{
CancelHandle, ElementData, Error, Event, EventKind, EventReceiver, Result, Role, StateFlag,
StateSet, Subscription, Toggled,
};
use crate::atspi::{map_atspi_role, map_atspi_role_number, AccessibleRef, LinuxProvider};
pub(crate) const EVENT_INTERFACES: &[&str] = &[
"org.a11y.atspi.Event.Object",
"org.a11y.atspi.Event.Window",
"org.a11y.atspi.Event.Focus",
"org.a11y.atspi.Event.Document",
];
pub(crate) fn subscribe_for_pid(
provider: &LinuxProvider,
pid: u32,
app_name: String,
) -> Result<Subscription> {
let app_ref = provider.find_app_by_pid(pid)?;
let sender_bus = app_ref.bus_name;
let conn = LinuxProvider::connect_a11y_bus()?;
let dbus = DBusProxy::new(&conn).map_err(|e| Error::Platform {
code: -1,
message: format!("DBusProxy: {e}"),
})?;
let mut rules: Vec<MatchRule<'static>> = Vec::with_capacity(EVENT_INTERFACES.len());
for iface in EVENT_INTERFACES {
let rule = MatchRule::builder()
.msg_type(MessageType::Signal)
.sender(sender_bus.clone())
.map_err(|e| Error::Platform {
code: -1,
message: format!("sender match rule: {e}"),
})?
.interface((*iface).to_string())
.map_err(|e| Error::Platform {
code: -1,
message: format!("interface match rule: {e}"),
})?
.build();
dbus.add_match_rule(rule.clone())
.map_err(|e| Error::Platform {
code: -1,
message: format!("add_match_rule({iface}): {e}"),
})?;
rules.push(rule);
}
let (tx, rx) = std::sync::mpsc::channel::<Event>();
let ctx = Arc::new(EventContext {
sender_bus: sender_bus.clone(),
app_name,
app_pid: pid,
tx: Mutex::new(tx),
conn: conn.clone(),
});
let iter = MessageIterator::from(conn.clone());
let stop = Arc::new(AtomicBool::new(false));
let stop_for_thread = stop.clone();
let ctx_for_thread = ctx.clone();
let handle = thread::spawn(move || {
for msg in iter {
if stop_for_thread.load(Ordering::Relaxed) {
break;
}
if let Ok(msg) = msg {
ctx_for_thread.dispatch(&msg);
}
}
});
let cancel_conn = conn.clone();
let cancel = CancelHandle::new(move || {
stop.store(true, Ordering::Relaxed);
if let Ok(dbus) = DBusProxy::new(&cancel_conn) {
for rule in &rules {
let _ = dbus.remove_match_rule(rule.clone());
}
}
let _ = cancel_conn.call_method(
Some("org.freedesktop.DBus"),
"/org/freedesktop/DBus",
Some("org.freedesktop.DBus.Peer"),
"Ping",
&(),
);
let _ = handle.join();
});
Ok(Subscription::new(EventReceiver::new(rx), cancel))
}
struct EventContext {
sender_bus: String,
app_name: String,
app_pid: u32,
tx: Mutex<std::sync::mpsc::Sender<Event>>,
conn: Connection,
}
impl EventContext {
fn emit(&self, kind: EventKind, target: Option<ElementData>) {
let event = Event {
kind,
target,
app_name: self.app_name.clone(),
app_pid: self.app_pid,
timestamp: std::time::Instant::now(),
};
if let Ok(tx) = self.tx.lock() {
let _ = tx.send(event);
}
}
fn dispatch(&self, msg: &zbus::Message) {
let header = msg.header();
let Some(sender) = header.sender() else {
return;
};
if sender.as_str() != self.sender_bus {
return;
}
let Some(iface) = header.interface() else {
return;
};
let Some(member) = header.member() else {
return;
};
let Some(path) = header.path() else {
return;
};
let body = msg.body();
let (detail, detail1, _detail2, _any, _props): (
String,
i32,
i32,
OwnedValue,
HashMap<String, OwnedValue>,
) = match body.deserialize() {
Ok(v) => v,
Err(_) => return,
};
let target_ref = AccessibleRef {
bus_name: self.sender_bus.clone(),
path: path.to_string(),
};
let target = build_event_snapshot(&self.conn, &target_ref, Some(self.app_pid));
let target_role = target.as_ref().map(|t| t.role);
let kinds = signal_to_kinds(
iface.as_str(),
member.as_str(),
&detail,
detail1,
target_role,
);
for kind in kinds {
self.emit(kind, target.clone());
}
}
}
pub(crate) fn signal_to_kinds(
iface: &str,
member: &str,
detail: &str,
detail1: i32,
target_role: Option<Role>,
) -> Vec<EventKind> {
match (iface, member) {
("org.a11y.atspi.Event.Focus", "Focus") => vec![EventKind::FocusChanged],
("org.a11y.atspi.Event.Object", "StateChanged") => {
let value = detail1 != 0;
let lower = detail.to_ascii_lowercase();
match lower.as_str() {
"focused" => {
if value {
vec![
EventKind::FocusChanged,
EventKind::StateChanged {
flag: StateFlag::Focused,
value,
},
]
} else {
vec![EventKind::StateChanged {
flag: StateFlag::Focused,
value,
}]
}
}
"checked" => vec![EventKind::StateChanged {
flag: StateFlag::Checked,
value,
}],
"enabled" | "sensitive" => vec![EventKind::StateChanged {
flag: StateFlag::Enabled,
value,
}],
"visible" | "showing" => vec![EventKind::StateChanged {
flag: StateFlag::Visible,
value,
}],
"expanded" => vec![EventKind::StateChanged {
flag: StateFlag::Expanded,
value,
}],
"selected" => vec![EventKind::StateChanged {
flag: StateFlag::Selected,
value,
}],
"busy" => vec![EventKind::StateChanged {
flag: StateFlag::Busy,
value,
}],
"editable" => vec![EventKind::StateChanged {
flag: StateFlag::Editable,
value,
}],
"focusable" => vec![EventKind::StateChanged {
flag: StateFlag::Focusable,
value,
}],
"modal" => vec![EventKind::StateChanged {
flag: StateFlag::Modal,
value,
}],
"required" => vec![EventKind::StateChanged {
flag: StateFlag::Required,
value,
}],
_ => vec![],
}
}
("org.a11y.atspi.Event.Object", "PropertyChange") => {
let d = detail.to_ascii_lowercase();
if d == "accessible-name" || d == "name" {
vec![EventKind::NameChanged]
} else if d == "accessible-value" || d == "value" {
let mut kinds = vec![EventKind::ValueChanged];
if matches!(target_role, Some(Role::TextField | Role::TextArea)) {
kinds.push(EventKind::TextChanged);
}
kinds
} else {
vec![]
}
}
("org.a11y.atspi.Event.Object", "ChildrenChanged") => vec![EventKind::StructureChanged],
("org.a11y.atspi.Event.Object", "TextChanged") => vec![EventKind::TextChanged],
("org.a11y.atspi.Event.Object", "TextSelectionChanged") => {
vec![EventKind::SelectionChanged]
}
("org.a11y.atspi.Event.Object", "TextAttributesChanged") => vec![],
("org.a11y.atspi.Event.Object", "SelectionChanged") => vec![EventKind::SelectionChanged],
("org.a11y.atspi.Event.Object", "ActiveDescendantChanged") => {
vec![EventKind::SelectionChanged]
}
("org.a11y.atspi.Event.Object", "ValueChanged") => {
let mut kinds = vec![EventKind::ValueChanged];
if matches!(target_role, Some(Role::TextField | Role::TextArea)) {
kinds.push(EventKind::TextChanged);
}
kinds
}
("org.a11y.atspi.Event.Object", "Announcement") => vec![EventKind::Announcement],
(
"org.a11y.atspi.Event.Object",
"BoundsChanged" | "VisibleDataChanged" | "AttributesChanged" | "ModelChanged"
| "ColumnReordered" | "RowReordered" | "ColumnInserted" | "RowInserted"
| "ColumnDeleted" | "RowDeleted",
) => vec![],
("org.a11y.atspi.Event.Window", "Create") => vec![EventKind::WindowOpened],
("org.a11y.atspi.Event.Window", "Destroy") => vec![EventKind::WindowClosed],
("org.a11y.atspi.Event.Window", "Activate") => vec![EventKind::WindowActivated],
("org.a11y.atspi.Event.Window", "Deactivate") => vec![EventKind::WindowDeactivated],
("org.a11y.atspi.Event.Window", "Minimize") => vec![EventKind::WindowDeactivated],
("org.a11y.atspi.Event.Window", "Restore") => vec![EventKind::WindowActivated],
("org.a11y.atspi.Event.Window", _) => vec![],
("org.a11y.atspi.Event.Document", _) => vec![],
_ => vec![],
}
}
fn build_event_snapshot(
conn: &Connection,
aref: &AccessibleRef,
pid: Option<u32>,
) -> Option<ElementData> {
let role_name = get_role_name(conn, aref).unwrap_or_default();
let role_num = if role_name.is_empty() {
get_role_number(conn, aref).unwrap_or(0)
} else {
0
};
let mut role = if !role_name.is_empty() {
let r = map_atspi_role(&role_name);
if r == Role::Unknown {
let n = get_role_number(conn, aref).unwrap_or(0);
map_atspi_role_number(n)
} else {
r
}
} else {
map_atspi_role_number(role_num)
};
let state_bits = get_state(conn, aref).unwrap_or_default();
let bits = bits_from_u32s(&state_bits);
if role == Role::TextArea {
const MULTI_LINE: u64 = 1 << 17;
if (bits & MULTI_LINE) == 0 {
role = Role::TextField;
}
}
let name = get_name(conn, aref).filter(|s| !s.is_empty());
let value = get_value(conn, aref, role);
let (numeric_value, min_value, max_value) = if matches!(
role,
Role::Slider | Role::ProgressBar | Role::ScrollBar | Role::SpinButton
) {
get_numeric(conn, aref)
} else {
(None, None, None)
};
let states = states_from_bits(bits, role);
let raw = {
let raw_role = if role_name.is_empty() {
format!("role_num:{}", role_num)
} else {
role_name
};
let mut raw = HashMap::new();
raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
raw.insert(
"bus_name".into(),
serde_json::Value::String(aref.bus_name.clone()),
);
raw.insert(
"object_path".into(),
serde_json::Value::String(aref.path.clone()),
);
raw
};
Some(ElementData {
role,
name,
value,
description: None,
bounds: None,
actions: vec![],
states,
numeric_value,
min_value,
max_value,
stable_id: Some(aref.path.clone()),
pid,
raw,
handle: 0,
})
}
fn make_proxy<'a>(conn: &'a Connection, bus: &str, path: &str, iface: &str) -> Option<Proxy<'a>> {
zbus::blocking::proxy::Builder::<Proxy>::new(conn)
.destination(bus.to_owned())
.ok()?
.path(path.to_owned())
.ok()?
.interface(iface.to_owned())
.ok()?
.cache_properties(zbus::proxy::CacheProperties::No)
.build()
.ok()
}
fn get_role_name(conn: &Connection, aref: &AccessibleRef) -> Option<String> {
let proxy = make_proxy(
conn,
&aref.bus_name,
&aref.path,
"org.a11y.atspi.Accessible",
)?;
let reply = proxy.call_method("GetRoleName", &()).ok()?;
reply.body().deserialize::<String>().ok()
}
fn get_role_number(conn: &Connection, aref: &AccessibleRef) -> Option<u32> {
let proxy = make_proxy(
conn,
&aref.bus_name,
&aref.path,
"org.a11y.atspi.Accessible",
)?;
let reply = proxy.call_method("GetRole", &()).ok()?;
reply.body().deserialize::<u32>().ok()
}
fn get_name(conn: &Connection, aref: &AccessibleRef) -> Option<String> {
let proxy = make_proxy(
conn,
&aref.bus_name,
&aref.path,
"org.a11y.atspi.Accessible",
)?;
proxy.get_property::<String>("Name").ok()
}
fn get_state(conn: &Connection, aref: &AccessibleRef) -> Option<Vec<u32>> {
let proxy = make_proxy(
conn,
&aref.bus_name,
&aref.path,
"org.a11y.atspi.Accessible",
)?;
let reply = proxy.call_method("GetState", &()).ok()?;
reply.body().deserialize::<Vec<u32>>().ok()
}
fn get_value(conn: &Connection, aref: &AccessibleRef, role: Role) -> Option<String> {
if matches!(
role,
Role::Application
| Role::Window
| Role::Dialog
| Role::Group
| Role::MenuBar
| Role::Toolbar
| Role::TabGroup
| Role::SplitGroup
| Role::Table
| Role::TableRow
| Role::Separator
) {
return None;
}
if let Some(proxy) = make_proxy(conn, &aref.bus_name, &aref.path, "org.a11y.atspi.Text") {
if let Ok(char_count) = proxy.get_property::<i32>("CharacterCount") {
if char_count > 0 {
if let Ok(reply) = proxy.call_method("GetText", &(0i32, char_count)) {
if let Ok(text) = reply.body().deserialize::<String>() {
if !text.is_empty() {
return Some(text);
}
}
}
}
}
}
if let Some(proxy) = make_proxy(conn, &aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
if let Ok(v) = proxy.get_property::<f64>("CurrentValue") {
return Some(v.to_string());
}
}
None
}
fn get_numeric(conn: &Connection, aref: &AccessibleRef) -> (Option<f64>, Option<f64>, Option<f64>) {
let Some(proxy) = make_proxy(conn, &aref.bus_name, &aref.path, "org.a11y.atspi.Value") else {
return (None, None, None);
};
(
proxy.get_property::<f64>("CurrentValue").ok(),
proxy.get_property::<f64>("MinimumValue").ok(),
proxy.get_property::<f64>("MaximumValue").ok(),
)
}
fn bits_from_u32s(state_bits: &[u32]) -> u64 {
match state_bits.len() {
0 => 0,
1 => state_bits[0] as u64,
_ => (state_bits[0] as u64) | ((state_bits[1] as u64) << 32),
}
}
fn states_from_bits(bits: u64, role: Role) -> StateSet {
const BUSY: u64 = 1 << 3;
const CHECKED: u64 = 1 << 4;
const EDITABLE: u64 = 1 << 7;
const ENABLED: u64 = 1 << 8;
const EXPANDABLE: u64 = 1 << 9;
const EXPANDED: u64 = 1 << 10;
const FOCUSABLE: u64 = 1 << 11;
const FOCUSED: u64 = 1 << 12;
const MODAL: u64 = 1 << 16;
const SELECTED: u64 = 1 << 23;
const SENSITIVE: u64 = 1 << 24;
const SHOWING: u64 = 1 << 25;
const VISIBLE: u64 = 1 << 30;
const INDETERMINATE: u64 = 1 << 32;
const REQUIRED: u64 = 1 << 33;
let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
let checked = match role {
Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
if (bits & INDETERMINATE) != 0 {
Some(Toggled::Mixed)
} else if (bits & CHECKED) != 0 {
Some(Toggled::On)
} else {
Some(Toggled::Off)
}
}
_ => None,
};
let expanded = if (bits & EXPANDABLE) != 0 {
Some((bits & EXPANDED) != 0)
} else {
None
};
StateSet {
enabled,
visible,
focused: (bits & FOCUSED) != 0,
checked,
selected: (bits & SELECTED) != 0,
expanded,
editable: (bits & EDITABLE) != 0,
focusable: (bits & FOCUSABLE) != 0,
modal: (bits & MODAL) != 0,
required: (bits & REQUIRED) != 0,
busy: (bits & BUSY) != 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_signal_emits_focus_changed() {
let kinds = signal_to_kinds("org.a11y.atspi.Event.Focus", "Focus", "", 0, None);
assert_eq!(kinds, vec![EventKind::FocusChanged]);
}
#[test]
fn state_changed_focused_true_emits_both_kinds() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"StateChanged",
"focused",
1,
None,
);
assert!(kinds.contains(&EventKind::FocusChanged));
assert!(kinds.iter().any(|k| matches!(
k,
EventKind::StateChanged {
flag: StateFlag::Focused,
value: true
}
)));
}
#[test]
fn state_changed_focused_false_is_state_only() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"StateChanged",
"focused",
0,
None,
);
assert_eq!(
kinds,
vec![EventKind::StateChanged {
flag: StateFlag::Focused,
value: false
}]
);
}
#[test]
fn state_changed_checked_maps_value() {
let on = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"StateChanged",
"checked",
1,
None,
);
assert_eq!(
on,
vec![EventKind::StateChanged {
flag: StateFlag::Checked,
value: true
}]
);
let off = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"StateChanged",
"checked",
0,
None,
);
assert_eq!(
off,
vec![EventKind::StateChanged {
flag: StateFlag::Checked,
value: false
}]
);
}
#[test]
fn state_changed_enabled_and_sensitive_collapse() {
for name in ["enabled", "sensitive"] {
let kinds =
signal_to_kinds("org.a11y.atspi.Event.Object", "StateChanged", name, 1, None);
assert_eq!(
kinds,
vec![EventKind::StateChanged {
flag: StateFlag::Enabled,
value: true
}],
"state '{name}' should map to Enabled"
);
}
}
#[test]
fn state_changed_unknown_state_is_dropped() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"StateChanged",
"expandable",
1,
None,
);
assert!(kinds.is_empty());
}
#[test]
fn children_changed_emits_structure_changed() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"ChildrenChanged",
"add",
0,
None,
);
assert_eq!(kinds, vec![EventKind::StructureChanged]);
}
#[test]
fn property_change_name_emits_name_changed() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"PropertyChange",
"accessible-name",
0,
None,
);
assert_eq!(kinds, vec![EventKind::NameChanged]);
}
#[test]
fn property_change_description_is_dropped() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"PropertyChange",
"accessible-description",
0,
None,
);
assert!(kinds.is_empty());
}
#[test]
fn property_change_value_emits_value_changed() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"PropertyChange",
"accessible-value",
0,
Some(Role::Slider),
);
assert_eq!(kinds, vec![EventKind::ValueChanged]);
}
#[test]
fn property_change_value_on_text_role_also_emits_text_changed() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"PropertyChange",
"accessible-value",
0,
Some(Role::TextField),
);
assert!(kinds.contains(&EventKind::ValueChanged));
assert!(kinds.contains(&EventKind::TextChanged));
}
#[test]
fn text_changed_maps_directly() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"TextChanged",
"insert",
0,
None,
);
assert_eq!(kinds, vec![EventKind::TextChanged]);
}
#[test]
fn value_changed_on_text_role_also_emits_text_changed() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"ValueChanged",
"",
0,
Some(Role::TextField),
);
assert!(kinds.contains(&EventKind::ValueChanged));
assert!(kinds.contains(&EventKind::TextChanged));
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"ValueChanged",
"",
0,
Some(Role::TextArea),
);
assert!(kinds.contains(&EventKind::TextChanged));
}
#[test]
fn value_changed_on_slider_is_value_only() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"ValueChanged",
"",
0,
Some(Role::Slider),
);
assert_eq!(kinds, vec![EventKind::ValueChanged]);
}
#[test]
fn window_signals_map_to_window_kinds() {
let cases = [
("Create", EventKind::WindowOpened),
("Destroy", EventKind::WindowClosed),
("Activate", EventKind::WindowActivated),
("Deactivate", EventKind::WindowDeactivated),
("Minimize", EventKind::WindowDeactivated),
("Restore", EventKind::WindowActivated),
];
for (member, expected) in cases {
let kinds = signal_to_kinds("org.a11y.atspi.Event.Window", member, "", 0, None);
assert_eq!(
kinds,
vec![expected.clone()],
"window member '{member}' should map to {expected:?}",
);
}
}
#[test]
fn selection_changed_maps_to_selection() {
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"SelectionChanged",
"",
0,
None,
);
assert_eq!(kinds, vec![EventKind::SelectionChanged]);
let kinds = signal_to_kinds(
"org.a11y.atspi.Event.Object",
"TextSelectionChanged",
"",
0,
None,
);
assert_eq!(kinds, vec![EventKind::SelectionChanged]);
}
#[test]
fn announcement_maps_to_announcement() {
let kinds = signal_to_kinds("org.a11y.atspi.Event.Object", "Announcement", "", 0, None);
assert_eq!(kinds, vec![EventKind::Announcement]);
}
#[test]
fn unrecognised_signals_are_dropped() {
assert!(signal_to_kinds("org.a11y.atspi.Event.Window", "Move", "", 0, None).is_empty());
assert!(
signal_to_kinds("org.a11y.atspi.Event.Object", "BoundsChanged", "", 0, None).is_empty()
);
assert!(signal_to_kinds("com.example.OtherBus", "Whatever", "", 0, None).is_empty());
}
#[test]
fn event_interfaces_covers_design_doc() {
assert!(EVENT_INTERFACES.contains(&"org.a11y.atspi.Event.Object"));
assert!(EVENT_INTERFACES.contains(&"org.a11y.atspi.Event.Window"));
assert!(EVENT_INTERFACES.contains(&"org.a11y.atspi.Event.Focus"));
assert!(EVENT_INTERFACES.contains(&"org.a11y.atspi.Event.Document"));
}
#[test]
fn bits_from_u32s_handles_short_and_long_arrays() {
assert_eq!(bits_from_u32s(&[]), 0);
assert_eq!(bits_from_u32s(&[0x1234]), 0x1234);
assert_eq!(bits_from_u32s(&[0x11, 0x22]), 0x22_0000_0011u64);
}
#[test]
fn states_from_bits_checked_requires_toggleable_role() {
const CHECKED: u64 = 1 << 4;
let s = states_from_bits(CHECKED, Role::Button);
assert!(s.checked.is_none());
let s = states_from_bits(CHECKED, Role::CheckBox);
assert_eq!(s.checked, Some(Toggled::On));
}
#[test]
fn states_from_bits_collapses_enabled_sensitive() {
const ENABLED: u64 = 1 << 8;
const SENSITIVE: u64 = 1 << 24;
assert!(states_from_bits(ENABLED, Role::Button).enabled);
assert!(states_from_bits(SENSITIVE, Role::Button).enabled);
assert!(!states_from_bits(0, Role::Button).enabled);
}
}