| 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 | |