diff --git a/atspi-proxies/src/accessible.rs b/atspi-proxies/src/accessible.rs index 1cf12914..20afea23 100644 --- a/atspi-proxies/src/accessible.rs +++ b/atspi-proxies/src/accessible.rs @@ -265,6 +265,18 @@ pub trait ObjectRefExt { &self, conn: &zbus::Connection, ) -> impl std::future::Future, zbus::Error>> + Send; + + /// Returns an [`AccessibleProxy`], the handle to the object's `Accessible` interface. + /// + /// # Errors + /// + /// `UniqueName` or `ObjectPath` are assumed to be valid because they are obtained from a valid `ObjectRef`. + /// If the builder is lacking the necessary parameters to build a proxy. See [`zbus::ProxyBuilder::build`]. + /// If this method fails, you may want to check the `AccessibleProxy` default values for missing / invalid parameters. + fn into_accessible_proxy( + self, + conn: &zbus::Connection, + ) -> impl std::future::Future, zbus::Error>> + Send; } impl ObjectRefExt for ObjectRef { @@ -272,17 +284,21 @@ impl ObjectRefExt for ObjectRef { &self, conn: &zbus::Connection, ) -> Result, zbus::Error> { - let builder = AccessibleProxy::builder(conn).destination(self.name.as_str()); - let Ok(builder) = builder else { - return Err(builder.unwrap_err()); - }; - - let builder = builder.path(self.path.as_str()); - let Ok(builder) = builder else { - return Err(builder.unwrap_err()); - }; + AccessibleProxy::builder(conn) + .destination(self.name.clone())? + .path(self.path.clone())? + .cache_properties(zbus::proxy::CacheProperties::No) + .build() + .await + } - builder + async fn into_accessible_proxy( + self, + conn: &zbus::Connection, + ) -> Result, zbus::Error> { + AccessibleProxy::builder(conn) + .destination(self.name)? + .path(self.path)? .cache_properties(zbus::proxy::CacheProperties::No) .build() .await diff --git a/atspi/Cargo.toml b/atspi/Cargo.toml index 10d3185e..5285509e 100644 --- a/atspi/Cargo.toml +++ b/atspi/Cargo.toml @@ -44,12 +44,29 @@ name = "event_parsing_100k" path = "./benches/event_parsing_100k.rs" harness = false +[[example]] +name = "tree" +path = "./examples/bus-tree.rs" +required-features = ["proxies-tokio", "zbus"] + +[[example]] +name = "focused-tokio" +path = "./examples/focused-tokio.rs" +required-features = ["connection-tokio"] + +[[example]] +name = "focused-async-std" +path = "./examples/focused-async-std.rs" +required-features = ["connection-async-std"] + [dev-dependencies] +async-std = { version = "1.12", default-features = false } atspi = { path = "." } -zbus.workspace = true criterion = "0.5" -futures-lite = { version = "2", default-features = false } +display_tree = "1.1" fastrand = "2.0" -async-std = { version = "1.12", default-features = false } +futures = { version = "0.3", default-features = false } +futures-lite = { version = "2", default-features = false } tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1" +zbus.workspace = true diff --git a/atspi/examples/bus-tree.rs b/atspi/examples/bus-tree.rs new file mode 100644 index 00000000..7fc873f0 --- /dev/null +++ b/atspi/examples/bus-tree.rs @@ -0,0 +1,162 @@ +//! This example demonstrates how to construct a tree of accessible objects on the accessibility-bus. +//! +//! "This example requires the `proxies-tokio`, `tokio` and `zbus` features to be enabled: +//! +//! ```sh +//! cargo run --example bus-tree --features zbus,proxies-tokio,tokio +//! ``` +//! Authors: +//! Luuk van der Duim, +//! Tait Hoyem + +use atspi::{ + connection::set_session_accessibility, + proxy::accessible::{AccessibleProxy, ObjectRefExt}, + zbus::{proxy::CacheProperties, Connection}, + AccessibilityConnection, Role, +}; +use display_tree::{AsTree, DisplayTree, Style}; +use futures::future::try_join_all; + +type Result = std::result::Result>; + +const REGISTRY_DEST: &str = "org.a11y.atspi.Registry"; +const REGISTRY_PATH: &str = "/org/a11y/atspi/accessible/root"; +const ACCCESSIBLE_INTERFACE: &str = "org.a11y.atspi.Accessible"; + +#[derive(Debug)] +struct A11yNode { + role: Role, + children: Vec, +} + +impl DisplayTree for A11yNode { + fn fmt(&self, f: &mut std::fmt::Formatter, style: Style) -> std::fmt::Result { + self.fmt_with(f, style, &mut vec![]) + } +} + +impl A11yNode { + fn fmt_with( + &self, + f: &mut std::fmt::Formatter<'_>, + style: Style, + prefix: &mut Vec, + ) -> std::fmt::Result { + for (i, is_last_at_i) in prefix.iter().enumerate() { + // if it is the last portion of the line + let is_last = i == prefix.len() - 1; + match (is_last, *is_last_at_i) { + (true, true) => write!(f, "{}", style.char_set.end_connector)?, + (true, false) => write!(f, "{}", style.char_set.connector)?, + // four spaces to emulate `tree` + (false, true) => write!(f, " ")?, + // three spaces and vertical char + (false, false) => write!(f, "{} ", style.char_set.vertical)?, + } + } + + // two horizontal chars to mimic `tree` + writeln!(f, "{}{} {}", style.char_set.horizontal, style.char_set.horizontal, self.role)?; + + for (i, child) in self.children.iter().enumerate() { + prefix.push(i == self.children.len() - 1); + child.fmt_with(f, style, prefix)?; + prefix.pop(); + } + + Ok(()) + } +} + +impl A11yNode { + async fn from_accessible_proxy(ap: AccessibleProxy<'_>) -> Result { + let connection = ap.inner().connection().clone(); + // Contains the processed `A11yNode`'s. + let mut nodes: Vec = Vec::new(); + + // Contains the `AccessibleProxy` yet to be processed. + let mut stack: Vec = vec![ap]; + + // If the stack has an `AccessibleProxy`, we take the last. + while let Some(ap) = stack.pop() { + // Prevent obects with huge child counts from stalling the program. + if ap.child_count().await? > 65536 { + continue; + } + + let child_objects = ap.get_children().await?; + let mut children_proxies = try_join_all( + child_objects + .into_iter() + .map(|child| child.into_accessible_proxy(&connection)), + ) + .await?; + + let roles = try_join_all(children_proxies.iter().map(|child| child.get_role())).await?; + stack.append(&mut children_proxies); + + let children = roles + .into_iter() + .map(|role| A11yNode { role, children: Vec::new() }) + .collect::>(); + + let role = ap.get_role().await?; + nodes.push(A11yNode { role, children }); + } + + let mut fold_stack: Vec = Vec::with_capacity(nodes.len()); + + while let Some(mut node) = nodes.pop() { + if node.children.is_empty() { + fold_stack.push(node); + continue; + } + + // If the node has children, we fold in the children from 'fold_stack'. + // There may be more on 'fold_stack' than the node requires. + let begin = fold_stack.len().saturating_sub(node.children.len()); + node.children = fold_stack.split_off(begin); + fold_stack.push(node); + } + + fold_stack.pop().ok_or("No root node built".into()) + } +} + +async fn get_registry_accessible<'a>(conn: &Connection) -> Result> { + let registry = AccessibleProxy::builder(conn) + .destination(REGISTRY_DEST)? + .path(REGISTRY_PATH)? + .interface(ACCCESSIBLE_INTERFACE)? + .cache_properties(CacheProperties::No) + .build() + .await?; + + Ok(registry) +} + +#[tokio::main] +async fn main() -> Result<()> { + set_session_accessibility(true).await?; + let a11y = AccessibilityConnection::new().await?; + + let conn = a11y.connection(); + let registry = get_registry_accessible(conn).await?; + + let no_children = registry.child_count().await?; + println!("Number of accessible applications on the a11y-bus: {no_children}"); + println!("Construct a tree of accessible objects on the a11y-bus\n"); + + let now = std::time::Instant::now(); + let tree = A11yNode::from_accessible_proxy(registry).await?; + let elapsed = now.elapsed(); + println!("Elapsed time: {:?}", elapsed); + + println!("\nPress 'Enter' to print the tree..."); + let _ = std::io::stdin().read_line(&mut String::new()); + + println!("{}", AsTree::new(&tree)); + + Ok(()) +} diff --git a/atspi/examples/focused-async-std.rs b/atspi/examples/focused-async-std.rs index 31253ce9..ac7d9f6b 100644 --- a/atspi/examples/focused-async-std.rs +++ b/atspi/examples/focused-async-std.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "connection")] use atspi::events::object::{ObjectEvents, StateChangedEvent}; -#[cfg(feature = "connection")] use futures_lite::stream::StreamExt; -#[cfg(feature = "connection")] use std::error::Error; -#[cfg(feature = "connection")] #[async_std::main] async fn main() -> Result<(), Box> { let atspi = atspi::AccessibilityConnection::new().await?; @@ -24,7 +20,3 @@ async fn main() -> Result<(), Box> { } Ok(()) } -#[cfg(not(feature = "connection"))] -fn main() { - println!("This test can not be run without the \"connection\" feature."); -} diff --git a/atspi/examples/focused-tokio.rs b/atspi/examples/focused-tokio.rs index 5b02e0af..c5cb494a 100644 --- a/atspi/examples/focused-tokio.rs +++ b/atspi/examples/focused-tokio.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "connection")] use atspi::events::object::{ObjectEvents, StateChangedEvent}; -#[cfg(feature = "connection")] use std::error::Error; -#[cfg(feature = "connection")] use tokio_stream::StreamExt; -#[cfg(feature = "connection")] #[tokio::main] async fn main() -> Result<(), Box> { let atspi = atspi::AccessibilityConnection::new().await?; @@ -24,7 +20,3 @@ async fn main() -> Result<(), Box> { } Ok(()) } -#[cfg(not(feature = "connection"))] -fn main() { - println!("This test can not be run without the \"connection\" feature."); -}