Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'bus-tree' example #188

Merged
merged 11 commits into from
Jun 18, 2024
36 changes: 26 additions & 10 deletions atspi-proxies/src/accessible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,24 +265,40 @@
&self,
conn: &zbus::Connection,
) -> impl std::future::Future<Output = Result<AccessibleProxy<'_>, 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<Output = Result<AccessibleProxy<'_>, zbus::Error>> + Send;
}

impl ObjectRefExt for ObjectRef {
async fn as_accessible_proxy(
&self,
conn: &zbus::Connection,
) -> Result<AccessibleProxy<'_>, 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
}

Check warning on line 293 in atspi-proxies/src/accessible.rs

View check run for this annotation

Codecov / codecov/patch

atspi-proxies/src/accessible.rs#L287-L293

Added lines #L287 - L293 were not covered by tests

builder
async fn into_accessible_proxy(
self,
conn: &zbus::Connection,
) -> Result<AccessibleProxy<'_>, zbus::Error> {
AccessibleProxy::builder(conn)
.destination(self.name)?
.path(self.path)?

Check warning on line 301 in atspi-proxies/src/accessible.rs

View check run for this annotation

Codecov / codecov/patch

atspi-proxies/src/accessible.rs#L295-L301

Added lines #L295 - L301 were not covered by tests
.cache_properties(zbus::proxy::CacheProperties::No)
.build()
.await
Expand Down
23 changes: 20 additions & 3 deletions atspi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
162 changes: 162 additions & 0 deletions atspi/examples/bus-tree.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, Box<dyn std::error::Error>>;

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<A11yNode>,
}

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<bool>,
) -> 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<Self> {
let connection = ap.inner().connection().clone();
// Contains the processed `A11yNode`'s.
let mut nodes: Vec<A11yNode> = Vec::new();

// Contains the `AccessibleProxy` yet to be processed.
let mut stack: Vec<AccessibleProxy> = 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::<Vec<_>>();

let role = ap.get_role().await?;
nodes.push(A11yNode { role, children });
}

let mut fold_stack: Vec<A11yNode> = 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<AccessibleProxy<'a>> {
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(())
}
8 changes: 0 additions & 8 deletions atspi/examples/focused-async-std.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
let atspi = atspi::AccessibilityConnection::new().await?;
Expand All @@ -24,7 +20,3 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
Ok(())
}
#[cfg(not(feature = "connection"))]
fn main() {
println!("This test can not be run without the \"connection\" feature.");
}
8 changes: 0 additions & 8 deletions atspi/examples/focused-tokio.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
let atspi = atspi::AccessibilityConnection::new().await?;
Expand All @@ -24,7 +20,3 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
Ok(())
}
#[cfg(not(feature = "connection"))]
fn main() {
println!("This test can not be run without the \"connection\" feature.");
}
Loading