1use std::sync::Mutex;
4use std::time::Duration;
5
6use xa11y_core::{
7 Action, ActionData, AppTarget, CancelHandle, ElementState, Error, Event, EventFilter,
8 EventKind, EventProvider, EventReceiver, NodeData, PermissionStatus, Provider, Rect, Result,
9 Role, StateSet, Subscription, Toggled, Tree,
10};
11use zbus::blocking::{Connection, Proxy};
12
13pub struct LinuxProvider {
15 a11y_bus: Connection,
16 cached_refs: Mutex<Vec<AccessibleRef>>,
18}
19
20#[derive(Debug, Clone)]
22struct AccessibleRef {
23 bus_name: String,
24 path: String,
25}
26
27impl LinuxProvider {
28 pub fn new() -> Result<Self> {
33 let a11y_bus = Self::connect_a11y_bus()?;
34 Ok(Self {
35 a11y_bus,
36 cached_refs: Mutex::new(Vec::new()),
37 })
38 }
39
40 fn connect_a11y_bus() -> Result<Connection> {
41 if let Ok(session) = Connection::session() {
45 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
46 .map_err(|e| Error::Platform {
47 code: -1,
48 message: format!("Failed to create a11y bus proxy: {}", e),
49 })?;
50
51 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
52 if let Ok(address) = addr_reply.body().deserialize::<String>() {
53 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
54 if let Ok(Ok(conn)) =
55 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
56 {
57 return Ok(conn);
58 }
59 }
60 }
61 }
62
63 return Ok(session);
65 }
66
67 Connection::session().map_err(|e| Error::Platform {
68 code: -1,
69 message: format!("Failed to connect to D-Bus session bus: {}", e),
70 })
71 }
72
73 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
74 Proxy::new(
75 &self.a11y_bus,
76 bus_name.to_owned(),
77 path.to_owned(),
78 interface.to_owned(),
79 )
80 .map_err(|e| Error::Platform {
81 code: -1,
82 message: format!("Failed to create proxy: {}", e),
83 })
84 }
85
86 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
89 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
90 Ok(p) => p,
91 Err(_) => return false,
92 };
93 let reply = match proxy.call_method("GetInterfaces", &()) {
94 Ok(r) => r,
95 Err(_) => return false,
96 };
97 let interfaces: Vec<String> = match reply.body().deserialize() {
98 Ok(v) => v,
99 Err(_) => return false,
100 };
101 interfaces.iter().any(|i| i.contains(iface))
102 }
103
104 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
106 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
107 let reply = proxy
108 .call_method("GetRole", &())
109 .map_err(|e| Error::Platform {
110 code: -1,
111 message: format!("GetRole failed: {}", e),
112 })?;
113 reply
114 .body()
115 .deserialize::<u32>()
116 .map_err(|e| Error::Platform {
117 code: -1,
118 message: format!("GetRole deserialize failed: {}", e),
119 })
120 }
121
122 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
124 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
125 let reply = proxy
126 .call_method("GetRoleName", &())
127 .map_err(|e| Error::Platform {
128 code: -1,
129 message: format!("GetRoleName failed: {}", e),
130 })?;
131 reply
132 .body()
133 .deserialize::<String>()
134 .map_err(|e| Error::Platform {
135 code: -1,
136 message: format!("GetRoleName deserialize failed: {}", e),
137 })
138 }
139
140 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
142 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
143 proxy
144 .get_property::<String>("Name")
145 .map_err(|e| Error::Platform {
146 code: -1,
147 message: format!("Get Name property failed: {}", e),
148 })
149 }
150
151 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
153 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
154 proxy
155 .get_property::<String>("Description")
156 .map_err(|e| Error::Platform {
157 code: -1,
158 message: format!("Get Description property failed: {}", e),
159 })
160 }
161
162 fn get_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
166 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
167 let reply = proxy
168 .call_method("GetChildren", &())
169 .map_err(|e| Error::Platform {
170 code: -1,
171 message: format!("GetChildren failed: {}", e),
172 })?;
173 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
174 reply.body().deserialize().map_err(|e| Error::Platform {
175 code: -1,
176 message: format!("GetChildren deserialize failed: {}", e),
177 })?;
178 Ok(children
179 .into_iter()
180 .map(|(bus_name, path)| AccessibleRef {
181 bus_name,
182 path: path.to_string(),
183 })
184 .collect())
185 }
186
187 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
189 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
190 let reply = proxy
191 .call_method("GetState", &())
192 .map_err(|e| Error::Platform {
193 code: -1,
194 message: format!("GetState failed: {}", e),
195 })?;
196 reply
197 .body()
198 .deserialize::<Vec<u32>>()
199 .map_err(|e| Error::Platform {
200 code: -1,
201 message: format!("GetState deserialize failed: {}", e),
202 })
203 }
204
205 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
209 if !self.has_interface(aref, "Component") {
210 return None;
211 }
212 let proxy = self
213 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
214 .ok()?;
215 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
218 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
219 if w <= 0 && h <= 0 {
220 return None;
221 }
222 Some(Rect {
223 x,
224 y,
225 width: w.max(0) as u32,
226 height: h.max(0) as u32,
227 })
228 }
229
230 fn get_actions(&self, aref: &AccessibleRef) -> Vec<Action> {
234 let mut actions = Vec::new();
235
236 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
238 if let Ok(n_actions) = proxy.get_property::<i32>("NActions") {
239 for i in 0..n_actions {
240 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
241 if let Ok(name) = reply.body().deserialize::<String>() {
242 if let Some(action) = map_atspi_action(&name) {
243 if !actions.contains(&action) {
244 actions.push(action);
245 }
246 }
247 }
248 }
249 }
250 }
251 }
252
253 if !actions.contains(&Action::Focus) {
255 if let Ok(proxy) =
256 self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
257 {
258 if proxy.call_method("GetExtents", &(0u32,)).is_ok() {
260 actions.push(Action::Focus);
261 }
262 }
263 }
264
265 actions
266 }
267
268 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
271 let text_value = self.get_text_content(aref);
275 if text_value.is_some() {
276 return text_value;
277 }
278 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
280 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
281 return Some(val.to_string());
282 }
283 }
284 None
285 }
286
287 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
289 let proxy = self
290 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
291 .ok()?;
292 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
293 if char_count > 0 {
294 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
295 let text: String = reply.body().deserialize().ok()?;
296 if !text.is_empty() {
297 return Some(text);
298 }
299 }
300 None
301 }
302
303 #[allow(clippy::too_many_arguments)]
305 #[allow(clippy::only_used_in_recursion)]
306 fn traverse(
307 &self,
308 aref: &AccessibleRef,
309 nodes: &mut Vec<NodeData>,
310 refs: &mut Vec<AccessibleRef>,
311 parent_idx: Option<u32>,
312 depth: u32,
313 screen_size: (u32, u32),
314 ) {
315 let role_name = self.get_role_name(aref).unwrap_or_default();
316 let role_num = self.get_role_number(aref).unwrap_or(0);
317 let role = if !role_name.is_empty() {
318 map_atspi_role(&role_name)
319 } else {
320 map_atspi_role_number(role_num)
321 };
322
323 let mut name = self.get_name(aref).ok().filter(|s| !s.is_empty());
324 let description = self.get_description(aref).ok().filter(|s| !s.is_empty());
325 let value = self.get_value(aref);
326
327 if name.is_none() && role == Role::StaticText {
330 if let Some(ref v) = value {
331 name = Some(v.clone());
332 }
333 }
334 let bounds = self.get_extents(aref);
335 let states = self.parse_states(aref, role);
336 let actions = self.get_actions(aref);
337
338 let raw = {
339 let raw_role = if role_name.is_empty() {
340 format!("role_num:{}", role_num)
341 } else {
342 role_name
343 };
344 xa11y_core::RawPlatformData::Linux {
345 atspi_role: raw_role,
346 bus_name: aref.bus_name.clone(),
347 object_path: aref.path.clone(),
348 }
349 };
350
351 let (numeric_value, min_value, max_value) = if matches!(
352 role,
353 Role::Slider | Role::ProgressBar | Role::ScrollBar | Role::SpinButton
354 ) {
355 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
356 (
357 proxy.get_property::<f64>("CurrentValue").ok(),
358 proxy.get_property::<f64>("MinimumValue").ok(),
359 proxy.get_property::<f64>("MaximumValue").ok(),
360 )
361 } else {
362 (None, None, None)
363 }
364 } else {
365 (None, None, None)
366 };
367
368 let node_idx = nodes.len() as u32;
369 nodes.push(NodeData {
370 role,
371 name,
372 value,
373 description,
374 bounds,
375 actions,
376 states,
377 numeric_value,
378 min_value,
379 max_value,
380 pid: None,
381 stable_id: Some(aref.path.clone()),
382 raw,
383 index: node_idx,
384 children_indices: vec![], parent_index: parent_idx,
386 });
387 refs.push(aref.clone());
388
389 let children = self.get_children(aref).unwrap_or_default();
391 let mut child_ids = Vec::new();
392
393 for child_ref in &children {
394 if child_ref.path == "/org/a11y/atspi/null"
396 || child_ref.bus_name.is_empty()
397 || child_ref.path.is_empty()
398 {
399 continue;
400 }
401 let child_idx = nodes.len() as u32;
402 child_ids.push(child_idx);
403 self.traverse(
404 child_ref,
405 nodes,
406 refs,
407 Some(node_idx),
408 depth + 1,
409 screen_size,
410 );
411 }
412
413 nodes[node_idx as usize].children_indices = child_ids;
415 }
416
417 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
419 let state_bits = self.get_state(aref).unwrap_or_default();
420
421 let bits: u64 = if state_bits.len() >= 2 {
423 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
424 } else if state_bits.len() == 1 {
425 state_bits[0] as u64
426 } else {
427 0
428 };
429
430 const BUSY: u64 = 1 << 3;
432 const CHECKED: u64 = 1 << 4;
433 const EDITABLE: u64 = 1 << 7;
434 const ENABLED: u64 = 1 << 8;
435 const EXPANDABLE: u64 = 1 << 9;
436 const EXPANDED: u64 = 1 << 10;
437 const FOCUSABLE: u64 = 1 << 11;
438 const FOCUSED: u64 = 1 << 12;
439 const MODAL: u64 = 1 << 16;
440 const SELECTED: u64 = 1 << 23;
441 const SENSITIVE: u64 = 1 << 24;
442 const SHOWING: u64 = 1 << 25;
443 const VISIBLE: u64 = 1 << 30;
444 const INDETERMINATE: u64 = 1 << 32;
445 const REQUIRED: u64 = 1 << 33;
446
447 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
448 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
449
450 let checked = match role {
451 Role::CheckBox | Role::RadioButton | Role::MenuItem => {
452 if (bits & INDETERMINATE) != 0 {
453 Some(Toggled::Mixed)
454 } else if (bits & CHECKED) != 0 {
455 Some(Toggled::On)
456 } else {
457 Some(Toggled::Off)
458 }
459 }
460 _ => None,
461 };
462
463 let expanded = if (bits & EXPANDABLE) != 0 {
464 Some((bits & EXPANDED) != 0)
465 } else {
466 None
467 };
468
469 StateSet {
470 enabled,
471 visible,
472 focused: (bits & FOCUSED) != 0,
473 checked,
474 selected: (bits & SELECTED) != 0,
475 expanded,
476 editable: (bits & EDITABLE) != 0,
477 focusable: (bits & FOCUSABLE) != 0,
478 modal: (bits & MODAL) != 0,
479 required: (bits & REQUIRED) != 0,
480 busy: (bits & BUSY) != 0,
481 }
482 }
483
484 fn detect_screen_size() -> (u32, u32) {
486 if let Ok(output) = std::process::Command::new("xdpyinfo").output() {
487 let stdout = String::from_utf8_lossy(&output.stdout);
488 for line in stdout.lines() {
489 let trimmed = line.trim();
490 if trimmed.starts_with("dimensions:") {
491 if let Some(dims) = trimmed.split_whitespace().nth(1) {
492 let parts: Vec<&str> = dims.split('x').collect();
493 if parts.len() == 2 {
494 if let (Ok(w), Ok(h)) = (parts[0].parse(), parts[1].parse()) {
495 return (w, h);
496 }
497 }
498 }
499 }
500 }
501 }
502 (1920, 1080)
503 }
504
505 fn find_app_by_name(&self, name: &str) -> Result<AccessibleRef> {
507 let registry = AccessibleRef {
508 bus_name: "org.a11y.atspi.Registry".to_string(),
509 path: "/org/a11y/atspi/accessible/root".to_string(),
510 };
511 let children = self.get_children(®istry)?;
512 let name_lower = name.to_lowercase();
513
514 for child in &children {
515 if child.path == "/org/a11y/atspi/null" {
516 continue;
517 }
518 if let Ok(app_name) = self.get_name(child) {
519 if app_name.to_lowercase().contains(&name_lower) {
520 return Ok(child.clone());
521 }
522 }
523 }
524
525 Err(Error::AppNotFound {
526 target: name.to_string(),
527 })
528 }
529
530 fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
532 let registry = AccessibleRef {
533 bus_name: "org.a11y.atspi.Registry".to_string(),
534 path: "/org/a11y/atspi/accessible/root".to_string(),
535 };
536 let children = self.get_children(®istry)?;
537
538 for child in &children {
539 if child.path == "/org/a11y/atspi/null" {
540 continue;
541 }
542 if let Ok(proxy) =
544 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
545 {
546 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
547 if app_pid as u32 == pid {
548 return Ok(child.clone());
549 }
550 }
551 }
552 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
554 if app_pid == pid {
555 return Ok(child.clone());
556 }
557 }
558 }
559
560 Err(Error::AppNotFound {
561 target: format!("PID {}", pid),
562 })
563 }
564
565 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
567 let proxy = self
568 .make_proxy(
569 "org.freedesktop.DBus",
570 "/org/freedesktop/DBus",
571 "org.freedesktop.DBus",
572 )
573 .ok()?;
574 let reply = proxy
575 .call_method("GetConnectionUnixProcessID", &(bus_name,))
576 .ok()?;
577 let pid: u32 = reply.body().deserialize().ok()?;
578 if pid > 0 {
579 Some(pid)
580 } else {
581 None
582 }
583 }
584
585 fn do_atspi_action(&self, aref: &AccessibleRef, action_name: &str) -> Result<()> {
587 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
588 let n_actions: i32 = proxy.get_property("NActions").unwrap_or(0);
589
590 for i in 0..n_actions {
591 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
592 if let Ok(name) = reply.body().deserialize::<String>() {
593 if name == action_name {
594 let _ =
595 proxy
596 .call_method("DoAction", &(i,))
597 .map_err(|e| Error::Platform {
598 code: -1,
599 message: format!("DoAction failed: {}", e),
600 })?;
601 return Ok(());
602 }
603 }
604 }
605 }
606
607 Err(Error::Platform {
608 code: -1,
609 message: format!("Action '{}' not found", action_name),
610 })
611 }
612
613 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
615 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
617 {
618 if let Ok(pid) = proxy.get_property::<i32>("Id") {
619 if pid > 0 {
620 return Some(pid as u32);
621 }
622 }
623 }
624
625 if let Ok(proxy) = self.make_proxy(
627 "org.freedesktop.DBus",
628 "/org/freedesktop/DBus",
629 "org.freedesktop.DBus",
630 ) {
631 if let Ok(reply) =
632 proxy.call_method("GetConnectionUnixProcessID", &(aref.bus_name.as_str(),))
633 {
634 if let Ok(pid) = reply.body().deserialize::<u32>() {
635 if pid > 0 {
636 return Some(pid);
637 }
638 }
639 }
640 }
641
642 None
643 }
644}
645
646impl Provider for LinuxProvider {
647 fn get_app_tree(&self, target: &AppTarget) -> Result<Tree> {
648 let app_ref = match target {
649 AppTarget::ByName(name) => self.find_app_by_name(name)?,
650 AppTarget::ByPid(pid) => self.find_app_by_pid(*pid)?,
651 AppTarget::ByWindow(_) => {
652 return Err(Error::Platform {
653 code: -1,
654 message: "ByWindow not supported on Linux AT-SPI2".to_string(),
655 });
656 }
657 };
658
659 let app_name = self.get_name(&app_ref).unwrap_or_default();
660 let screen_size = Self::detect_screen_size();
661 let mut nodes = Vec::new();
662 let mut refs = Vec::new();
663
664 self.traverse(&app_ref, &mut nodes, &mut refs, None, 0, screen_size);
665
666 if nodes.is_empty() {
667 return Err(Error::AppNotFound {
668 target: format!("{:?}", target),
669 });
670 }
671
672 *self.cached_refs.lock().unwrap() = refs;
674
675 let pid = self.get_app_pid(&app_ref);
676
677 Ok(Tree::new(app_name, pid, screen_size, nodes))
678 }
679
680 fn get_apps(&self) -> Result<Tree> {
681 let screen_size = Self::detect_screen_size();
682 let mut nodes = Vec::new();
683
684 nodes.push(NodeData {
685 role: Role::Application,
686 name: Some("Desktop".to_string()),
687 value: None,
688 description: None,
689 bounds: Some(Rect {
690 x: 0,
691 y: 0,
692 width: screen_size.0,
693 height: screen_size.1,
694 }),
695 actions: vec![],
696 states: StateSet::default(),
697 numeric_value: None,
698 min_value: None,
699 max_value: None,
700 pid: None,
701 stable_id: None,
702 raw: xa11y_core::RawPlatformData::Synthetic,
703 index: 0,
704 children_indices: vec![],
705 parent_index: None,
706 });
707
708 let mut refs = Vec::new();
709 refs.push(AccessibleRef {
710 bus_name: String::new(),
711 path: String::new(),
712 }); let registry = AccessibleRef {
715 bus_name: "org.a11y.atspi.Registry".to_string(),
716 path: "/org/a11y/atspi/accessible/root".to_string(),
717 };
718 let children = self.get_children(®istry).unwrap_or_default();
719 let mut root_children = Vec::new();
720
721 for child in &children {
722 if child.path == "/org/a11y/atspi/null" {
723 continue;
724 }
725 let app_name = self.get_name(child).unwrap_or_default();
726 if app_name.is_empty() {
727 continue;
728 }
729 let child_idx = nodes.len() as u32;
730 root_children.push(child_idx);
731 self.traverse(child, &mut nodes, &mut refs, Some(0), 1, screen_size);
732 }
733
734 nodes[0].children_indices = root_children;
735
736 *self.cached_refs.lock().unwrap() = refs;
737
738 Ok(Tree::new("Desktop".to_string(), None, screen_size, nodes))
739 }
740
741 fn perform_action(
742 &self,
743 tree: &Tree,
744 node: &NodeData,
745 action: Action,
746 data: Option<ActionData>,
747 ) -> Result<()> {
748 let node_idx = tree.node_index(node);
749
750 let cache = self.cached_refs.lock().unwrap();
752 let target = cache
753 .get(node_idx as usize)
754 .ok_or(Error::ElementStale {
755 selector: format!("index:{}", node_idx),
756 })?
757 .clone();
758 drop(cache);
759
760 match action {
761 Action::Press => self
762 .do_atspi_action(&target, "click")
763 .or_else(|_| self.do_atspi_action(&target, "activate"))
764 .or_else(|_| self.do_atspi_action(&target, "press")),
765 Action::Focus => {
766 if let Ok(proxy) =
768 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
769 {
770 if proxy.call_method("GrabFocus", &()).is_ok() {
771 return Ok(());
772 }
773 }
774 self.do_atspi_action(&target, "focus")
775 .or_else(|_| self.do_atspi_action(&target, "setFocus"))
776 }
777 Action::SetValue => match data {
778 Some(ActionData::NumericValue(v)) => {
779 let proxy =
780 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
781 proxy
782 .set_property("CurrentValue", v)
783 .map_err(|e| Error::Platform {
784 code: -1,
785 message: format!("SetValue failed: {}", e),
786 })
787 }
788 Some(ActionData::Value(text)) => {
789 let proxy = self
790 .make_proxy(
791 &target.bus_name,
792 &target.path,
793 "org.a11y.atspi.EditableText",
794 )
795 .map_err(|_| Error::TextValueNotSupported)?;
796 let _ = proxy.call_method("DeleteText", &(0i32, i32::MAX));
797 proxy
798 .call_method("InsertText", &(0i32, &*text, text.len() as i32))
799 .map_err(|_| Error::TextValueNotSupported)?;
800 Ok(())
801 }
802 _ => Err(Error::Platform {
803 code: -1,
804 message: "SetValue requires ActionData".to_string(),
805 }),
806 },
807 Action::Toggle => self
808 .do_atspi_action(&target, "toggle")
809 .or_else(|_| self.do_atspi_action(&target, "click"))
810 .or_else(|_| self.do_atspi_action(&target, "activate")),
811 Action::Expand => self
812 .do_atspi_action(&target, "expand")
813 .or_else(|_| self.do_atspi_action(&target, "open")),
814 Action::Collapse => self
815 .do_atspi_action(&target, "collapse")
816 .or_else(|_| self.do_atspi_action(&target, "close")),
817 Action::Select => self.do_atspi_action(&target, "select"),
818 Action::ShowMenu => self
819 .do_atspi_action(&target, "menu")
820 .or_else(|_| self.do_atspi_action(&target, "showmenu")),
821 Action::ScrollIntoView => {
822 let proxy =
823 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
824 proxy
825 .call_method("ScrollTo", &(0u32,))
826 .map_err(|e| Error::Platform {
827 code: -1,
828 message: format!("ScrollTo failed: {}", e),
829 })?;
830 Ok(())
831 }
832 Action::Increment => self.do_atspi_action(&target, "increment").or_else(|_| {
833 let proxy =
835 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
836 let current: f64 =
837 proxy
838 .get_property("CurrentValue")
839 .map_err(|e| Error::Platform {
840 code: -1,
841 message: format!("Value.CurrentValue failed: {}", e),
842 })?;
843 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
844 let step = if step <= 0.0 { 1.0 } else { step };
845 proxy
846 .set_property("CurrentValue", current + step)
847 .map_err(|e| Error::Platform {
848 code: -1,
849 message: format!("Value.SetCurrentValue failed: {}", e),
850 })
851 }),
852 Action::Decrement => self.do_atspi_action(&target, "decrement").or_else(|_| {
853 let proxy =
854 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
855 let current: f64 =
856 proxy
857 .get_property("CurrentValue")
858 .map_err(|e| Error::Platform {
859 code: -1,
860 message: format!("Value.CurrentValue failed: {}", e),
861 })?;
862 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
863 let step = if step <= 0.0 { 1.0 } else { step };
864 proxy
865 .set_property("CurrentValue", current - step)
866 .map_err(|e| Error::Platform {
867 code: -1,
868 message: format!("Value.SetCurrentValue failed: {}", e),
869 })
870 }),
871 Action::Blur => {
872 let proxy =
874 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Accessible")?;
875 if let Ok(reply) = proxy.call_method("GetParent", &()) {
876 if let Ok((bus, path)) = reply
877 .body()
878 .deserialize::<(String, zbus::zvariant::OwnedObjectPath)>()
879 {
880 let path_str = path.as_str();
881 if path_str != "/org/a11y/atspi/null" {
882 if let Ok(p) =
883 self.make_proxy(&bus, path_str, "org.a11y.atspi.Component")
884 {
885 let _ = p.call_method("GrabFocus", &());
886 return Ok(());
887 }
888 }
889 }
890 }
891 Ok(())
892 }
893
894 Action::ScrollDown | Action::ScrollRight => {
895 let amount = match data {
896 Some(ActionData::ScrollAmount(amount)) => amount,
897 _ => {
898 return Err(Error::Platform {
899 code: -1,
900 message: "Scroll requires ActionData::ScrollAmount".to_string(),
901 })
902 }
903 };
904 let is_vertical = matches!(action, Action::ScrollDown);
905 let (pos_name, neg_name) = if is_vertical {
906 ("scroll down", "scroll up")
907 } else {
908 ("scroll right", "scroll left")
909 };
910 let action_name = if amount >= 0.0 { pos_name } else { neg_name };
911 let count = (amount.abs() as u32).max(1);
913 for _ in 0..count {
914 if self.do_atspi_action(&target, action_name).is_err() {
915 let proxy = self.make_proxy(
917 &target.bus_name,
918 &target.path,
919 "org.a11y.atspi.Component",
920 )?;
921 let scroll_type: u32 = if is_vertical {
922 if amount >= 0.0 {
923 3
924 } else {
925 2
926 } } else {
928 if amount >= 0.0 {
929 5
930 } else {
931 4
932 } };
934 proxy
935 .call_method("ScrollTo", &(scroll_type,))
936 .map_err(|e| Error::Platform {
937 code: -1,
938 message: format!("ScrollTo failed: {}", e),
939 })?;
940 return Ok(());
941 }
942 }
943 Ok(())
944 }
945
946 Action::SetTextSelection => {
947 let (start, end) = match data {
948 Some(ActionData::TextSelection { start, end }) => (start, end),
949 _ => {
950 return Err(Error::Platform {
951 code: -1,
952 message: "SetTextSelection requires ActionData::TextSelection"
953 .to_string(),
954 })
955 }
956 };
957 let proxy =
958 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
959 if proxy
961 .call_method("SetSelection", &(0i32, start as i32, end as i32))
962 .is_err()
963 {
964 proxy
965 .call_method("AddSelection", &(start as i32, end as i32))
966 .map_err(|e| Error::Platform {
967 code: -1,
968 message: format!("Text.AddSelection failed: {}", e),
969 })?;
970 }
971 Ok(())
972 }
973
974 Action::TypeText => {
975 let text = match data {
976 Some(ActionData::Value(text)) => text,
977 _ => {
978 return Err(Error::Platform {
979 code: -1,
980 message: "TypeText requires ActionData::Value".to_string(),
981 })
982 }
983 };
984 let text_proxy =
987 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
988 let insert_pos = text_proxy
989 .as_ref()
990 .ok()
991 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
992 .unwrap_or(-1); let proxy = self
995 .make_proxy(
996 &target.bus_name,
997 &target.path,
998 "org.a11y.atspi.EditableText",
999 )
1000 .map_err(|_| Error::TextValueNotSupported)?;
1001 let pos = if insert_pos >= 0 {
1002 insert_pos
1003 } else {
1004 i32::MAX
1005 };
1006 proxy
1007 .call_method("InsertText", &(pos, &*text, text.len() as i32))
1008 .map_err(|e| Error::Platform {
1009 code: -1,
1010 message: format!("EditableText.InsertText failed: {}", e),
1011 })?;
1012 Ok(())
1013 }
1014 }
1015 }
1016
1017 fn check_permissions(&self) -> Result<PermissionStatus> {
1018 let registry = AccessibleRef {
1019 bus_name: "org.a11y.atspi.Registry".to_string(),
1020 path: "/org/a11y/atspi/accessible/root".to_string(),
1021 };
1022 match self.get_children(®istry) {
1023 Ok(_) => Ok(PermissionStatus::Granted),
1024 Err(_) => Ok(PermissionStatus::Denied {
1025 instructions:
1026 "Enable accessibility: gsettings set org.gnome.desktop.interface toolkit-accessibility true\nEnsure at-spi2-core is installed."
1027 .to_string(),
1028 }),
1029 }
1030 }
1031}
1032
1033impl EventProvider for LinuxProvider {
1036 fn subscribe(&self, target: &AppTarget, filter: EventFilter) -> Result<Subscription> {
1037 let (tx, rx) = std::sync::mpsc::channel();
1038
1039 let (app_name, app_pid) = match target {
1040 AppTarget::ByName(name) => {
1041 let app_ref = self.find_app_by_name(name)?;
1042 let pid = self.get_app_pid(&app_ref).unwrap_or(0);
1043 (self.get_name(&app_ref).unwrap_or_default(), pid)
1044 }
1045 AppTarget::ByPid(pid) => {
1046 let app_ref = self.find_app_by_pid(*pid)?;
1047 (self.get_name(&app_ref).unwrap_or_default(), *pid)
1048 }
1049 AppTarget::ByWindow(_) => {
1050 return Err(Error::Platform {
1051 code: -1,
1052 message: "ByWindow not supported for event subscription".to_string(),
1053 })
1054 }
1055 };
1056
1057 let poll_provider = LinuxProvider::new()?;
1059 let target_clone = target.clone();
1060 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1061 let stop_clone = stop.clone();
1062
1063 let handle = std::thread::spawn(move || {
1065 let mut prev_focused: Option<String> = None;
1066 let mut prev_node_count: usize = 0;
1067
1068 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1069 std::thread::sleep(Duration::from_millis(100));
1070
1071 let tree = match poll_provider.get_app_tree(&target_clone) {
1072 Ok(t) => t,
1073 Err(_) => continue,
1074 };
1075
1076 let focused_name = tree
1078 .iter()
1079 .find(|n| n.states.focused)
1080 .and_then(|n| n.name.clone());
1081 if focused_name != prev_focused {
1082 if prev_focused.is_some() {
1083 let kind = EventKind::FocusChanged;
1084 if filter.kinds.is_empty() || filter.kinds.contains(&kind) {
1085 let _ = tx.send(Event {
1086 kind,
1087 app_name: app_name.clone(),
1088 app_pid,
1089 target: tree.iter().find(|n| n.states.focused).cloned(),
1090 state_flag: None,
1091 state_value: None,
1092 text_change: None,
1093 timestamp: std::time::Instant::now(),
1094 });
1095 }
1096 }
1097 prev_focused = focused_name;
1098 }
1099
1100 let node_count = tree.len();
1102 if node_count != prev_node_count && prev_node_count > 0 {
1103 let kind = EventKind::StructureChanged;
1104 if filter.kinds.is_empty() || filter.kinds.contains(&kind) {
1105 let _ = tx.send(Event {
1106 kind,
1107 app_name: app_name.clone(),
1108 app_pid,
1109 target: None,
1110 state_flag: None,
1111 state_value: None,
1112 text_change: None,
1113 timestamp: std::time::Instant::now(),
1114 });
1115 }
1116 }
1117 prev_node_count = node_count;
1118 }
1119 });
1120
1121 let cancel = CancelHandle::new(move || {
1122 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1123 let _ = handle.join();
1124 });
1125
1126 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1127 }
1128
1129 fn wait_for_event(
1130 &self,
1131 target: &AppTarget,
1132 filter: EventFilter,
1133 timeout: Duration,
1134 ) -> Result<Event> {
1135 let sub = self.subscribe(target, filter)?;
1136 let start = std::time::Instant::now();
1137 loop {
1138 if let Some(event) = sub.try_recv() {
1139 return Ok(event);
1140 }
1141 let elapsed = start.elapsed();
1142 if elapsed >= timeout {
1143 return Err(Error::Timeout { elapsed });
1144 }
1145 std::thread::sleep(Duration::from_millis(10));
1146 }
1147 }
1148
1149 fn wait_for(
1150 &self,
1151 target: &AppTarget,
1152 selector: &str,
1153 state: ElementState,
1154 timeout: Duration,
1155 ) -> Result<Option<NodeData>> {
1156 let start = std::time::Instant::now();
1157 let poll_interval = Duration::from_millis(100);
1158
1159 loop {
1160 let elapsed = start.elapsed();
1161 if elapsed >= timeout {
1162 return Err(Error::Timeout { elapsed });
1163 }
1164
1165 let tree = self.get_app_tree(target)?;
1166 let matches = tree.query(selector).ok();
1167 let node = matches.as_ref().and_then(|m| m.first().copied());
1168
1169 if state.is_met(node) {
1170 return Ok(node.cloned());
1171 }
1172
1173 std::thread::sleep(poll_interval);
1174 }
1175 }
1176}
1177
1178fn map_atspi_role(role_name: &str) -> Role {
1180 match role_name.to_lowercase().as_str() {
1181 "application" => Role::Application,
1182 "window" | "frame" => Role::Window,
1183 "dialog" | "file chooser" => Role::Dialog,
1184 "alert" | "notification" => Role::Alert,
1185 "push button" | "push button menu" => Role::Button,
1186 "check box" | "check menu item" => Role::CheckBox,
1187 "radio button" | "radio menu item" => Role::RadioButton,
1188 "entry" | "password text" => Role::TextField,
1189 "spin button" => Role::SpinButton,
1190 "text" => Role::TextArea,
1191 "label" | "static" | "caption" => Role::StaticText,
1192 "combo box" => Role::ComboBox,
1193 "list" | "list box" => Role::List,
1194 "list item" => Role::ListItem,
1195 "menu" => Role::Menu,
1196 "menu item" | "tearoff menu item" => Role::MenuItem,
1197 "menu bar" => Role::MenuBar,
1198 "page tab" => Role::Tab,
1199 "page tab list" => Role::TabGroup,
1200 "table" | "tree table" => Role::Table,
1201 "table row" => Role::TableRow,
1202 "table cell" | "table column header" | "table row header" => Role::TableCell,
1203 "tool bar" => Role::Toolbar,
1204 "scroll bar" => Role::ScrollBar,
1205 "slider" => Role::Slider,
1206 "image" | "icon" | "desktop icon" => Role::Image,
1207 "link" => Role::Link,
1208 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1209 "progress bar" => Role::ProgressBar,
1210 "tree item" => Role::TreeItem,
1211 "document web" | "document frame" => Role::WebArea,
1212 "heading" => Role::Heading,
1213 "separator" => Role::Separator,
1214 "split pane" => Role::SplitGroup,
1215 "tooltip" | "tool tip" => Role::Tooltip,
1216 "status bar" | "statusbar" => Role::Status,
1217 "landmark" | "navigation" => Role::Navigation,
1218 _ => Role::Unknown,
1219 }
1220}
1221
1222fn map_atspi_role_number(role: u32) -> Role {
1225 match role {
1226 2 => Role::Alert, 7 => Role::CheckBox, 8 => Role::CheckBox, 11 => Role::ComboBox, 16 => Role::Dialog, 19 => Role::Dialog, 20 => Role::Group, 23 => Role::Window, 26 => Role::Image, 27 => Role::Image, 29 => Role::StaticText, 31 => Role::List, 32 => Role::ListItem, 33 => Role::Menu, 34 => Role::MenuBar, 35 => Role::MenuItem, 37 => Role::Tab, 38 => Role::TabGroup, 39 => Role::Group, 40 => Role::TextField, 42 => Role::ProgressBar, 43 => Role::Button, 44 => Role::RadioButton, 45 => Role::RadioButton, 48 => Role::ScrollBar, 49 => Role::Group, 50 => Role::Separator, 51 => Role::Slider, 52 => Role::SpinButton, 53 => Role::SplitGroup, 55 => Role::Table, 56 => Role::TableCell, 57 => Role::TableCell, 58 => Role::TableCell, 61 => Role::TextArea, 62 => Role::Button, 63 => Role::Toolbar, 65 => Role::Group, 66 => Role::Table, 67 => Role::Unknown, 68 => Role::Group, 69 => Role::Window, 75 => Role::Application, 79 => Role::TextField, 82 => Role::WebArea, 83 => Role::Heading, 85 => Role::Group, 86 => Role::Group, 87 => Role::Group, 88 => Role::Link, 90 => Role::TableRow, 91 => Role::TreeItem, 95 => Role::WebArea, 98 => Role::List, 93 => Role::Tooltip, 97 => Role::Status, 101 => Role::Alert, 116 => Role::StaticText, 129 => Role::Button, _ => Role::Unknown,
1286 }
1287}
1288
1289fn map_atspi_action(action_name: &str) -> Option<Action> {
1291 match action_name.to_lowercase().as_str() {
1292 "click" | "activate" | "press" | "invoke" => Some(Action::Press),
1293 "toggle" | "check" | "uncheck" => Some(Action::Toggle),
1294 "expand" | "open" => Some(Action::Expand),
1295 "collapse" | "close" => Some(Action::Collapse),
1296 "select" => Some(Action::Select),
1297 "menu" | "showmenu" | "popup" | "show menu" => Some(Action::ShowMenu),
1298 "increment" => Some(Action::Increment),
1299 "decrement" => Some(Action::Decrement),
1300 _ => None,
1301 }
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307
1308 #[test]
1309 fn test_role_mapping() {
1310 assert_eq!(map_atspi_role("push button"), Role::Button);
1311 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1312 assert_eq!(map_atspi_role("entry"), Role::TextField);
1313 assert_eq!(map_atspi_role("label"), Role::StaticText);
1314 assert_eq!(map_atspi_role("window"), Role::Window);
1315 assert_eq!(map_atspi_role("frame"), Role::Window);
1316 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1317 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1318 assert_eq!(map_atspi_role("slider"), Role::Slider);
1319 assert_eq!(map_atspi_role("panel"), Role::Group);
1320 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1321 }
1322
1323 #[test]
1324 fn test_action_mapping() {
1325 assert_eq!(map_atspi_action("click"), Some(Action::Press));
1326 assert_eq!(map_atspi_action("activate"), Some(Action::Press));
1327 assert_eq!(map_atspi_action("toggle"), Some(Action::Toggle));
1328 assert_eq!(map_atspi_action("expand"), Some(Action::Expand));
1329 assert_eq!(map_atspi_action("collapse"), Some(Action::Collapse));
1330 assert_eq!(map_atspi_action("select"), Some(Action::Select));
1331 assert_eq!(map_atspi_action("increment"), Some(Action::Increment));
1332 assert_eq!(map_atspi_action("foobar"), None);
1333 }
1334}