1use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6use std::time::Duration;
7
8use rayon::prelude::*;
9use xa11y_core::selector::{Combinator, MatchOp, SelectorSegment};
10use xa11y_core::{
11 CancelHandle, ElementData, Error, Event, EventReceiver, EventType, Provider, Rect, Result,
12 Role, Selector, StateSet, Subscription, Toggled,
13};
14use zbus::blocking::{Connection, Proxy};
15
16static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
18
19pub struct LinuxProvider {
21 a11y_bus: Connection,
22 handle_cache: Mutex<HashMap<u64, AccessibleRef>>,
24 action_indices: Mutex<HashMap<u64, HashMap<String, i32>>>,
27}
28
29#[derive(Debug, Clone)]
31struct AccessibleRef {
32 bus_name: String,
33 path: String,
34}
35
36impl LinuxProvider {
37 pub fn new() -> Result<Self> {
42 let a11y_bus = Self::connect_a11y_bus()?;
43 Ok(Self {
44 a11y_bus,
45 handle_cache: Mutex::new(HashMap::new()),
46 action_indices: Mutex::new(HashMap::new()),
47 })
48 }
49
50 fn connect_a11y_bus() -> Result<Connection> {
51 if let Ok(session) = Connection::session() {
55 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
56 .map_err(|e| Error::Platform {
57 code: -1,
58 message: format!("Failed to create a11y bus proxy: {}", e),
59 })?;
60
61 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
62 if let Ok(address) = addr_reply.body().deserialize::<String>() {
63 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
64 if let Ok(Ok(conn)) =
65 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
66 {
67 return Ok(conn);
68 }
69 }
70 }
71 }
72
73 return Ok(session);
75 }
76
77 Connection::session().map_err(|e| Error::Platform {
78 code: -1,
79 message: format!("Failed to connect to D-Bus session bus: {}", e),
80 })
81 }
82
83 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
84 zbus::blocking::proxy::Builder::<Proxy>::new(&self.a11y_bus)
87 .destination(bus_name.to_owned())
88 .map_err(|e| Error::Platform {
89 code: -1,
90 message: format!("Failed to set proxy destination: {}", e),
91 })?
92 .path(path.to_owned())
93 .map_err(|e| Error::Platform {
94 code: -1,
95 message: format!("Failed to set proxy path: {}", e),
96 })?
97 .interface(interface.to_owned())
98 .map_err(|e| Error::Platform {
99 code: -1,
100 message: format!("Failed to set proxy interface: {}", e),
101 })?
102 .cache_properties(zbus::proxy::CacheProperties::No)
103 .build()
104 .map_err(|e| Error::Platform {
105 code: -1,
106 message: format!("Failed to create proxy: {}", e),
107 })
108 }
109
110 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
113 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
114 Ok(p) => p,
115 Err(_) => return false,
116 };
117 let reply = match proxy.call_method("GetInterfaces", &()) {
118 Ok(r) => r,
119 Err(_) => return false,
120 };
121 let interfaces: Vec<String> = match reply.body().deserialize() {
122 Ok(v) => v,
123 Err(_) => return false,
124 };
125 interfaces.iter().any(|i| i.contains(iface))
126 }
127
128 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
130 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
131 let reply = proxy
132 .call_method("GetRole", &())
133 .map_err(|e| Error::Platform {
134 code: -1,
135 message: format!("GetRole failed: {}", e),
136 })?;
137 reply
138 .body()
139 .deserialize::<u32>()
140 .map_err(|e| Error::Platform {
141 code: -1,
142 message: format!("GetRole deserialize failed: {}", e),
143 })
144 }
145
146 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
148 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
149 let reply = proxy
150 .call_method("GetRoleName", &())
151 .map_err(|e| Error::Platform {
152 code: -1,
153 message: format!("GetRoleName failed: {}", e),
154 })?;
155 reply
156 .body()
157 .deserialize::<String>()
158 .map_err(|e| Error::Platform {
159 code: -1,
160 message: format!("GetRoleName deserialize failed: {}", e),
161 })
162 }
163
164 fn check_chromium_a11y_enabled(
181 &self,
182 parent: &AccessibleRef,
183 role_hint: Option<Role>,
184 ) -> Result<()> {
185 let app_root = AccessibleRef {
186 bus_name: parent.bus_name.clone(),
187 path: "/org/a11y/atspi/accessible/root".to_string(),
188 };
189 let toolkit = match self
190 .make_proxy(
191 &app_root.bus_name,
192 &app_root.path,
193 "org.a11y.atspi.Application",
194 )
195 .ok()
196 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
197 {
198 Some(t) => t,
199 None => return Ok(()),
200 };
201 if !toolkit.eq_ignore_ascii_case("Chromium") {
202 return Ok(());
203 }
204 let role = role_hint.unwrap_or_else(|| self.resolve_role(parent));
205 if role != Role::Window {
206 return Ok(());
207 }
208 let app_name = self.get_name(&app_root).unwrap_or_default();
209 Err(Error::AccessibilityNotEnabled {
210 app: app_name,
211 instructions: "Chromium/Electron app exposes an empty accessibility tree on Linux. \
212 Relaunch with `--force-renderer-accessibility` (or set the env var \
213 `ACCESSIBILITY_ENABLED=1`) so the renderer accessibility bridge is initialised."
214 .to_string(),
215 })
216 }
217
218 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
220 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
221 proxy
222 .get_property::<String>("Name")
223 .map_err(|e| Error::Platform {
224 code: -1,
225 message: format!("Get Name property failed: {}", e),
226 })
227 }
228
229 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
231 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
232 proxy
233 .get_property::<String>("Description")
234 .map_err(|e| Error::Platform {
235 code: -1,
236 message: format!("Get Description property failed: {}", e),
237 })
238 }
239
240 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
244 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
245 let reply = proxy
246 .call_method("GetChildren", &())
247 .map_err(|e| Error::Platform {
248 code: -1,
249 message: format!("GetChildren failed: {}", e),
250 })?;
251 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
252 reply.body().deserialize().map_err(|e| Error::Platform {
253 code: -1,
254 message: format!("GetChildren deserialize failed: {}", e),
255 })?;
256 Ok(children
257 .into_iter()
258 .map(|(bus_name, path)| AccessibleRef {
259 bus_name,
260 path: path.to_string(),
261 })
262 .collect())
263 }
264
265 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
267 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
268 let reply = proxy
269 .call_method("GetState", &())
270 .map_err(|e| Error::Platform {
271 code: -1,
272 message: format!("GetState failed: {}", e),
273 })?;
274 reply
275 .body()
276 .deserialize::<Vec<u32>>()
277 .map_err(|e| Error::Platform {
278 code: -1,
279 message: format!("GetState deserialize failed: {}", e),
280 })
281 }
282
283 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
289 let state_bits = self.get_state(aref).unwrap_or_default();
290 let bits: u64 = if state_bits.len() >= 2 {
291 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
292 } else if state_bits.len() == 1 {
293 state_bits[0] as u64
294 } else {
295 0
296 };
297 const MULTI_LINE: u64 = 1 << 17;
299 (bits & MULTI_LINE) != 0
300 }
301
302 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
306 if !self.has_interface(aref, "Component") {
307 return None;
308 }
309 let proxy = self
310 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
311 .ok()?;
312 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
315 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
316 if w <= 0 && h <= 0 {
317 return None;
318 }
319 Some(Rect {
320 x,
321 y,
322 width: w.max(0) as u32,
323 height: h.max(0) as u32,
324 })
325 }
326
327 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
333 let mut actions = Vec::new();
334 let mut indices = HashMap::new();
335
336 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
338 let n_actions = proxy
340 .get_property::<i32>("NActions")
341 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
342 .unwrap_or(0);
343 for i in 0..n_actions {
344 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
345 if let Ok(name) = reply.body().deserialize::<String>() {
346 if let Some(action_name) = map_atspi_action_name(&name) {
347 if !actions.contains(&action_name) {
348 indices.insert(action_name.clone(), i);
349 actions.push(action_name);
350 }
351 }
352 }
353 }
354 }
355 }
356
357 if !actions.contains(&"focus".to_string()) {
359 if let Ok(proxy) =
360 self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
361 {
362 if proxy.call_method("GetExtents", &(0u32,)).is_ok() {
364 actions.push("focus".to_string());
365 }
366 }
367 }
368
369 (actions, indices)
370 }
371
372 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
375 let text_value = self.get_text_content(aref);
379 if text_value.is_some() {
380 return text_value;
381 }
382 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
384 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
385 return Some(val.to_string());
386 }
387 }
388 None
389 }
390
391 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
393 let proxy = self
394 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
395 .ok()?;
396 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
397 if char_count > 0 {
398 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
399 let text: String = reply.body().deserialize().ok()?;
400 if !text.is_empty() {
401 return Some(text);
402 }
403 }
404 None
405 }
406
407 fn cache_element(&self, aref: AccessibleRef) -> u64 {
409 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
410 self.handle_cache.lock().unwrap().insert(handle, aref);
411 handle
412 }
413
414 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
416 self.handle_cache
417 .lock()
418 .unwrap()
419 .get(&handle)
420 .cloned()
421 .ok_or(Error::ElementStale {
422 selector: format!("handle:{}", handle),
423 })
424 }
425
426 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
431 let role_name = self.get_role_name(aref).unwrap_or_default();
432 let role_num = self.get_role_number(aref).unwrap_or(0);
433 let role = {
434 let by_name = if !role_name.is_empty() {
435 map_atspi_role(&role_name)
436 } else {
437 Role::Unknown
438 };
439 let coarse = if by_name != Role::Unknown {
440 by_name
441 } else {
442 map_atspi_role_number(role_num)
443 };
444 if coarse == Role::TextArea && !self.is_multi_line(aref) {
445 Role::TextField
446 } else {
447 coarse
448 }
449 };
450
451 let (
455 ((mut name, value), description),
456 (
457 (states, bounds),
458 ((actions, action_index_map), (numeric_value, min_value, max_value)),
459 ),
460 ) = rayon::join(
461 || {
462 rayon::join(
463 || {
464 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
465 let value = if role_has_value(role) {
466 self.get_value(aref)
467 } else {
468 None
469 };
470 (name, value)
471 },
472 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
473 )
474 },
475 || {
476 rayon::join(
477 || {
478 rayon::join(
479 || self.parse_states(aref, role),
480 || {
481 if role != Role::Application {
482 self.get_extents(aref)
483 } else {
484 None
485 }
486 },
487 )
488 },
489 || {
490 rayon::join(
491 || {
492 if role_has_actions(role) {
493 self.get_actions(aref)
494 } else {
495 (vec![], HashMap::new())
496 }
497 },
498 || {
499 if matches!(
500 role,
501 Role::Slider
502 | Role::ProgressBar
503 | Role::ScrollBar
504 | Role::SpinButton
505 ) {
506 if let Ok(proxy) = self.make_proxy(
507 &aref.bus_name,
508 &aref.path,
509 "org.a11y.atspi.Value",
510 ) {
511 (
512 proxy.get_property::<f64>("CurrentValue").ok(),
513 proxy.get_property::<f64>("MinimumValue").ok(),
514 proxy.get_property::<f64>("MaximumValue").ok(),
515 )
516 } else {
517 (None, None, None)
518 }
519 } else {
520 (None, None, None)
521 }
522 },
523 )
524 },
525 )
526 },
527 );
528
529 if name.is_none() && role == Role::StaticText {
532 if let Some(ref v) = value {
533 name = Some(v.clone());
534 }
535 }
536
537 let raw = {
538 let raw_role = if role_name.is_empty() {
539 format!("role_num:{}", role_num)
540 } else {
541 role_name
542 };
543 {
544 let mut raw = HashMap::new();
545 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
546 raw.insert(
547 "bus_name".into(),
548 serde_json::Value::String(aref.bus_name.clone()),
549 );
550 raw.insert(
551 "object_path".into(),
552 serde_json::Value::String(aref.path.clone()),
553 );
554 raw
555 }
556 };
557
558 let handle = self.cache_element(aref.clone());
559 if !action_index_map.is_empty() {
560 self.action_indices
561 .lock()
562 .unwrap()
563 .insert(handle, action_index_map);
564 }
565
566 let mut data = ElementData {
567 role,
568 name,
569 value,
570 description,
571 bounds,
572 actions,
573 states,
574 numeric_value,
575 min_value,
576 max_value,
577 pid,
578 stable_id: Some(aref.path.clone()),
579 attributes: HashMap::new(),
580 raw,
581 handle,
582 };
583 data.populate_attributes();
584 data
585 }
586
587 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
589 let proxy = self.make_proxy(
591 &aref.bus_name,
592 &aref.path,
593 "org.freedesktop.DBus.Properties",
594 )?;
595 let reply = proxy
596 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
597 .map_err(|e| Error::Platform {
598 code: -1,
599 message: format!("Get Parent property failed: {}", e),
600 })?;
601 let variant: zbus::zvariant::OwnedValue =
603 reply.body().deserialize().map_err(|e| Error::Platform {
604 code: -1,
605 message: format!("Parent deserialize variant failed: {}", e),
606 })?;
607 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
608 zbus::zvariant::Value::from(variant).try_into().map_err(
609 |e: zbus::zvariant::Error| Error::Platform {
610 code: -1,
611 message: format!("Parent deserialize struct failed: {}", e),
612 },
613 )?;
614 let path_str = path.as_str();
615 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
616 return Ok(None);
617 }
618 if path_str == "/org/a11y/atspi/accessible/root" {
620 return Ok(None);
621 }
622 Ok(Some(AccessibleRef {
623 bus_name: bus,
624 path: path_str.to_string(),
625 }))
626 }
627
628 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
630 let state_bits = self.get_state(aref).unwrap_or_default();
631
632 let bits: u64 = if state_bits.len() >= 2 {
634 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
635 } else if state_bits.len() == 1 {
636 state_bits[0] as u64
637 } else {
638 0
639 };
640
641 const BUSY: u64 = 1 << 3;
643 const CHECKED: u64 = 1 << 4;
644 const EDITABLE: u64 = 1 << 7;
645 const ENABLED: u64 = 1 << 8;
646 const EXPANDABLE: u64 = 1 << 9;
647 const EXPANDED: u64 = 1 << 10;
648 const FOCUSABLE: u64 = 1 << 11;
649 const FOCUSED: u64 = 1 << 12;
650 const MODAL: u64 = 1 << 16;
651 const SELECTED: u64 = 1 << 23;
652 const SENSITIVE: u64 = 1 << 24;
653 const SHOWING: u64 = 1 << 25;
654 const VISIBLE: u64 = 1 << 30;
655 const INDETERMINATE: u64 = 1 << 32;
656 const REQUIRED: u64 = 1 << 33;
657
658 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
659 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
660
661 let checked = match role {
662 Role::CheckBox | Role::RadioButton | Role::MenuItem => {
663 if (bits & INDETERMINATE) != 0 {
664 Some(Toggled::Mixed)
665 } else if (bits & CHECKED) != 0 {
666 Some(Toggled::On)
667 } else {
668 Some(Toggled::Off)
669 }
670 }
671 _ => None,
672 };
673
674 let expanded = if (bits & EXPANDABLE) != 0 {
675 Some((bits & EXPANDED) != 0)
676 } else {
677 None
678 };
679
680 StateSet {
681 enabled,
682 visible,
683 focused: (bits & FOCUSED) != 0,
684 checked,
685 selected: (bits & SELECTED) != 0,
686 expanded,
687 editable: (bits & EDITABLE) != 0,
688 focusable: (bits & FOCUSABLE) != 0,
689 modal: (bits & MODAL) != 0,
690 required: (bits & REQUIRED) != 0,
691 busy: (bits & BUSY) != 0,
692 }
693 }
694
695 fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
697 let registry = AccessibleRef {
698 bus_name: "org.a11y.atspi.Registry".to_string(),
699 path: "/org/a11y/atspi/accessible/root".to_string(),
700 };
701 let children = self.get_atspi_children(®istry)?;
702
703 for child in &children {
704 if child.path == "/org/a11y/atspi/null" {
705 continue;
706 }
707 if let Ok(proxy) =
709 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
710 {
711 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
712 if app_pid as u32 == pid {
713 return Ok(child.clone());
714 }
715 }
716 }
717 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
719 if app_pid == pid {
720 return Ok(child.clone());
721 }
722 }
723 }
724
725 Err(Error::Platform {
726 code: -1,
727 message: format!("No application found with PID {}", pid),
728 })
729 }
730
731 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
733 let proxy = self
734 .make_proxy(
735 "org.freedesktop.DBus",
736 "/org/freedesktop/DBus",
737 "org.freedesktop.DBus",
738 )
739 .ok()?;
740 let reply = proxy
741 .call_method("GetConnectionUnixProcessID", &(bus_name,))
742 .ok()?;
743 let pid: u32 = reply.body().deserialize().ok()?;
744 if pid > 0 {
745 Some(pid)
746 } else {
747 None
748 }
749 }
750
751 fn do_atspi_action_by_name(&self, aref: &AccessibleRef, action_name: &str) -> Result<()> {
754 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
755 let n_actions = proxy
756 .get_property::<i32>("NActions")
757 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
758 .unwrap_or(0);
759 for i in 0..n_actions {
760 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
761 if let Ok(name) = reply.body().deserialize::<String>() {
762 if name.eq_ignore_ascii_case(action_name) {
763 proxy
764 .call_method("DoAction", &(i,))
765 .map_err(|e| Error::Platform {
766 code: -1,
767 message: format!("DoAction failed: {}", e),
768 })?;
769 return Ok(());
770 }
771 }
772 }
773 }
774 Err(Error::Platform {
775 code: -1,
776 message: format!("Action '{}' not found", action_name),
777 })
778 }
779
780 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
782 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
783 proxy
784 .call_method("DoAction", &(index,))
785 .map_err(|e| Error::Platform {
786 code: -1,
787 message: format!("DoAction({}) failed: {}", index, e),
788 })?;
789 Ok(())
790 }
791
792 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
794 self.action_indices
795 .lock()
796 .unwrap()
797 .get(&handle)
798 .and_then(|map| map.get(action).copied())
799 .ok_or_else(|| Error::ActionNotSupported {
800 action: action.to_string(),
801 role: Role::Unknown, })
803 }
804
805 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
807 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
809 {
810 if let Ok(pid) = proxy.get_property::<i32>("Id") {
811 if pid > 0 {
812 return Some(pid as u32);
813 }
814 }
815 }
816
817 if let Ok(proxy) = self.make_proxy(
819 "org.freedesktop.DBus",
820 "/org/freedesktop/DBus",
821 "org.freedesktop.DBus",
822 ) {
823 if let Ok(reply) =
824 proxy.call_method("GetConnectionUnixProcessID", &(aref.bus_name.as_str(),))
825 {
826 if let Ok(pid) = reply.body().deserialize::<u32>() {
827 if pid > 0 {
828 return Some(pid);
829 }
830 }
831 }
832 }
833
834 None
835 }
836
837 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
839 let role_name = self.get_role_name(aref).unwrap_or_default();
840 let by_name = if !role_name.is_empty() {
841 map_atspi_role(&role_name)
842 } else {
843 Role::Unknown
844 };
845 let coarse = if by_name != Role::Unknown {
846 by_name
847 } else {
848 let role_num = self.get_role_number(aref).unwrap_or(0);
850 map_atspi_role_number(role_num)
851 };
852 if coarse == Role::TextArea && !self.is_multi_line(aref) {
854 Role::TextField
855 } else {
856 coarse
857 }
858 }
859
860 fn matches_ref(
863 &self,
864 aref: &AccessibleRef,
865 simple: &xa11y_core::selector::SimpleSelector,
866 ) -> bool {
867 let needs_role = simple.role.is_some() || simple.filters.iter().any(|f| f.attr == "role");
869 let role = if needs_role {
870 Some(self.resolve_role(aref))
871 } else {
872 None
873 };
874
875 if let Some(ref role_match) = simple.role {
876 match role_match {
877 xa11y_core::selector::RoleMatch::Normalized(expected) => {
878 if role != Some(*expected) {
879 return false;
880 }
881 }
882 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
883 let raw_role = self.get_role_name(aref).unwrap_or_default();
885 if raw_role != *platform_role {
886 return false;
887 }
888 }
889 }
890 }
891
892 for filter in &simple.filters {
893 let attr_value: Option<String> = match filter.attr.as_str() {
894 "role" => role.map(|r| r.to_snake_case().to_string()),
895 "name" => {
896 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
897 if name.is_none() && role == Some(Role::StaticText) {
899 self.get_value(aref)
900 } else {
901 name
902 }
903 }
904 "value" => self.get_value(aref),
905 "description" => self.get_description(aref).ok().filter(|s| !s.is_empty()),
906 _ => None,
908 };
909
910 let matches = match &filter.op {
911 MatchOp::Exact => attr_value.as_deref() == Some(filter.value.as_str()),
912 MatchOp::Contains => {
913 let fl = filter.value.to_lowercase();
914 attr_value
915 .as_deref()
916 .is_some_and(|v| v.to_lowercase().contains(&fl))
917 }
918 MatchOp::StartsWith => {
919 let fl = filter.value.to_lowercase();
920 attr_value
921 .as_deref()
922 .is_some_and(|v| v.to_lowercase().starts_with(&fl))
923 }
924 MatchOp::EndsWith => {
925 let fl = filter.value.to_lowercase();
926 attr_value
927 .as_deref()
928 .is_some_and(|v| v.to_lowercase().ends_with(&fl))
929 }
930 };
931
932 if !matches {
933 return false;
934 }
935 }
936
937 true
938 }
939
940 fn collect_matching_refs(
946 &self,
947 parent: &AccessibleRef,
948 simple: &xa11y_core::selector::SimpleSelector,
949 depth: u32,
950 max_depth: u32,
951 limit: Option<usize>,
952 ) -> Result<Vec<AccessibleRef>> {
953 if depth > max_depth {
954 return Ok(vec![]);
955 }
956
957 let children = self.get_atspi_children(parent)?;
958
959 let mut to_search: Vec<AccessibleRef> = Vec::new();
961 for child in children {
962 if child.path == "/org/a11y/atspi/null"
963 || child.bus_name.is_empty()
964 || child.path.is_empty()
965 {
966 continue;
967 }
968
969 let child_role = self.get_role_name(&child).unwrap_or_default();
970 if child_role == "application" {
971 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
972 for gc in grandchildren {
973 if gc.path == "/org/a11y/atspi/null"
974 || gc.bus_name.is_empty()
975 || gc.path.is_empty()
976 {
977 continue;
978 }
979 let gc_role = self.get_role_name(&gc).unwrap_or_default();
980 if gc_role == "application" {
981 continue;
982 }
983 to_search.push(gc);
984 }
985 continue;
986 }
987 to_search.push(child);
988 }
989
990 if to_search.is_empty() {
993 self.check_chromium_a11y_enabled(parent, None)?;
994 }
995
996 let per_child: Vec<Vec<AccessibleRef>> = to_search
998 .par_iter()
999 .map(|child| {
1000 let mut child_results = Vec::new();
1001 if self.matches_ref(child, simple) {
1002 child_results.push(child.clone());
1003 }
1004 if let Ok(sub) =
1005 self.collect_matching_refs(child, simple, depth + 1, max_depth, limit)
1006 {
1007 child_results.extend(sub);
1008 }
1009 child_results
1010 })
1011 .collect();
1012
1013 let mut results = Vec::new();
1015 for batch in per_child {
1016 for r in batch {
1017 results.push(r);
1018 if let Some(limit) = limit {
1019 if results.len() >= limit {
1020 return Ok(results);
1021 }
1022 }
1023 }
1024 }
1025 Ok(results)
1026 }
1027}
1028
1029impl Provider for LinuxProvider {
1030 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1031 match element {
1032 None => {
1033 let registry = AccessibleRef {
1035 bus_name: "org.a11y.atspi.Registry".to_string(),
1036 path: "/org/a11y/atspi/accessible/root".to_string(),
1037 };
1038 let children = self.get_atspi_children(®istry)?;
1039
1040 let valid: Vec<(&AccessibleRef, String)> = children
1042 .iter()
1043 .filter(|c| c.path != "/org/a11y/atspi/null")
1044 .filter_map(|c| {
1045 let name = self.get_name(c).unwrap_or_default();
1046 if name.is_empty() {
1047 None
1048 } else {
1049 Some((c, name))
1050 }
1051 })
1052 .collect();
1053
1054 let results: Vec<ElementData> = valid
1055 .par_iter()
1056 .map(|(child, app_name)| {
1057 let pid = self.get_app_pid(child);
1058 let mut data = self.build_element_data(child, pid);
1059 data.name = Some(app_name.clone());
1060 data
1061 })
1062 .collect();
1063
1064 Ok(results)
1065 }
1066 Some(element_data) => {
1067 let aref = self.get_cached(element_data.handle)?;
1068 let children = self.get_atspi_children(&aref).unwrap_or_default();
1069 let pid = element_data.pid;
1070
1071 let mut to_build: Vec<AccessibleRef> = Vec::new();
1074 for child_ref in &children {
1075 if child_ref.path == "/org/a11y/atspi/null"
1076 || child_ref.bus_name.is_empty()
1077 || child_ref.path.is_empty()
1078 {
1079 continue;
1080 }
1081 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1082 if child_role == "application" {
1083 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1084 for gc_ref in grandchildren {
1085 if gc_ref.path == "/org/a11y/atspi/null"
1086 || gc_ref.bus_name.is_empty()
1087 || gc_ref.path.is_empty()
1088 {
1089 continue;
1090 }
1091 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1092 if gc_role == "application" {
1093 continue;
1094 }
1095 to_build.push(gc_ref);
1096 }
1097 continue;
1098 }
1099 to_build.push(child_ref.clone());
1100 }
1101
1102 if to_build.is_empty() {
1103 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1104 }
1105
1106 let results: Vec<ElementData> = to_build
1107 .par_iter()
1108 .map(|r| self.build_element_data(r, pid))
1109 .collect();
1110
1111 Ok(results)
1112 }
1113 }
1114 }
1115
1116 fn find_elements(
1117 &self,
1118 root: Option<&ElementData>,
1119 selector: &Selector,
1120 limit: Option<usize>,
1121 max_depth: Option<u32>,
1122 ) -> Result<Vec<ElementData>> {
1123 if selector.segments.is_empty() {
1124 return Ok(vec![]);
1125 }
1126
1127 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1128
1129 let first = &selector.segments[0].simple;
1132
1133 let phase1_limit = if selector.segments.len() == 1 {
1134 limit
1135 } else {
1136 None
1137 };
1138 let phase1_limit = match (phase1_limit, first.nth) {
1139 (Some(l), Some(n)) => Some(l.max(n)),
1140 (_, Some(n)) => Some(n),
1141 (l, None) => l,
1142 };
1143
1144 let phase1_depth = if root.is_none()
1146 && matches!(
1147 first.role,
1148 Some(xa11y_core::selector::RoleMatch::Normalized(
1149 Role::Application
1150 ))
1151 ) {
1152 0
1153 } else {
1154 max_depth_val
1155 };
1156
1157 let start_ref = match root {
1158 None => AccessibleRef {
1159 bus_name: "org.a11y.atspi.Registry".to_string(),
1160 path: "/org/a11y/atspi/accessible/root".to_string(),
1161 },
1162 Some(el) => self.get_cached(el.handle)?,
1163 };
1164
1165 let mut matching_refs =
1166 self.collect_matching_refs(&start_ref, first, 0, phase1_depth, phase1_limit)?;
1167
1168 let pid_from_root = root.and_then(|r| r.pid);
1169
1170 if selector.segments.len() == 1 {
1172 if let Some(nth) = first.nth {
1173 if nth <= matching_refs.len() {
1174 let aref = &matching_refs[nth - 1];
1175 let pid = if root.is_none() {
1176 self.get_app_pid(aref)
1177 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1178 } else {
1179 pid_from_root
1180 };
1181 return Ok(vec![self.build_element_data(aref, pid)]);
1182 } else {
1183 return Ok(vec![]);
1184 }
1185 }
1186
1187 if let Some(limit) = limit {
1188 matching_refs.truncate(limit);
1189 }
1190
1191 let is_root_search = root.is_none();
1192 return Ok(matching_refs
1193 .par_iter()
1194 .map(|aref| {
1195 let pid = if is_root_search {
1196 self.get_app_pid(aref)
1197 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1198 } else {
1199 pid_from_root
1200 };
1201 self.build_element_data(aref, pid)
1202 })
1203 .collect());
1204 }
1205
1206 let is_root_search = root.is_none();
1209 let mut candidates: Vec<ElementData> = matching_refs
1210 .par_iter()
1211 .map(|aref| {
1212 let pid = if is_root_search {
1213 self.get_app_pid(aref)
1214 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1215 } else {
1216 pid_from_root
1217 };
1218 self.build_element_data(aref, pid)
1219 })
1220 .collect();
1221
1222 for segment in &selector.segments[1..] {
1223 let mut next_candidates = Vec::new();
1224 for candidate in &candidates {
1225 match segment.combinator {
1226 Combinator::Child => {
1227 let children = self.get_children(Some(candidate))?;
1228 for child in children {
1229 if xa11y_core::selector::matches_simple(&child, &segment.simple) {
1230 next_candidates.push(child);
1231 }
1232 }
1233 }
1234 Combinator::Descendant => {
1235 let sub_selector = Selector {
1236 segments: vec![SelectorSegment {
1237 combinator: Combinator::Root,
1238 simple: segment.simple.clone(),
1239 }],
1240 };
1241 let mut sub_results = xa11y_core::selector::find_elements_in_tree(
1242 |el| self.get_children(el),
1243 Some(candidate),
1244 &sub_selector,
1245 None,
1246 Some(max_depth_val),
1247 )?;
1248 next_candidates.append(&mut sub_results);
1249 }
1250 Combinator::Root => unreachable!(),
1251 }
1252 }
1253 let mut seen = HashSet::new();
1254 next_candidates.retain(|e| seen.insert(e.handle));
1255 candidates = next_candidates;
1256 }
1257
1258 if let Some(nth) = selector.segments.last().and_then(|s| s.simple.nth) {
1260 if nth <= candidates.len() {
1261 candidates = vec![candidates.remove(nth - 1)];
1262 } else {
1263 candidates.clear();
1264 }
1265 }
1266
1267 if let Some(limit) = limit {
1268 candidates.truncate(limit);
1269 }
1270
1271 Ok(candidates)
1272 }
1273
1274 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1275 let aref = self.get_cached(element.handle)?;
1276 match self.get_atspi_parent(&aref)? {
1277 Some(parent_ref) => {
1278 let data = self.build_element_data(&parent_ref, element.pid);
1279 Ok(Some(data))
1280 }
1281 None => Ok(None),
1282 }
1283 }
1284
1285 fn press(&self, element: &ElementData) -> Result<()> {
1286 let target = self.get_cached(element.handle)?;
1287 let index = self
1288 .get_action_index(element.handle, "press")
1289 .map_err(|_| Error::ActionNotSupported {
1290 action: "press".to_string(),
1291 role: element.role,
1292 })?;
1293 self.do_atspi_action_by_index(&target, index)
1294 }
1295
1296 fn focus(&self, element: &ElementData) -> Result<()> {
1297 let target = self.get_cached(element.handle)?;
1298 if let Ok(proxy) =
1300 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1301 {
1302 if proxy.call_method("GrabFocus", &()).is_ok() {
1303 return Ok(());
1304 }
1305 }
1306 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1307 return self.do_atspi_action_by_index(&target, index);
1308 }
1309 Err(Error::ActionNotSupported {
1310 action: "focus".to_string(),
1311 role: element.role,
1312 })
1313 }
1314
1315 fn blur(&self, element: &ElementData) -> Result<()> {
1316 let target = self.get_cached(element.handle)?;
1317 if let Ok(Some(parent_ref)) = self.get_atspi_parent(&target) {
1319 if parent_ref.path != "/org/a11y/atspi/null" {
1320 if let Ok(p) = self.make_proxy(
1321 &parent_ref.bus_name,
1322 &parent_ref.path,
1323 "org.a11y.atspi.Component",
1324 ) {
1325 let _ = p.call_method("GrabFocus", &());
1326 return Ok(());
1327 }
1328 }
1329 }
1330 Ok(())
1331 }
1332
1333 fn toggle(&self, element: &ElementData) -> Result<()> {
1334 let target = self.get_cached(element.handle)?;
1335 let index = self
1336 .get_action_index(element.handle, "toggle")
1337 .map_err(|_| Error::ActionNotSupported {
1338 action: "toggle".to_string(),
1339 role: element.role,
1340 })?;
1341 self.do_atspi_action_by_index(&target, index)
1342 }
1343
1344 fn select(&self, element: &ElementData) -> Result<()> {
1345 let target = self.get_cached(element.handle)?;
1346 let index = self
1347 .get_action_index(element.handle, "select")
1348 .map_err(|_| Error::ActionNotSupported {
1349 action: "select".to_string(),
1350 role: element.role,
1351 })?;
1352 self.do_atspi_action_by_index(&target, index)
1353 }
1354
1355 fn expand(&self, element: &ElementData) -> Result<()> {
1356 let target = self.get_cached(element.handle)?;
1357 let index = self
1358 .get_action_index(element.handle, "expand")
1359 .map_err(|_| Error::ActionNotSupported {
1360 action: "expand".to_string(),
1361 role: element.role,
1362 })?;
1363 self.do_atspi_action_by_index(&target, index)
1364 }
1365
1366 fn collapse(&self, element: &ElementData) -> Result<()> {
1367 let target = self.get_cached(element.handle)?;
1368 let index = self
1369 .get_action_index(element.handle, "collapse")
1370 .map_err(|_| Error::ActionNotSupported {
1371 action: "collapse".to_string(),
1372 role: element.role,
1373 })?;
1374 self.do_atspi_action_by_index(&target, index)
1375 }
1376
1377 fn show_menu(&self, element: &ElementData) -> Result<()> {
1378 let target = self.get_cached(element.handle)?;
1379 let index = self
1380 .get_action_index(element.handle, "show_menu")
1381 .map_err(|_| Error::ActionNotSupported {
1382 action: "show_menu".to_string(),
1383 role: element.role,
1384 })?;
1385 self.do_atspi_action_by_index(&target, index)
1386 }
1387
1388 fn increment(&self, element: &ElementData) -> Result<()> {
1389 let target = self.get_cached(element.handle)?;
1390 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1392 return self.do_atspi_action_by_index(&target, index);
1393 }
1394 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1395 let current: f64 = proxy
1396 .get_property("CurrentValue")
1397 .map_err(|e| Error::Platform {
1398 code: -1,
1399 message: format!("Value.CurrentValue failed: {}", e),
1400 })?;
1401 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1402 let step = if step <= 0.0 { 1.0 } else { step };
1403 proxy
1404 .set_property("CurrentValue", current + step)
1405 .map_err(|e| Error::Platform {
1406 code: -1,
1407 message: format!("Value.SetCurrentValue failed: {}", e),
1408 })
1409 }
1410
1411 fn decrement(&self, element: &ElementData) -> Result<()> {
1412 let target = self.get_cached(element.handle)?;
1413 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1414 return self.do_atspi_action_by_index(&target, index);
1415 }
1416 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1417 let current: f64 = proxy
1418 .get_property("CurrentValue")
1419 .map_err(|e| Error::Platform {
1420 code: -1,
1421 message: format!("Value.CurrentValue failed: {}", e),
1422 })?;
1423 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1424 let step = if step <= 0.0 { 1.0 } else { step };
1425 proxy
1426 .set_property("CurrentValue", current - step)
1427 .map_err(|e| Error::Platform {
1428 code: -1,
1429 message: format!("Value.SetCurrentValue failed: {}", e),
1430 })
1431 }
1432
1433 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1434 let target = self.get_cached(element.handle)?;
1435 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1436 proxy
1437 .call_method("ScrollTo", &(0u32,))
1438 .map_err(|e| Error::Platform {
1439 code: -1,
1440 message: format!("ScrollTo failed: {}", e),
1441 })?;
1442 Ok(())
1443 }
1444
1445 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1446 let target = self.get_cached(element.handle)?;
1447 let proxy = self
1448 .make_proxy(
1449 &target.bus_name,
1450 &target.path,
1451 "org.a11y.atspi.EditableText",
1452 )
1453 .map_err(|_| Error::TextValueNotSupported)?;
1454 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1456 return Ok(());
1457 }
1458 let _ = proxy.call_method("DeleteText", &(0i32, i32::MAX));
1460 proxy
1461 .call_method("InsertText", &(0i32, value, value.len() as i32))
1462 .map_err(|_| Error::TextValueNotSupported)?;
1463 Ok(())
1464 }
1465
1466 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1467 let target = self.get_cached(element.handle)?;
1468 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1469 proxy
1470 .set_property("CurrentValue", value)
1471 .map_err(|e| Error::Platform {
1472 code: -1,
1473 message: format!("SetValue failed: {}", e),
1474 })
1475 }
1476
1477 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1478 let target = self.get_cached(element.handle)?;
1479 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1482 let insert_pos = text_proxy
1483 .as_ref()
1484 .ok()
1485 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1486 .unwrap_or(-1); let proxy = self
1489 .make_proxy(
1490 &target.bus_name,
1491 &target.path,
1492 "org.a11y.atspi.EditableText",
1493 )
1494 .map_err(|_| Error::TextValueNotSupported)?;
1495 let pos = if insert_pos >= 0 {
1496 insert_pos
1497 } else {
1498 i32::MAX
1499 };
1500 proxy
1501 .call_method("InsertText", &(pos, text, text.len() as i32))
1502 .map_err(|e| Error::Platform {
1503 code: -1,
1504 message: format!("EditableText.InsertText failed: {}", e),
1505 })?;
1506 Ok(())
1507 }
1508
1509 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1510 let target = self.get_cached(element.handle)?;
1511 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1512 if proxy
1514 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1515 .is_err()
1516 {
1517 proxy
1518 .call_method("AddSelection", &(start as i32, end as i32))
1519 .map_err(|e| Error::Platform {
1520 code: -1,
1521 message: format!("Text.AddSelection failed: {}", e),
1522 })?;
1523 }
1524 Ok(())
1525 }
1526
1527 fn scroll_down(&self, element: &ElementData, amount: f64) -> Result<()> {
1528 let target = self.get_cached(element.handle)?;
1529 let count = (amount.abs() as u32).max(1);
1530 for _ in 0..count {
1531 if self
1532 .do_atspi_action_by_name(&target, "scroll down")
1533 .is_err()
1534 {
1535 if let Ok(proxy) =
1539 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1540 {
1541 if proxy.call_method("ScrollTo", &(3u32,)).is_ok() {
1543 return Ok(());
1544 }
1545 }
1546 return Err(Error::ActionNotSupported {
1547 action: "scroll_down".to_string(),
1548 role: element.role,
1549 });
1550 }
1551 }
1552 Ok(())
1553 }
1554
1555 fn scroll_up(&self, element: &ElementData, amount: f64) -> Result<()> {
1556 let target = self.get_cached(element.handle)?;
1557 let count = (amount.abs() as u32).max(1);
1558 for _ in 0..count {
1559 if self.do_atspi_action_by_name(&target, "scroll up").is_err() {
1560 if let Ok(proxy) =
1564 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1565 {
1566 if proxy.call_method("ScrollTo", &(2u32,)).is_ok() {
1568 return Ok(());
1569 }
1570 }
1571 return Err(Error::ActionNotSupported {
1572 action: "scroll_up".to_string(),
1573 role: element.role,
1574 });
1575 }
1576 }
1577 Ok(())
1578 }
1579
1580 fn scroll_right(&self, element: &ElementData, amount: f64) -> Result<()> {
1581 let target = self.get_cached(element.handle)?;
1582 let count = (amount.abs() as u32).max(1);
1583 for _ in 0..count {
1584 if self
1585 .do_atspi_action_by_name(&target, "scroll right")
1586 .is_err()
1587 {
1588 if let Ok(proxy) =
1592 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1593 {
1594 if proxy.call_method("ScrollTo", &(5u32,)).is_ok() {
1596 return Ok(());
1597 }
1598 }
1599 return Err(Error::ActionNotSupported {
1600 action: "scroll_right".to_string(),
1601 role: element.role,
1602 });
1603 }
1604 }
1605 Ok(())
1606 }
1607
1608 fn scroll_left(&self, element: &ElementData, amount: f64) -> Result<()> {
1609 let target = self.get_cached(element.handle)?;
1610 let count = (amount.abs() as u32).max(1);
1611 for _ in 0..count {
1612 if self
1613 .do_atspi_action_by_name(&target, "scroll left")
1614 .is_err()
1615 {
1616 if let Ok(proxy) =
1620 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1621 {
1622 if proxy.call_method("ScrollTo", &(4u32,)).is_ok() {
1624 return Ok(());
1625 }
1626 }
1627 return Err(Error::ActionNotSupported {
1628 action: "scroll_left".to_string(),
1629 role: element.role,
1630 });
1631 }
1632 }
1633 Ok(())
1634 }
1635
1636 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1637 match action {
1638 "press" => self.press(element),
1639 "focus" => self.focus(element),
1640 "blur" => self.blur(element),
1641 "toggle" => self.toggle(element),
1642 "select" => self.select(element),
1643 "expand" => self.expand(element),
1644 "collapse" => self.collapse(element),
1645 "show_menu" => self.show_menu(element),
1646 "increment" => self.increment(element),
1647 "decrement" => self.decrement(element),
1648 "scroll_into_view" => self.scroll_into_view(element),
1649 _ => Err(Error::ActionNotSupported {
1650 action: action.to_string(),
1651 role: element.role,
1652 }),
1653 }
1654 }
1655
1656 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1657 let pid = element.pid.ok_or(Error::Platform {
1658 code: -1,
1659 message: "Element has no PID for subscribe".to_string(),
1660 })?;
1661 let app_name = element.name.clone().unwrap_or_default();
1662 self.subscribe_impl(app_name, pid, pid)
1663 }
1664}
1665
1666impl LinuxProvider {
1669 fn subscribe_impl(&self, app_name: String, app_pid: u32, pid: u32) -> Result<Subscription> {
1671 let (tx, rx) = std::sync::mpsc::channel();
1672 let poll_provider = LinuxProvider::new()?;
1673 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1674 let stop_clone = stop.clone();
1675
1676 let handle = std::thread::spawn(move || {
1677 let mut prev_focused: Option<String> = None;
1678 let mut prev_element_count: usize = 0;
1679
1680 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1681 std::thread::sleep(Duration::from_millis(100));
1682
1683 let app_ref = match poll_provider.find_app_by_pid(pid) {
1685 Ok(r) => r,
1686 Err(_) => continue,
1687 };
1688 let app_data = poll_provider.build_element_data(&app_ref, Some(pid));
1689
1690 let mut stack = vec![app_data];
1692 let mut element_count: usize = 0;
1693 let mut focused_element: Option<ElementData> = None;
1694 let mut visited = HashSet::new();
1695
1696 while let Some(el) = stack.pop() {
1697 let path_key = format!("{:?}:{}", el.raw, el.handle);
1698 if !visited.insert(path_key) {
1699 continue;
1700 }
1701 element_count += 1;
1702 if el.states.focused && focused_element.is_none() {
1703 focused_element = Some(el.clone());
1704 }
1705 if let Ok(children) = poll_provider.get_children(Some(&el)) {
1706 stack.extend(children);
1707 }
1708 }
1709
1710 let focused_name = focused_element.as_ref().and_then(|e| e.name.clone());
1711 if focused_name != prev_focused {
1712 if prev_focused.is_some() {
1713 let _ = tx.send(Event {
1714 event_type: EventType::FocusChanged,
1715 app_name: app_name.clone(),
1716 app_pid,
1717 target: focused_element,
1718 state_flag: None,
1719 state_value: None,
1720 text_change: None,
1721 timestamp: std::time::Instant::now(),
1722 });
1723 }
1724 prev_focused = focused_name;
1725 }
1726
1727 if element_count != prev_element_count && prev_element_count > 0 {
1728 let _ = tx.send(Event {
1729 event_type: EventType::StructureChanged,
1730 app_name: app_name.clone(),
1731 app_pid,
1732 target: None,
1733 state_flag: None,
1734 state_value: None,
1735 text_change: None,
1736 timestamp: std::time::Instant::now(),
1737 });
1738 }
1739 prev_element_count = element_count;
1740 }
1741 });
1742
1743 let cancel = CancelHandle::new(move || {
1744 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1745 let _ = handle.join();
1746 });
1747
1748 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1749 }
1750}
1751
1752fn role_has_value(role: Role) -> bool {
1755 !matches!(
1756 role,
1757 Role::Application
1758 | Role::Window
1759 | Role::Dialog
1760 | Role::Group
1761 | Role::MenuBar
1762 | Role::Toolbar
1763 | Role::TabGroup
1764 | Role::SplitGroup
1765 | Role::Table
1766 | Role::TableRow
1767 | Role::Separator
1768 )
1769}
1770
1771fn role_has_actions(role: Role) -> bool {
1774 matches!(
1775 role,
1776 Role::Button
1777 | Role::CheckBox
1778 | Role::RadioButton
1779 | Role::MenuItem
1780 | Role::Link
1781 | Role::ComboBox
1782 | Role::TextField
1783 | Role::TextArea
1784 | Role::SpinButton
1785 | Role::Tab
1786 | Role::TreeItem
1787 | Role::ListItem
1788 | Role::ScrollBar
1789 | Role::Slider
1790 | Role::Menu
1791 | Role::Image
1792 | Role::Unknown
1793 )
1794}
1795
1796fn map_atspi_role(role_name: &str) -> Role {
1798 match role_name.to_lowercase().as_str() {
1799 "application" => Role::Application,
1800 "window" | "frame" => Role::Window,
1801 "dialog" | "file chooser" => Role::Dialog,
1802 "alert" | "notification" => Role::Alert,
1803 "push button" | "push button menu" => Role::Button,
1804 "toggle button" => Role::Switch,
1805 "check box" | "check menu item" => Role::CheckBox,
1806 "radio button" | "radio menu item" => Role::RadioButton,
1807 "entry" | "password text" => Role::TextField,
1808 "spin button" | "spinbutton" => Role::SpinButton,
1809 "text" | "textbox" => Role::TextArea,
1813 "label" | "static" | "caption" => Role::StaticText,
1814 "combo box" | "combobox" => Role::ComboBox,
1815 "list" | "list box" | "listbox" => Role::List,
1817 "list item" => Role::ListItem,
1818 "menu" => Role::Menu,
1819 "menu item" | "tearoff menu item" => Role::MenuItem,
1820 "menu bar" => Role::MenuBar,
1821 "page tab" => Role::Tab,
1822 "page tab list" => Role::TabGroup,
1823 "table" | "tree table" => Role::Table,
1824 "table row" => Role::TableRow,
1825 "table cell" | "table column header" | "table row header" => Role::TableCell,
1826 "tool bar" => Role::Toolbar,
1827 "scroll bar" => Role::ScrollBar,
1828 "slider" => Role::Slider,
1829 "image" | "icon" | "desktop icon" => Role::Image,
1830 "link" => Role::Link,
1831 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1832 "progress bar" => Role::ProgressBar,
1833 "tree item" => Role::TreeItem,
1834 "document web" | "document frame" => Role::WebArea,
1835 "heading" => Role::Heading,
1836 "separator" => Role::Separator,
1837 "split pane" => Role::SplitGroup,
1838 "tooltip" | "tool tip" => Role::Tooltip,
1839 "status bar" | "statusbar" => Role::Status,
1840 "landmark" | "navigation" => Role::Navigation,
1841 _ => xa11y_core::unknown_role(role_name),
1842 }
1843}
1844
1845fn map_atspi_role_number(role: u32) -> Role {
1848 match role {
1849 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::Switch, 63 => Role::Toolbar, 65 => Role::Group, 66 => Role::Table, 67 => Role::Unknown, 68 => Role::Group, 69 => Role::Window, 75 => Role::Application, 78 => Role::TextArea, 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, 97 => Role::List, 98 => Role::List, 93 => Role::Tooltip, 101 => Role::Alert, 116 => Role::StaticText, 129 => Role::Button, _ => xa11y_core::unknown_role(&format!("AT-SPI role number {role}")),
1911 }
1912}
1913
1914fn map_atspi_action_name(action_name: &str) -> Option<String> {
1928 let lower = action_name.to_lowercase();
1929 let canonical = match lower.as_str() {
1930 "click" | "activate" | "press" | "invoke" => "press",
1931 "toggle" | "check" | "uncheck" => "toggle",
1932 "expand" | "open" => "expand",
1933 "collapse" | "close" => "collapse",
1934 "select" => "select",
1935 "menu" | "showmenu" | "show_menu" | "popup" | "show menu" => "show_menu",
1936 "increment" => "increment",
1937 "decrement" => "decrement",
1938 _ => return None,
1939 };
1940 Some(canonical.to_string())
1941}
1942
1943#[cfg(test)]
1944mod tests {
1945 use super::*;
1946
1947 #[test]
1948 fn test_role_mapping() {
1949 assert_eq!(map_atspi_role("push button"), Role::Button);
1950 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
1951 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1952 assert_eq!(map_atspi_role("entry"), Role::TextField);
1953 assert_eq!(map_atspi_role("label"), Role::StaticText);
1954 assert_eq!(map_atspi_role("window"), Role::Window);
1955 assert_eq!(map_atspi_role("frame"), Role::Window);
1956 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1957 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1958 assert_eq!(map_atspi_role("slider"), Role::Slider);
1959 assert_eq!(map_atspi_role("panel"), Role::Group);
1960 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1961 }
1962
1963 #[test]
1964 fn test_numeric_role_mapping() {
1965 assert_eq!(map_atspi_role_number(62), Role::Switch);
1968 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
1971 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
1973
1974 #[test]
1975 fn test_action_name_mapping() {
1976 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
1977 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
1978 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
1979 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
1980 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
1981 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
1982 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
1983 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
1984 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
1985 assert_eq!(
1986 map_atspi_action_name("collapse"),
1987 Some("collapse".to_string())
1988 );
1989 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
1990 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
1991 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
1992 assert_eq!(
1993 map_atspi_action_name("showmenu"),
1994 Some("show_menu".to_string())
1995 );
1996 assert_eq!(
1997 map_atspi_action_name("popup"),
1998 Some("show_menu".to_string())
1999 );
2000 assert_eq!(
2001 map_atspi_action_name("show menu"),
2002 Some("show_menu".to_string())
2003 );
2004 assert_eq!(
2005 map_atspi_action_name("increment"),
2006 Some("increment".to_string())
2007 );
2008 assert_eq!(
2009 map_atspi_action_name("decrement"),
2010 Some("decrement".to_string())
2011 );
2012 assert_eq!(map_atspi_action_name("foobar"), None);
2013 }
2014
2015 #[test]
2018 fn test_action_name_aliases_roundtrip() {
2019 let atspi_names = [
2020 "click",
2021 "activate",
2022 "press",
2023 "invoke",
2024 "toggle",
2025 "check",
2026 "uncheck",
2027 "expand",
2028 "open",
2029 "collapse",
2030 "close",
2031 "select",
2032 "menu",
2033 "showmenu",
2034 "popup",
2035 "show menu",
2036 "increment",
2037 "decrement",
2038 ];
2039 for name in atspi_names {
2040 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2041 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2042 });
2043 let back = map_atspi_action_name(&canonical)
2045 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2046 assert_eq!(
2047 canonical, back,
2048 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2049 name, canonical, back, canonical
2050 );
2051 }
2052 }
2053
2054 #[test]
2056 fn test_action_name_case_insensitive() {
2057 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2058 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2059 assert_eq!(
2060 map_atspi_action_name("Increment"),
2061 Some("increment".to_string())
2062 );
2063 }
2064}