fix: read GTK4 a11y trees by pinning proxy destination (#31)#32
Conversation
get_app_state and list_apps returned an empty tree (one root with role "unknown", child_count 0) for GTK4 apps like Nautilus, Text Editor, and baobab, while GTK3/Chromium/Electron worked. Root cause is the atspi P2P trait, not GTK4 object-path parsing. snapshot_tree/read_node opened proxies via AccessibilityConnection::object_as_accessible. When an app advertises a peer-to-peer bus address it routes reads over that socket; when it does not, the fallback builds an AccessibleProxy with a path but no destination. On the shared a11y bus that proxy cannot address the app and every call fails with ServiceUnknown, which read_node swallows into role "unknown" / child_count 0. The working/broken split tracks P2P support exactly, not the toolkit: GTK3 gnome-disks advertises P2P and worked; GTK4 Nautilus does not implement the legacy GetApplicationBusAddress and broke. The crate deserializes GTK4 app-specific paths (/org/gnome/<App>/a11y/<uuid>) correctly once the proxy has a destination. Replace all object_as_accessible call sites with a small open_accessible helper backed by ObjectRefExt::as_accessible_proxy, which always pins the destination to the object's bus name. Verified live on GNOME 50: Nautilus goes from 1 node to a full 28-node tree, gnome-disks unchanged at 111 nodes.
There was a problem hiding this comment.
Code Review
This pull request fixes an issue where GTK4 applications return an empty accessibility tree by replacing the use of AccessibilityConnection::object_as_accessible with a new helper function open_accessible. This helper uses ObjectRefExt::as_accessible_proxy to ensure the destination is always pinned to the object's bus name, preventing failures on the shared accessibility bus. The reviewer provided a valuable suggestion to improve the lifetime annotations of the open_accessible function, recommending that the returned proxy's lifetime be tied to the connection rather than the temporary object reference.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async fn open_accessible<'r>( | ||
| conn: &AccessibilityConnection, | ||
| object_ref: &'r ObjectRefOwned, | ||
| ) -> Result<AccessibleProxy<'r>, atspi::AtspiError> { | ||
| object_ref.as_accessible_proxy(conn.connection()).await | ||
| } |
There was a problem hiding this comment.
The lifetime of the returned AccessibleProxy is conceptually tied to the zbus::Connection (borrowed from conn), not the ObjectRefOwned reference (object_ref).
By tying the returned proxy's lifetime to 'r (the lifetime of object_ref), you unnecessarily restrict the lifetime of the returned proxy to the scope of the temporary object_ref reference. Although this compiles in the current call sites due to covariance and localized usage, it is more idiomatic and robust to tie the returned proxy's lifetime to the connection 'conn.
| async fn open_accessible<'r>( | |
| conn: &AccessibilityConnection, | |
| object_ref: &'r ObjectRefOwned, | |
| ) -> Result<AccessibleProxy<'r>, atspi::AtspiError> { | |
| object_ref.as_accessible_proxy(conn.connection()).await | |
| } | |
| async fn open_accessible<'conn>( | |
| conn: &'conn AccessibilityConnection, | |
| object_ref: &ObjectRefOwned, | |
| ) -> Result<AccessibleProxy<'conn>, atspi::AtspiError> { | |
| object_ref.as_accessible_proxy(conn.connection()).await | |
| } |
Fixes #31.
What was happening
get_app_state/list_appsreturned an empty tree for GTK4 apps - a single root withrole: "unknown",child_count: 0- while GTK3 / Chromium / Electron worked.Root cause (not what the issue guessed)
The issue suspected the
atspicrate couldn't parse GTK4's app-specific object paths (/org/gnome/<App>/a11y/<uuid>). That's not it - the crate deserializes those paths fine.The reads went through the
atspiP2Ptrait'sobject_as_accessible. When an app advertises a peer-to-peer bus address, reads route over that socket. When it doesn't, the fallback builds anAccessibleProxywith a path but no destination. On the shared a11y bus that proxy can't address the app, so every call fails withServiceUnknown- whichread_nodeswallows intorole: "unknown"/child_count: 0.The working/broken split tracks P2P support, not the toolkit. Probed live on GNOME 50:
unknown/0)unknown/0)GTK4 apps just don't implement the legacy
GetApplicationBusAddress, so they always hit the broken fallback.Fix
Drop the
P2Ptrait and open proxies withObjectRefExt::as_accessible_proxy, which always pins the destination to the object's bus name. Oneopen_accessiblehelper, all six call sites swapped.Verification (live, GNOME 50 Wayland, real
snapshot_tree)/org/gnome/Nautilus/a11y/<uuid>paths,application -> window "Home" -> ...Local gates green:
cargo fmt --check,cargo check --locked --all-targets,cargo clippy --locked --all-targets -- -D warnings,cargo test(120 passed).