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" )))] |
8 | compile_error!("You must specify at least one of the `async-std` or `tokio` features." ); |
9 | |
10 | pub use atspi_common as common; |
11 | |
12 | use atspi_proxies::{ |
13 | bus::{BusProxy, StatusProxy}, |
14 | registry::RegistryProxy, |
15 | }; |
16 | use common::error::AtspiError; |
17 | use common::events::{BusProperties, Event, EventProperties, HasMatchRule, HasRegistryEventString}; |
18 | use futures_lite::stream::{Stream, StreamExt}; |
19 | use std::ops::Deref; |
20 | use zbus::{fdo::DBusProxy, Address, MatchRule, MessageStream, MessageType}; |
21 | |
22 | /// A wrapper for results whose error type is [`AtspiError`]. |
23 | pub type AtspiResult<T> = std::result::Result<T, AtspiError>; |
24 | |
25 | /// A connection to the at-spi bus |
26 | pub struct AccessibilityConnection { |
27 | registry: RegistryProxy<'static>, |
28 | dbus_proxy: DBusProxy<'static>, |
29 | } |
30 | |
31 | impl 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 | |
312 | impl 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. |
339 | pub async fn set_session_accessibility(status: bool) -> std::result::Result<(), AtspiError> { |
340 | // Get a connection to the session bus. |
341 | let session: Connection = 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. |
369 | pub async fn read_session_accessibility() -> AtspiResult<bool> { |
370 | // Get a connection to the session bus. |
371 | let session: Connection = 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 | } |
379 | |