diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b9e33..9021fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- GTK4 applications (Nautilus, Text Editor, baobab, and others) now return their + full accessibility tree instead of a single `role: "unknown"` root with + `child_count: 0`. Reads were routed through the `atspi` `P2P` trait's + `object_as_accessible`, whose no-peer fallback builds a proxy with a path but + no destination; on the shared a11y bus that fails with `ServiceUnknown` for + any app that does not advertise a peer-to-peer bus address. Modern GTK4 apps + do not implement the legacy `GetApplicationBusAddress`, so they hit the broken + fallback while GTK3/Chromium/Electron apps kept working. Reads now use + `ObjectRefExt::as_accessible_proxy`, which always pins the destination to the + object's bus name. (#31) + ## [0.2.8] - 2026-06-17 ### Changed diff --git a/src/atspi_tree.rs b/src/atspi_tree.rs index 1a81f87..f9a60fb 100644 --- a/src/atspi_tree.rs +++ b/src/atspi_tree.rs @@ -1,8 +1,10 @@ use crate::diagnostics::hydrate_session_bus_env; use anyhow::{anyhow, Context, Result}; use atspi::{ - connection::P2P, - proxy::{accessible::AccessibleProxy, proxy_ext::ProxyExt}, + proxy::{ + accessible::{AccessibleProxy, ObjectRefExt}, + proxy_ext::ProxyExt, + }, AccessibilityConnection, CoordType, ObjectRef, ObjectRefOwned, StateSet, }; use schemars::JsonSchema; @@ -105,7 +107,7 @@ pub async fn list_accessible_apps(limit: usize) -> Result Result { let conn = connect().await?; let object_ref = object_ref_from_id(object_ref_id)?; - let proxy = conn - .object_as_accessible(&object_ref) + let proxy = open_accessible(&conn, &object_ref) .await .with_context(|| format!("failed to open AT-SPI object {object_ref_id}"))?; let action = proxy @@ -191,8 +192,7 @@ pub async fn perform_action( pub async fn set_element_value(object_ref_id: &str, value: &str) -> Result { let conn = connect().await?; let object_ref = object_ref_from_id(object_ref_id)?; - let proxy = conn - .object_as_accessible(&object_ref) + let proxy = open_accessible(&conn, &object_ref) .await .with_context(|| format!("failed to open AT-SPI object {object_ref_id}"))?; let proxies = proxy.proxies().await?; @@ -240,6 +240,25 @@ async fn connect() -> Result { .context("failed to connect to AT-SPI bus") } +/// Open an `AccessibleProxy` for an object on the a11y bus. +/// +/// We deliberately avoid `AccessibilityConnection::object_as_accessible` (the +/// `P2P` trait). For apps that advertise a peer-to-peer bus address it routes +/// reads over that socket, but for apps that don't (notably GTK4 apps such as +/// Nautilus / Text Editor / baobab, which don't implement the legacy +/// `GetApplicationBusAddress`) it falls back to a proxy built with only a path +/// and *no destination*. On the shared a11y bus that proxy can't address the +/// app and every call fails with `ServiceUnknown`, which surfaces as an empty +/// tree (`role: "unknown"`, `child_count: 0`). `as_accessible_proxy` always +/// pins the destination to the object's bus name, so it works for every app +/// regardless of P2P support. See issue #31. +async fn open_accessible<'r>( + conn: &AccessibilityConnection, + object_ref: &'r ObjectRefOwned, +) -> Result, atspi::AtspiError> { + object_ref.as_accessible_proxy(conn.connection()).await +} + async fn registry_children(conn: &AccessibilityConnection) -> Result> { let root = conn .root_accessible_on_registry() @@ -313,7 +332,7 @@ async fn root_matches( object_ref: &ObjectRefOwned, needle: &str, ) -> bool { - let Ok(proxy) = conn.object_as_accessible(object_ref).await else { + let Ok(proxy) = open_accessible(conn, object_ref).await else { return object_ref_id(object_ref) .to_ascii_lowercase() .contains(needle); @@ -325,7 +344,7 @@ async fn root_matches( let children = proxy.get_children().await.unwrap_or_default(); for child_ref in children.into_iter().take(8) { - let Ok(child_proxy) = conn.object_as_accessible(&child_ref).await else { + let Ok(child_proxy) = open_accessible(conn, &child_ref).await else { continue; }; if proxy_matches(&child_proxy, &child_ref, needle).await {