use std::time::Duration;
use super::worker::{LinkError, RegistryLink, ThreadedReporter, Timing};
use super::{DeviceEvent, KeyEventReporter};
const REGISTRY: &str = "org.a11y.atspi.Registry";
const DEVICE_EVENT_CONTROLLER: &str = "/org/a11y/atspi/registry/deviceeventcontroller";
const DEVICE_EVENT_CONTROLLER_INTERFACE: &str = "org.a11y.atspi.DeviceEventController";
const CALL_TIMEOUT: Duration = Duration::from_secs(4);
pub(super) fn start_reporter() -> Box<dyn KeyEventReporter> {
Box::new(ThreadedReporter::spawn(
Box::new(|| AtspiLink::connect().map(|link| Box::new(link) as Box<dyn RegistryLink>)),
Timing::PLATFORM,
))
}
struct AtspiLink {
connection: zbus::Connection,
}
impl AtspiLink {
fn connect() -> Result<Self, LinkError> {
pollster::block_on(async {
let address = accessibility_bus_address().await?;
let connection = zbus::connection::Builder::address(address.as_str())
.map_err(|error| LinkError::NoBus(error.to_string()))?
.method_timeout(CALL_TIMEOUT)
.build()
.await
.map_err(|error| LinkError::NoBus(error.to_string()))?;
Ok(Self { connection })
})
}
}
async fn accessibility_bus_address() -> Result<String, LinkError> {
if let Ok(address) = std::env::var("AT_SPI_BUS_ADDRESS")
&& !address.is_empty()
{
return Ok(address);
}
let no_bus = |error: zbus::Error| LinkError::NoBus(error.to_string());
let session = zbus::connection::Builder::session()
.map_err(no_bus)?
.method_timeout(CALL_TIMEOUT)
.build()
.await
.map_err(no_bus)?;
let reply = session
.call_method(
Some("org.a11y.Bus"),
"/org/a11y/bus",
Some("org.a11y.Bus"),
"GetAddress",
&(),
)
.await
.map_err(no_bus)?;
reply.body().deserialize::<String>().map_err(no_bus)
}
type NotifyBody<'a> = ((u32, i32, i32, i32, i32, &'a str, bool),);
fn notify_body(event: &DeviceEvent) -> NotifyBody<'_> {
((
event.kind as u32,
event.keysym as i32,
event.hw_code as i32,
event.modifiers as i32,
event.timestamp as i32,
event.event_string.as_str(),
event.is_text,
),)
}
impl RegistryLink for AtspiLink {
fn notify(&mut self, event: &DeviceEvent) -> Result<bool, LinkError> {
pollster::block_on(async {
let reply = self
.connection
.call_method(
Some(REGISTRY),
DEVICE_EVENT_CONTROLLER,
Some(DEVICE_EVENT_CONTROLLER_INTERFACE),
"NotifyListenersSync",
¬ify_body(event),
)
.await
.map_err(classify)?;
reply
.body()
.deserialize::<bool>()
.map_err(|error| LinkError::Refused(error.to_string()))
})
}
}
fn classify(error: zbus::Error) -> LinkError {
match &error {
zbus::Error::MethodError(..) | zbus::Error::FDO(_) => LinkError::Refused(error.to_string()),
zbus::Error::InputOutput(io) if io.kind() == std::io::ErrorKind::TimedOut => {
LinkError::Refused(error.to_string())
}
_ => LinkError::Broken(error.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::{
DEVICE_EVENT_CONTROLLER, DEVICE_EVENT_CONTROLLER_INTERFACE, REGISTRY, notify_body,
};
use crate::key_report::{DeviceEvent, DeviceEventKind};
#[test]
fn the_report_is_one_uiiiisb_struct() {
let event = DeviceEvent {
kind: DeviceEventKind::Released,
keysym: 0xff54,
hw_code: 116,
modifiers: 1 << 2,
timestamp: 4_000_000_000,
event_string: "é".into(),
is_text: true,
};
let message = zbus::Message::method_call(DEVICE_EVENT_CONTROLLER, "NotifyListenersSync")
.and_then(|builder| builder.destination(REGISTRY))
.and_then(|builder| builder.interface(DEVICE_EVENT_CONTROLLER_INTERFACE))
.and_then(|builder| builder.build(¬ify_body(&event)))
.expect("the message builds");
let wire = message.data().bytes();
let field = |signature: &str| {
let mut bytes = vec![8, 1, b'g', 0, signature.len() as u8];
bytes.extend_from_slice(signature.as_bytes());
bytes.push(0);
wire.windows(bytes.len()).any(|window| window == bytes)
};
assert!(
field("(uiiiisb)") && !field("uiiiisb"),
"the header's body signature is not one (uiiiisb) struct: {:?}",
String::from_utf8_lossy(wire)
);
let body = message.body();
let ((kind, keysym, hw_code, modifiers, timestamp, text, is_text),): ((
u32,
i32,
i32,
i32,
i32,
String,
bool,
),) = body.deserialize().expect("the body reads back");
assert_eq!(
(kind, keysym, hw_code, modifiers, text.as_str(), is_text),
(1, 0xff54, 116, 4, "é", true)
);
assert_eq!(
timestamp as u32, 4_000_000_000,
"the time wraps into the i32"
);
}
}