atspi_connection/lib.rs
1//! A connection to AT-SPI.
2//! This connection may receive any [`atspi_common::events::Event`] structures.
3
4#![deny(clippy::all, clippy::pedantic, clippy::cargo, unsafe_code, rustdoc::all, missing_docs)]
5#![allow(clippy::multiple_crate_versions)]
6
7pub use atspi_common as common;
8use common::{
9 error::AtspiError,
10 events::{DBusInterface, DBusMatchRule, DBusMember, MessageConversion, RegistryEventString},
11 EventProperties, Result as AtspiResult,
12};
13
14#[cfg(feature = "wrappers")]
15use atspi_common::events::Event;
16
17#[cfg(feature = "p2p")]
18mod p2p;
19#[cfg(feature = "p2p")]
20pub use p2p::{Peer, P2P};
21
22use atspi_proxies::{
23 accessible::AccessibleProxy,
24 bus::{BusProxy, StatusProxy},
25 registry::RegistryProxy,
26};
27
28#[cfg(feature = "wrappers")]
29use futures_lite::stream::{Stream, StreamExt};
30use std::ops::Deref;
31use zbus::{
32 fdo::DBusProxy,
33 proxy::{CacheProperties, Defaults},
34 Address, MatchRule,
35};
36#[cfg(feature = "wrappers")]
37use zbus::{message::Type as MessageType, MessageStream};
38
39#[cfg(feature = "p2p")]
40use crate::p2p::Peers;
41
42/// A connection to the at-spi bus
43#[derive(Clone, Debug)]
44pub struct AccessibilityConnection {
45 registry: RegistryProxy<'static>,
46 dbus_proxy: DBusProxy<'static>,
47
48 #[cfg(feature = "p2p")]
49 peers: Peers,
50}
51
52impl AccessibilityConnection {
53 /// Open a new connection to the bus
54 /// # Errors
55 /// May error when a bus is not available,
56 /// or when the accessibility bus (AT-SPI) can not be found.
57 #[cfg_attr(feature = "tracing", tracing::instrument)]
58 pub async fn new() -> AtspiResult<Self> {
59 // Grab the a11y bus address from the session bus
60 let a11y_bus_addr = {
61 #[cfg(feature = "tracing")]
62 tracing::debug!("Connecting to session bus");
63 let session_bus = Box::pin(zbus::Connection::session()).await?;
64
65 #[cfg(feature = "tracing")]
66 tracing::debug!(
67 name = session_bus.unique_name().map(|n| n.as_str()),
68 "Connected to session bus"
69 );
70
71 let proxy = BusProxy::new(&session_bus).await?;
72 #[cfg(feature = "tracing")]
73 tracing::debug!("Getting a11y bus address from session bus");
74 proxy.get_address().await?
75 };
76
77 #[cfg(feature = "tracing")]
78 tracing::debug!(address = %a11y_bus_addr, "Got a11y bus address");
79 let addr: Address = a11y_bus_addr.parse()?;
80
81 let accessibility_conn = Self::from_address(addr).await?;
82
83 #[cfg(feature = "p2p")]
84 accessibility_conn
85 .peers
86 .spawn_peer_listener_task(accessibility_conn.connection());
87
88 Ok(accessibility_conn)
89 }
90
91 /// Returns an [`AccessibilityConnection`], a wrapper for the [`RegistryProxy`]; a handle for the registry provider
92 /// on the accessibility bus.
93 ///
94 /// You may want to call this if you have the accessibility bus address and want a connection with
95 /// a convenient async event stream provisioning.
96 ///
97 /// Without address, you will want to call `open`, which tries to obtain the accessibility bus' address
98 /// on your behalf.
99 ///
100 /// # Errors
101 ///
102 /// `RegistryProxy` is configured with invalid path, interface or destination
103 pub async fn from_address(bus_addr: Address) -> AtspiResult<Self> {
104 #[cfg(feature = "tracing")]
105 tracing::info!("Connecting to a11y bus");
106 let bus = Box::pin(zbus::connection::Builder::address(bus_addr)?.build()).await?;
107
108 #[cfg(feature = "tracing")]
109 tracing::info!(name = bus.unique_name().map(|n| n.as_str()), "Connected to a11y bus");
110
111 // The Proxy holds a strong reference to a Connection, so we only need to store the proxy
112 let registry = RegistryProxy::new(&bus).await?;
113 let dbus_proxy = DBusProxy::new(&bus).await?;
114
115 #[cfg(not(feature = "p2p"))]
116 return Ok(Self { registry, dbus_proxy });
117
118 #[cfg(feature = "p2p")]
119 let peers = Peers::initialize_peers(&bus).await?;
120 #[cfg(feature = "p2p")]
121 return Ok(Self { registry, dbus_proxy, peers });
122 }
123
124 /// Stream yielding all `Event` types.
125 ///
126 /// Monitor this stream to be notified and receive events on the a11y bus.
127 ///
128 /// # Example
129 /// Basic use:
130 ///
131 /// ```rust
132 /// use atspi_connection::AccessibilityConnection;
133 /// use enumflags2::BitFlag;
134 /// use atspi_connection::common::events::{ObjectEvents, object::StateChangedEvent};
135 /// use zbus::{fdo::DBusProxy, MatchRule, message::Type as MessageType};
136 /// use atspi_connection::common::events::Event;
137 /// # use futures_lite::StreamExt;
138 /// # use std::error::Error;
139 ///
140 /// # fn main() {
141 /// # assert!(tokio_test::block_on(example()).is_ok());
142 /// # }
143 ///
144 /// # async fn example() -> Result<(), Box<dyn Error>> {
145 /// let atspi = AccessibilityConnection::new().await?;
146 /// atspi.register_event::<ObjectEvents>().await?;
147 ///
148 /// let mut events = atspi.event_stream();
149 /// std::pin::pin!(&mut events);
150 /// # let output = std::process::Command::new("busctl")
151 /// # .arg("--user")
152 /// # .arg("call")
153 /// # .arg("org.a11y.Bus")
154 /// # .arg("/org/a11y/bus")
155 /// # .arg("org.a11y.Bus")
156 /// # .arg("GetAddress")
157 /// # .output()
158 /// # .unwrap();
159 /// # let addr_string = String::from_utf8(output.stdout).unwrap();
160 /// # let addr_str = addr_string
161 /// # .strip_prefix("s \"")
162 /// # .unwrap()
163 /// # .trim()
164 /// # .strip_suffix('"')
165 /// # .unwrap();
166 /// # let mut base_cmd = std::process::Command::new("busctl");
167 /// # let thing = base_cmd
168 /// # .arg("--address")
169 /// # .arg(addr_str)
170 /// # .arg("emit")
171 /// # .arg("/org/a11y/atspi/null")
172 /// # .arg("org.a11y.atspi.Event.Object")
173 /// # .arg("StateChanged")
174 /// # .arg("siiva{sv}")
175 /// # .arg("")
176 /// # .arg("0")
177 /// # .arg("0")
178 /// # .arg("i")
179 /// # .arg("0")
180 /// # .arg("0")
181 /// # .output()
182 /// # .unwrap();
183 ///
184 /// while let Some(Ok(ev)) = events.next().await {
185 /// // Handle Object events
186 /// if let Ok(event) = StateChangedEvent::try_from(ev) {
187 /// # break;
188 /// // do something else here
189 /// } else { continue }
190 /// }
191 /// # Ok(())
192 /// # }
193 /// ```
194 #[cfg(feature = "wrappers")]
195 pub fn event_stream(&self) -> impl Stream<Item = Result<Event, AtspiError>> {
196 MessageStream::from(self.registry.inner().connection()).filter_map(|res| {
197 let msg = match res {
198 Ok(m) => m,
199 Err(e) => return Some(Err(e.into())),
200 };
201
202 let msg_header = msg.header();
203 let Some(msg_interface) = msg_header.interface() else {
204 return Some(Err(AtspiError::MissingInterface));
205 };
206
207 // Most events we encounter are sent on an "org.a11y.atspi.Event.*" interface,
208 // but there are exceptions like "org.a11y.atspi.Cache" or "org.a11y.atspi.Registry".
209 // So only signals with a suitable interface name will be parsed.
210 if msg_interface.starts_with("org.a11y.atspi")
211 && msg.message_type() == MessageType::Signal
212 {
213 Some(Event::try_from(&msg))
214 } else {
215 None
216 }
217 })
218 }
219
220 /// Registers an events as defined in [`atspi_common::events`].\
221 /// This function registers a single event, like so:
222 ///
223 /// # Example
224 /// ```rust
225 /// use atspi_connection::common::events::object::StateChangedEvent;
226 /// # tokio_test::block_on(async {
227 /// let connection = atspi_connection::AccessibilityConnection::new().await.unwrap();
228 /// connection.register_event::<StateChangedEvent>().await.unwrap();
229 /// # })
230 /// ```
231 ///
232 /// # Errors
233 /// This function may return an error if a [`zbus::Error`] is caused by all the various calls to [`zbus::fdo::DBusProxy`] and [`zbus::MatchRule::try_from`].
234 pub async fn add_match_rule<T: DBusMatchRule>(&self) -> Result<(), AtspiError> {
235 let match_rule = MatchRule::try_from(<T as DBusMatchRule>::MATCH_RULE_STRING)?;
236 self.dbus_proxy.add_match_rule(match_rule).await?;
237 Ok(())
238 }
239
240 /// Deregisters an event as defined in [`atspi_common::events`].\
241 /// This function registers a single event, like so:
242 ///
243 /// # Example
244 /// ```rust
245 /// use atspi_connection::common::events::object::StateChangedEvent;
246 /// # tokio_test::block_on(async {
247 /// let connection = atspi_connection::AccessibilityConnection::new().await.unwrap();
248 /// connection.add_match_rule::<StateChangedEvent>().await.unwrap();
249 /// connection.remove_match_rule::<StateChangedEvent>().await.unwrap();
250 /// # })
251 /// ```
252 ///
253 /// # Errors
254 /// This function may return an error if a [`zbus::Error`] is caused by all the various calls to [`zbus::fdo::DBusProxy`] and [`zbus::MatchRule::try_from`].
255 pub async fn remove_match_rule<T: DBusMatchRule>(&self) -> Result<(), AtspiError> {
256 let match_rule = MatchRule::try_from(<T as DBusMatchRule>::MATCH_RULE_STRING)?;
257 self.dbus_proxy.add_match_rule(match_rule).await?;
258 Ok(())
259 }
260
261 /// Add a registry event.
262 /// This tells accessible applications which events should be forwarded to the accessibility bus.
263 /// This is called by [`Self::register_event`].
264 ///
265 /// # Example
266 /// ```rust
267 /// use atspi_connection::common::events::object::StateChangedEvent;
268 /// # tokio_test::block_on(async {
269 /// let connection = atspi_connection::AccessibilityConnection::new().await.unwrap();
270 /// connection.add_registry_event::<StateChangedEvent>().await.unwrap();
271 /// connection.remove_registry_event::<StateChangedEvent>().await.unwrap();
272 /// # })
273 /// ```
274 ///
275 /// # Errors
276 /// May cause an error if the `DBus` method [`atspi_proxies::registry::RegistryProxy::register_event`] fails.
277 pub async fn add_registry_event<T: RegistryEventString>(&self) -> Result<(), AtspiError> {
278 self.registry
279 .register_event(<T as RegistryEventString>::REGISTRY_EVENT_STRING)
280 .await?;
281 Ok(())
282 }
283
284 /// Remove a registry event.
285 /// This tells accessible applications which events should be forwarded to the accessibility bus.
286 /// This is called by [`Self::deregister_event`].
287 /// It may be called like so:
288 ///
289 /// # Example
290 /// ```rust
291 /// use atspi_connection::common::events::object::StateChangedEvent;
292 /// # tokio_test::block_on(async {
293 /// let connection = atspi_connection::AccessibilityConnection::new().await.unwrap();
294 /// connection.add_registry_event::<StateChangedEvent>().await.unwrap();
295 /// connection.remove_registry_event::<StateChangedEvent>().await.unwrap();
296 /// # })
297 /// ```
298 ///
299 /// # Errors
300 /// May cause an error if the `DBus` method [`RegistryProxy::deregister_event`] fails.
301 pub async fn remove_registry_event<T: RegistryEventString>(&self) -> Result<(), AtspiError> {
302 self.registry
303 .deregister_event(<T as RegistryEventString>::REGISTRY_EVENT_STRING)
304 .await?;
305 Ok(())
306 }
307
308 /// This calls [`Self::add_registry_event`] and [`Self::add_match_rule`], two components necessary to receive accessibility events.
309 /// # Errors
310 /// This will only fail if [`Self::add_registry_event`[ or [`Self::add_match_rule`] fails.
311 pub async fn register_event<T: RegistryEventString + DBusMatchRule>(
312 &self,
313 ) -> Result<(), AtspiError> {
314 self.add_registry_event::<T>().await?;
315 self.add_match_rule::<T>().await?;
316 Ok(())
317 }
318
319 /// This calls [`Self::remove_registry_event`] and [`Self::remove_match_rule`], two components necessary to receive accessibility events.
320 /// # Errors
321 /// This will only fail if [`Self::remove_registry_event`] or [`Self::remove_match_rule`] fails.
322 pub async fn deregister_event<T: RegistryEventString + DBusMatchRule>(
323 &self,
324 ) -> Result<(), AtspiError> {
325 self.remove_registry_event::<T>().await?;
326 self.remove_match_rule::<T>().await?;
327 Ok(())
328 }
329
330 /// Shorthand for a reference to the underlying [`zbus::Connection`]
331 #[must_use = "The reference to the underlying zbus::Connection must be used"]
332 pub fn connection(&self) -> &zbus::Connection {
333 self.registry.inner().connection()
334 }
335
336 /// Send an event over the accessibility bus.
337 /// This converts the event into a [`zbus::Message`] using the [`DBusMember`] + [`DBusInterface`] trait.
338 ///
339 /// # Errors
340 /// This will only fail if:
341 /// 1. [`zbus::Message`] fails at any point, or
342 /// 2. sending the event fails for some reason.
343 ///
344 /// Both of these conditions should never happen as long as you have a valid event.
345 pub async fn send_event<'a, T>(&self, event: T) -> Result<(), AtspiError>
346 where
347 T: DBusMember + DBusInterface + EventProperties + MessageConversion<'a>,
348 {
349 let conn = self.connection();
350 let new_message = zbus::Message::signal(
351 event.path(),
352 <T as DBusInterface>::DBUS_INTERFACE,
353 <T as DBusMember>::DBUS_MEMBER,
354 )?
355 .sender(conn.unique_name().ok_or(AtspiError::MissingName)?)?
356 // this re-encodes the entire body; it's not great..., but you can't replace a sender once a message a created.
357 .build(&event.body())?;
358 Ok(conn.send(&new_message).await?)
359 }
360
361 /// Get the root accessible object from the atspi registry;
362 /// This semantically represents the root of the accessibility tree
363 /// and can be used for tree traversals.
364 ///
365 /// It may be called like so:
366 ///
367 /// # Example
368 /// ```rust
369 /// use atspi_connection::AccessibilityConnection;
370 /// use zbus::proxy::CacheProperties;
371 /// # tokio_test::block_on(async {
372 /// let connection = atspi_connection::AccessibilityConnection::new().await.unwrap();
373 /// let root = connection.root_accessible_on_registry().await.unwrap();
374 /// let children = root.get_children().await;
375 /// assert!(children.is_ok());
376 /// # });
377 /// ```
378 /// # Errors
379 /// This will fail if a dbus connection cannot be established when trying to
380 /// connect to the registry
381 ///
382 /// # Panics
383 /// This will panic if the `default_service` is not set on the `RegistryProxy`, which should never happen.
384 pub async fn root_accessible_on_registry(&self) -> Result<AccessibleProxy<'_>, AtspiError> {
385 let registry_well_known_name = RegistryProxy::DESTINATION
386 .as_ref()
387 .expect("Registry default_service is set");
388 let registry = AccessibleProxy::builder(self.connection())
389 .destination(registry_well_known_name)?
390 // registry has an incomplete implementation of the DBus interface
391 // and therefore cannot be cached; thus we always disable caching it
392 .cache_properties(CacheProperties::No)
393 .build()
394 .await?;
395
396 Ok(registry)
397 }
398}
399
400impl Deref for AccessibilityConnection {
401 type Target = RegistryProxy<'static>;
402
403 fn deref(&self) -> &Self::Target {
404 &self.registry
405 }
406}
407
408/// Set the `IsEnabled` property in the session bus.
409///
410/// Assistive Technology provider applications (ATs) should set the accessibility
411/// `IsEnabled` status on the users session bus on startup as applications may monitor this property
412/// to enable their accessibility support dynamically.
413///
414/// See: The [freedesktop - AT-SPI2 wiki](https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/)
415///
416/// # Example
417/// ```rust
418/// let result = tokio_test::block_on( atspi_connection::set_session_accessibility(true) );
419/// assert!(result.is_ok());
420/// ```
421/// # Errors
422///
423/// 1. when no connection with the session bus can be established,
424/// 2. if creation of a [`atspi_proxies::bus::StatusProxy`] fails
425/// 3. if the `IsEnabled` property cannot be read
426/// 4. the `IsEnabled` property cannot be set.
427pub async fn set_session_accessibility(status: bool) -> std::result::Result<(), AtspiError> {
428 // Get a connection to the session bus.
429 let session = Box::pin(zbus::Connection::session()).await?;
430
431 // Acquire a `StatusProxy` for the session bus.
432 let status_proxy = StatusProxy::new(&session).await?;
433
434 if status_proxy.is_enabled().await? != status {
435 status_proxy.set_is_enabled(status).await?;
436 }
437 Ok(())
438}
439
440/// Read the `IsEnabled` accessibility status property on the session bus.
441///
442/// # Examples
443/// ```rust
444/// # tokio_test::block_on( async {
445/// let status = atspi_connection::read_session_accessibility().await;
446///
447/// // The status is either true or false
448/// assert!(status.is_ok());
449/// # });
450/// ```
451///
452/// # Errors
453///
454/// - If no connection with the session bus could be established.
455/// - If creation of a [`atspi_proxies::bus::StatusProxy`] fails.
456/// - If the `IsEnabled` property cannot be read.
457pub async fn read_session_accessibility() -> AtspiResult<bool> {
458 // Get a connection to the session bus.
459 let session = Box::pin(zbus::Connection::session()).await?;
460
461 // Acquire a `StatusProxy` for the session bus.
462 let status_proxy = StatusProxy::new(&session).await?;
463
464 // Read the `IsEnabled` property.
465 status_proxy.is_enabled().await.map_err(Into::into)
466}