use super::{A11yNode, A11yOptions, NodeId};
use crate::extensions::computer::{backend, state::Rect};
use zbus::{Connection, OwnedVariant};
const A11Y_BUS: &str = "org.a11y.Bus";
const A11Y_BUS_PATH: &str = "/org/a11y/bus";
const A11Y_ROOT: &str = "/org/a11y/atspi/accessible/root";
const IFACE_ACCESSIBLE: &str = "org.a11y.atspi.Accessible";
const IFACE_COMPONENT: &str = "org.a11y.atspi.Component";
const IFACE_ACTION: &str = "org.a11y.atspi.Action";
const IFACE_TEXT: &str = "org.a11y.atspi.Text";
const IFACE_EDITABLE_TEXT: &str = "org.a11y.atspi.EditableText";
const IFACE_VALUE: &str = "org.a11y.atspi.Value";
const IFACE_APPLICATION: &str = "org.a11y.atspi.Application";
const IFACE_SELECTION: &str = "org.a11y.atspi.Selection";
const IFACE_PROPS: &str = "org.freedesktop.DBus.Properties";
pub fn available() -> bool {
std::env::var_os("DBUS_SESSION_BUS_ADDRESS").is_some()
}
pub async fn connect() -> Result<Connection, String> {
let session = Connection::session()
.await
.map_err(|e| format!("no session bus: {e}"))?;
let reply = session
.call_method(
Some(A11Y_BUS),
A11Y_BUS_PATH,
Some(A11Y_BUS),
"GetAddress",
&(),
)
.await
.map_err(|e| {
format!(
"the accessibility bus is not available: {e}. Start it with `at-spi2-core` \
(systemd user unit or `at-spi-bus-launcher`) and retry"
)
})?;
let (address,): (String,) = reply
.body()
.deserialize()
.map_err(|e| format!("a11y bus address is not a string: {e}"))?;
zbus::connection::Builder::address(address.as_str())
.build()
.await
.map_err(|e| format!("could not connect to the accessibility bus: {e}"))
}
async fn call<T>(
conn: &Connection,
dest: &str,
path: &str,
iface: &str,
method: &str,
) -> Result<T, String>
where
T: for<'d> serde::Deserialize<'d> + zbus::Type,
{
let reply = conn
.call_method(Some(dest), path, Some(iface), method, &())
.await
.map_err(|e| format!("{iface}.{method} on {path}: {e}"))?;
reply
.body()
.deserialize()
.map_err(|e| format!("{iface}.{method} on {path}: unexpected reply: {e}"))
}
async fn call_with<B, T>(
conn: &Connection,
dest: &str,
path: &str,
iface: &str,
method: &str,
body: &B,
) -> Result<T, String>
where
B: serde::Serialize + zbus::DynamicType,
T: for<'d> serde::Deserialize<'d> + zbus::Type,
{
let reply = conn
.call_method(Some(dest), path, Some(iface), method, body)
.await
.map_err(|e| format!("{iface}.{method} on {path}: {e}"))?;
reply
.body()
.deserialize()
.map_err(|e| format!("{iface}.{method} on {path}: unexpected reply: {e}"))
}
async fn string_property(
conn: &Connection,
dest: &str,
path: &str,
iface: &str,
name: &str,
) -> Result<String, String> {
let (value,): (OwnedVariant,) =
call_with(conn, dest, path, IFACE_PROPS, "Get", &(iface, name)).await?;
match String::try_from(value) {
Ok(text) => Ok(text),
Err(_) => Ok(String::new()),
}
}
async fn int_property(
conn: &Connection,
dest: &str,
path: &str,
iface: &str,
name: &str,
) -> Result<i64, String> {
let (value,): (OwnedVariant,) =
call_with(conn, dest, path, IFACE_PROPS, "Get", &(iface, name)).await?;
if let Ok(v) = i32::try_from(
value
.try_clone()
.unwrap_or_else(|_| OwnedVariant::from(0i32)),
) {
return Ok(i64::from(v));
}
if let Ok(v) = i64::try_from(
value
.try_clone()
.unwrap_or_else(|_| OwnedVariant::from(0i32)),
) {
return Ok(v);
}
if let Ok(v) = u32::try_from(value) {
return Ok(i64::from(v));
}
Ok(0)
}
async fn children(
conn: &Connection,
dest: &str,
path: &str,
) -> Result<Vec<(String, String)>, String> {
let (list,): (Vec<(String, zbus::OwnedObjectPath)>,) =
call(conn, dest, path, IFACE_ACCESSIBLE, "GetChildren").await?;
Ok(list
.into_iter()
.map(|(bus, object)| (bus, object.to_string()))
.collect())
}
async fn role_name(conn: &Connection, dest: &str, path: &str) -> Result<String, String> {
let (role,): (String,) = call(conn, dest, path, IFACE_ACCESSIBLE, "GetRoleName").await?;
Ok(role)
}
async fn interfaces(conn: &Connection, dest: &str, path: &str) -> Result<Vec<String>, String> {
let (list,): (Vec<String>,) = call(conn, dest, path, IFACE_ACCESSIBLE, "GetInterfaces").await?;
Ok(list)
}
async fn extents(conn: &Connection, dest: &str, path: &str) -> Option<Rect> {
let reply = conn
.call_method(
Some(dest),
path,
Some(IFACE_COMPONENT),
"GetExtents",
&(0u32,),
)
.await
.ok()?;
let (x, y, width, height): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
if width <= 0 || height <= 0 {
return None;
}
Some(Rect {
x,
y,
width: width as u32,
height: height as u32,
})
}
async fn action_names(conn: &Connection, dest: &str, path: &str) -> Vec<String> {
let reply = conn
.call_method(Some(dest), path, Some(IFACE_ACTION), "GetActions", &())
.await;
let Ok(reply) = reply else {
return Vec::new();
};
let Ok(actions) = reply
.body()
.deserialize::<(Vec<(String, String, String)>,)>()
else {
return Vec::new();
};
actions.0.into_iter().map(|(name, _, _)| name).collect()
}
fn state_names(bits: &[u32]) -> Vec<String> {
const NAMES: [&str; 32] = [
"invalid",
"active",
"armed",
"busy",
"checked",
"collapsed",
"defunct",
"editable",
"enabled",
"expandable",
"expanded",
"focusable",
"focused",
"has_tooltip",
"horizontal",
"iconified",
"modal",
"multi_line",
"multiselectable",
"opaque",
"pressed",
"resizable",
"selectable",
"selected",
"sensitive",
"showing",
"single_line",
"stale",
"transient",
"vertical",
"visible",
"manages_descendants",
];
let mut out = Vec::new();
for (word_index, word) in bits.iter().enumerate() {
for bit in 0..32u32 {
if word & (1 << bit) == 0 {
continue;
}
let index = word_index * 32 + bit as usize;
if let Some(name) = NAMES.get(index) {
out.push((*name).to_string());
}
}
}
out
}
async fn states(conn: &Connection, dest: &str, path: &str) -> Vec<String> {
match call::<(Vec<u32>,)>(conn, dest, path, IFACE_ACCESSIBLE, "GetState").await {
Ok((bits,)) => state_names(&bits),
Err(_) => Vec::new(),
}
}
struct Walk<'a> {
conn: &'a Connection,
options: &'a A11yOptions,
visited: usize,
nodes: Vec<NodeId>,
out: Vec<A11yNode>,
}
impl Walk<'_> {
async fn node(&mut self, bus: &str, path: &str, depth: usize) {
if self.visited >= self.options.max_visited || self.nodes.len() >= self.options.max_nodes {
return;
}
self.visited += 1;
let conn = self.conn;
let name = string_property(conn, bus, path, IFACE_ACCESSIBLE, "Name")
.await
.unwrap_or_default();
let description = string_property(conn, bus, path, IFACE_ACCESSIBLE, "Description")
.await
.unwrap_or_default();
let accessible_id = string_property(conn, bus, path, IFACE_ACCESSIBLE, "AccessibleId")
.await
.unwrap_or_default();
let role = role_name(conn, bus, path)
.await
.unwrap_or_else(|_| "unknown".to_string());
let interfaces = interfaces(conn, bus, path).await.unwrap_or_default();
let actions = if self.options.patterns {
action_names(conn, bus, path).await
} else {
Vec::new()
};
let rect = extents(conn, bus, path).await;
let states = states(conn, bus, path).await;
let id = NodeId {
bus: bus.to_string(),
path: path.to_string(),
name: name.clone(),
role: role.clone(),
};
let index = self.nodes.len();
self.nodes.push(id);
self.out.push(A11yNode {
index,
depth,
role,
name,
description,
automation_id: accessible_id,
states,
rect,
actions,
interfaces,
has_children: false,
});
if depth >= self.options.max_depth {
return;
}
let Ok(list) = children(conn, bus, path).await else {
return;
};
if !list.is_empty()
&& let Some(node) = self.out.last_mut()
{
node.has_children = true;
}
for (child_bus, child_path) in list {
if self.visited >= self.options.max_visited
|| self.nodes.len() >= self.options.max_nodes
{
break;
}
Box::pin(self.node(&child_bus, &child_path, depth + 1)).await;
}
}
}
pub async fn read_tree(
window: Option<u64>,
options: A11yOptions,
) -> Result<(Vec<A11yNode>, Vec<NodeId>), String> {
let conn = connect().await?;
let target = match window {
Some(id) => Some(backend::window(id)?),
None => backend::windows()?.into_iter().find(|w| w.focused),
};
let (root_bus, root_path) = match &target {
Some(window) => application_for_pid(&conn, window.pid).await?,
None => {
("".to_string(), A11Y_ROOT.to_string())
}
};
let (bus, path) = if root_bus.is_empty() {
(String::new(), root_path)
} else {
(root_bus, root_path)
};
let mut state = Walk {
conn: &conn,
options: &options,
visited: 0,
nodes: Vec::new(),
out: Vec::new(),
};
if bus.is_empty() {
let list = children(&conn, "org.a11y.atspi.Registry", &path).await?;
for (child_bus, child_path) in list {
if state.nodes.len() >= options.max_nodes {
break;
}
state.node(&child_bus, &child_path, 0).await;
}
} else {
state.node(&bus, &path, 0).await;
}
let Walk { nodes, out, .. } = state;
if out.is_empty() {
return Err(match target {
Some(w) => format!(
"window {} ({:?}) exposes no accessibility tree; the application may not have accessibility enabled",
w.id, w.app
),
None => "nothing is focused, and the desktop exposes no accessibility tree".to_string(),
});
}
Ok((out, nodes))
}
async fn application_for_pid(conn: &Connection, pid: u32) -> Result<(String, String), String> {
let list = children(conn, "org.a11y.atspi.Registry", A11Y_ROOT).await?;
for (bus, path) in list {
let id = int_property(conn, &bus, &path, IFACE_APPLICATION, "Id")
.await
.unwrap_or(0);
if id == i64::from(pid) {
return Ok((bus, path));
}
}
Err(format!(
"no accessibility application reports pid {pid}; the window may belong to an app without accessibility enabled \
(Electron needs --force-renderer-accessibility)"
))
}
pub async fn do_action(target: &NodeId, index: u32) -> Result<(), String> {
let conn = connect().await?;
let (ok,): (bool,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_ACTION,
"DoAction",
&(index,),
)
.await?;
if ok {
Ok(())
} else {
Err(format!(
"the application refused action {index} on {}",
target.name
))
}
}
pub async fn read_text(target: &NodeId) -> Result<String, String> {
let conn = connect().await?;
let (count,): (i32,) = call(
&conn,
&target.bus,
&target.path,
IFACE_TEXT,
"GetCharacterCount",
)
.await
.unwrap_or((0,));
if count <= 0 {
return Ok(String::new());
}
let (text,): (String,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_TEXT,
"GetText",
&(0i32, count),
)
.await?;
Ok(text)
}
pub async fn set_text(target: &NodeId, text: &str) -> Result<(), String> {
let conn = connect().await?;
let result: Result<(bool,), String> = call_with(
&conn,
&target.bus,
&target.path,
IFACE_EDITABLE_TEXT,
"SetTextContents",
&(text,),
)
.await;
match result {
Ok((true,)) => Ok(()),
_ => {
let numeric = text.trim().parse::<f64>().map_err(|_| {
format!(
"{} does not accept text: it implements neither EditableText nor a numeric Value",
target.name
)
})?;
let (_,): (bool,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_VALUE,
"SetCurrentValue",
&(numeric,),
)
.await?;
Ok(())
}
}
}
pub async fn read_value(target: &NodeId) -> Result<String, String> {
let conn = connect().await?;
let (value,): (OwnedVariant,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_PROPS,
"Get",
&(IFACE_VALUE, "CurrentValue"),
)
.await?;
if let Ok(v) = f64::try_from(value) {
return Ok(format!("{v}"));
}
read_text(target).await
}
pub async fn grab_focus(target: &NodeId) -> Result<(), String> {
let conn = connect().await?;
let (ok,): (bool,) = call(
&conn,
&target.bus,
&target.path,
IFACE_COMPONENT,
"GrabFocus",
)
.await?;
if ok {
Ok(())
} else {
Err(format!("{} refused focus", target.name))
}
}
pub async fn select_text(target: &NodeId, start: i32, end: i32) -> Result<(), String> {
let conn = connect().await?;
let (ok,): (bool,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_TEXT,
"SetSelection",
&(0i32, start, end),
)
.await?;
if ok {
Ok(())
} else {
Err(format!("{} refused the selection", target.name))
}
}
pub async fn select_child(target: &NodeId, index: i32) -> Result<(), String> {
let conn = connect().await?;
let (ok,): (bool,) = call_with(
&conn,
&target.bus,
&target.path,
IFACE_SELECTION,
"SelectChild",
&(index,),
)
.await?;
if ok {
Ok(())
} else {
Err(format!("{} refused selecting child {index}", target.name))
}
}
pub async fn caret_offset(target: &NodeId) -> Option<i32> {
let conn = connect().await.ok()?;
let (offset,): (i32,) = call(
&conn,
&target.bus,
&target.path,
IFACE_TEXT,
"GetCaretOffset",
)
.await
.ok()?;
Some(offset)
}
pub async fn action_by_name(target: &NodeId, wanted: &str) -> Result<(), String> {
let conn = connect().await?;
let reply = conn
.call_method(
Some(target.bus.as_str()),
target.path.as_str(),
Some(IFACE_ACTION),
"GetActions",
&(),
)
.await
.map_err(|e| format!("{IFACE_ACTION}.GetActions: {e}"))?;
let (actions,): (Vec<(String, String, String)>,) = reply
.body()
.deserialize()
.map_err(|e| format!("{IFACE_ACTION}.GetActions: {e}"))?;
let position = actions.iter().position(|(name, _, _)| {
let name = name.to_ascii_lowercase();
name == wanted.to_ascii_lowercase() || name.contains(&wanted.to_ascii_lowercase())
});
match position {
Some(index) => do_action(target, index as u32).await,
None => Err(format!(
"{} has no {wanted:?} action (available: {})",
target.name,
actions
.iter()
.map(|(name, _, _)| name.as_str())
.collect::<Vec<_>>()
.join(", ")
)),
}
}
pub async fn node_rect(target: &NodeId) -> Option<Rect> {
let conn = connect().await.ok()?;
extents(&conn, &target.bus, &target.path).await
}