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