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::{AttrName, Combinator, MatchOp, SelectorSegment};
10use xa11y_core::{
11 Action, ActionData, CancelHandle, ElementData, Error, Event, EventReceiver, EventType,
12 Provider, Rect, Result, 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}
25
26#[derive(Debug, Clone)]
28struct AccessibleRef {
29 bus_name: String,
30 path: String,
31}
32
33impl LinuxProvider {
34 pub fn new() -> Result<Self> {
39 let a11y_bus = Self::connect_a11y_bus()?;
40 Ok(Self {
41 a11y_bus,
42 handle_cache: Mutex::new(HashMap::new()),
43 })
44 }
45
46 fn connect_a11y_bus() -> Result<Connection> {
47 if let Ok(session) = Connection::session() {
51 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
52 .map_err(|e| Error::Platform {
53 code: -1,
54 message: format!("Failed to create a11y bus proxy: {}", e),
55 })?;
56
57 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
58 if let Ok(address) = addr_reply.body().deserialize::<String>() {
59 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
60 if let Ok(Ok(conn)) =
61 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
62 {
63 return Ok(conn);
64 }
65 }
66 }
67 }
68
69 return Ok(session);
71 }
72
73 Connection::session().map_err(|e| Error::Platform {
74 code: -1,
75 message: format!("Failed to connect to D-Bus session bus: {}", e),
76 })
77 }
78
79 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
80 zbus::blocking::proxy::Builder::<Proxy>::new(&self.a11y_bus)
83 .destination(bus_name.to_owned())
84 .map_err(|e| Error::Platform {
85 code: -1,
86 message: format!("Failed to set proxy destination: {}", e),
87 })?
88 .path(path.to_owned())
89 .map_err(|e| Error::Platform {
90 code: -1,
91 message: format!("Failed to set proxy path: {}", e),
92 })?
93 .interface(interface.to_owned())
94 .map_err(|e| Error::Platform {
95 code: -1,
96 message: format!("Failed to set proxy interface: {}", e),
97 })?
98 .cache_properties(zbus::proxy::CacheProperties::No)
99 .build()
100 .map_err(|e| Error::Platform {
101 code: -1,
102 message: format!("Failed to create proxy: {}", e),
103 })
104 }
105
106 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
109 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
110 Ok(p) => p,
111 Err(_) => return false,
112 };
113 let reply = match proxy.call_method("GetInterfaces", &()) {
114 Ok(r) => r,
115 Err(_) => return false,
116 };
117 let interfaces: Vec<String> = match reply.body().deserialize() {
118 Ok(v) => v,
119 Err(_) => return false,
120 };
121 interfaces.iter().any(|i| i.contains(iface))
122 }
123
124 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
126 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
127 let reply = proxy
128 .call_method("GetRole", &())
129 .map_err(|e| Error::Platform {
130 code: -1,
131 message: format!("GetRole failed: {}", e),
132 })?;
133 reply
134 .body()
135 .deserialize::<u32>()
136 .map_err(|e| Error::Platform {
137 code: -1,
138 message: format!("GetRole deserialize failed: {}", e),
139 })
140 }
141
142 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
144 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
145 let reply = proxy
146 .call_method("GetRoleName", &())
147 .map_err(|e| Error::Platform {
148 code: -1,
149 message: format!("GetRoleName failed: {}", e),
150 })?;
151 reply
152 .body()
153 .deserialize::<String>()
154 .map_err(|e| Error::Platform {
155 code: -1,
156 message: format!("GetRoleName deserialize failed: {}", e),
157 })
158 }
159
160 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
162 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
163 proxy
164 .get_property::<String>("Name")
165 .map_err(|e| Error::Platform {
166 code: -1,
167 message: format!("Get Name property failed: {}", e),
168 })
169 }
170
171 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
173 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
174 proxy
175 .get_property::<String>("Description")
176 .map_err(|e| Error::Platform {
177 code: -1,
178 message: format!("Get Description property failed: {}", e),
179 })
180 }
181
182 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
186 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
187 let reply = proxy
188 .call_method("GetChildren", &())
189 .map_err(|e| Error::Platform {
190 code: -1,
191 message: format!("GetChildren failed: {}", e),
192 })?;
193 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
194 reply.body().deserialize().map_err(|e| Error::Platform {
195 code: -1,
196 message: format!("GetChildren deserialize failed: {}", e),
197 })?;
198 Ok(children
199 .into_iter()
200 .map(|(bus_name, path)| AccessibleRef {
201 bus_name,
202 path: path.to_string(),
203 })
204 .collect())
205 }
206
207 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
209 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
210 let reply = proxy
211 .call_method("GetState", &())
212 .map_err(|e| Error::Platform {
213 code: -1,
214 message: format!("GetState failed: {}", e),
215 })?;
216 reply
217 .body()
218 .deserialize::<Vec<u32>>()
219 .map_err(|e| Error::Platform {
220 code: -1,
221 message: format!("GetState deserialize failed: {}", e),
222 })
223 }
224
225 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
231 let state_bits = self.get_state(aref).unwrap_or_default();
232 let bits: u64 = if state_bits.len() >= 2 {
233 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
234 } else if state_bits.len() == 1 {
235 state_bits[0] as u64
236 } else {
237 0
238 };
239 const MULTI_LINE: u64 = 1 << 17;
241 (bits & MULTI_LINE) != 0
242 }
243
244 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
248 if !self.has_interface(aref, "Component") {
249 return None;
250 }
251 let proxy = self
252 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
253 .ok()?;
254 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
257 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
258 if w <= 0 && h <= 0 {
259 return None;
260 }
261 Some(Rect {
262 x,
263 y,
264 width: w.max(0) as u32,
265 height: h.max(0) as u32,
266 })
267 }
268
269 fn get_actions(&self, aref: &AccessibleRef) -> Vec<Action> {
273 let mut actions = Vec::new();
274
275 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
277 let n_actions = proxy
279 .get_property::<i32>("NActions")
280 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
281 .unwrap_or(0);
282 for i in 0..n_actions {
283 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
284 if let Ok(name) = reply.body().deserialize::<String>() {
285 if let Some(action) = map_atspi_action(&name) {
286 if !actions.contains(&action) {
287 actions.push(action);
288 }
289 }
290 }
291 }
292 }
293 }
294
295 if !actions.contains(&Action::Focus) {
297 if let Ok(proxy) =
298 self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
299 {
300 if proxy.call_method("GetExtents", &(0u32,)).is_ok() {
302 actions.push(Action::Focus);
303 }
304 }
305 }
306
307 actions
308 }
309
310 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
313 let text_value = self.get_text_content(aref);
317 if text_value.is_some() {
318 return text_value;
319 }
320 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
322 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
323 return Some(val.to_string());
324 }
325 }
326 None
327 }
328
329 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
331 let proxy = self
332 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
333 .ok()?;
334 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
335 if char_count > 0 {
336 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
337 let text: String = reply.body().deserialize().ok()?;
338 if !text.is_empty() {
339 return Some(text);
340 }
341 }
342 None
343 }
344
345 fn cache_element(&self, aref: AccessibleRef) -> u64 {
347 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
348 self.handle_cache.lock().unwrap().insert(handle, aref);
349 handle
350 }
351
352 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
354 self.handle_cache
355 .lock()
356 .unwrap()
357 .get(&handle)
358 .cloned()
359 .ok_or(Error::ElementStale {
360 selector: format!("handle:{}", handle),
361 })
362 }
363
364 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
369 let role_name = self.get_role_name(aref).unwrap_or_default();
370 let role_num = self.get_role_number(aref).unwrap_or(0);
371 let role = {
372 let by_name = if !role_name.is_empty() {
373 map_atspi_role(&role_name)
374 } else {
375 Role::Unknown
376 };
377 let coarse = if by_name != Role::Unknown {
378 by_name
379 } else {
380 map_atspi_role_number(role_num)
381 };
382 if coarse == Role::TextArea && !self.is_multi_line(aref) {
383 Role::TextField
384 } else {
385 coarse
386 }
387 };
388
389 let (
393 ((mut name, value), description),
394 ((states, bounds), (actions, (numeric_value, min_value, max_value))),
395 ) = rayon::join(
396 || {
397 rayon::join(
398 || {
399 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
400 let value = if role_has_value(role) {
401 self.get_value(aref)
402 } else {
403 None
404 };
405 (name, value)
406 },
407 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
408 )
409 },
410 || {
411 rayon::join(
412 || {
413 rayon::join(
414 || self.parse_states(aref, role),
415 || {
416 if role != Role::Application {
417 self.get_extents(aref)
418 } else {
419 None
420 }
421 },
422 )
423 },
424 || {
425 rayon::join(
426 || {
427 if role_has_actions(role) {
428 self.get_actions(aref)
429 } else {
430 vec![]
431 }
432 },
433 || {
434 if matches!(
435 role,
436 Role::Slider
437 | Role::ProgressBar
438 | Role::ScrollBar
439 | Role::SpinButton
440 ) {
441 if let Ok(proxy) = self.make_proxy(
442 &aref.bus_name,
443 &aref.path,
444 "org.a11y.atspi.Value",
445 ) {
446 (
447 proxy.get_property::<f64>("CurrentValue").ok(),
448 proxy.get_property::<f64>("MinimumValue").ok(),
449 proxy.get_property::<f64>("MaximumValue").ok(),
450 )
451 } else {
452 (None, None, None)
453 }
454 } else {
455 (None, None, None)
456 }
457 },
458 )
459 },
460 )
461 },
462 );
463
464 if name.is_none() && role == Role::StaticText {
467 if let Some(ref v) = value {
468 name = Some(v.clone());
469 }
470 }
471
472 let raw = {
473 let raw_role = if role_name.is_empty() {
474 format!("role_num:{}", role_num)
475 } else {
476 role_name
477 };
478 xa11y_core::RawPlatformData::Linux {
479 atspi_role: raw_role,
480 bus_name: aref.bus_name.clone(),
481 object_path: aref.path.clone(),
482 }
483 };
484
485 let handle = self.cache_element(aref.clone());
486
487 ElementData {
488 role,
489 name,
490 value,
491 description,
492 bounds,
493 actions,
494 states,
495 numeric_value,
496 min_value,
497 max_value,
498 pid,
499 stable_id: Some(aref.path.clone()),
500 raw,
501 handle,
502 }
503 }
504
505 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
507 let proxy = self.make_proxy(
509 &aref.bus_name,
510 &aref.path,
511 "org.freedesktop.DBus.Properties",
512 )?;
513 let reply = proxy
514 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
515 .map_err(|e| Error::Platform {
516 code: -1,
517 message: format!("Get Parent property failed: {}", e),
518 })?;
519 let variant: zbus::zvariant::OwnedValue =
521 reply.body().deserialize().map_err(|e| Error::Platform {
522 code: -1,
523 message: format!("Parent deserialize variant failed: {}", e),
524 })?;
525 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
526 zbus::zvariant::Value::from(variant).try_into().map_err(
527 |e: zbus::zvariant::Error| Error::Platform {
528 code: -1,
529 message: format!("Parent deserialize struct failed: {}", e),
530 },
531 )?;
532 let path_str = path.as_str();
533 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
534 return Ok(None);
535 }
536 if path_str == "/org/a11y/atspi/accessible/root" {
538 return Ok(None);
539 }
540 Ok(Some(AccessibleRef {
541 bus_name: bus,
542 path: path_str.to_string(),
543 }))
544 }
545
546 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
548 let state_bits = self.get_state(aref).unwrap_or_default();
549
550 let bits: u64 = if state_bits.len() >= 2 {
552 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
553 } else if state_bits.len() == 1 {
554 state_bits[0] as u64
555 } else {
556 0
557 };
558
559 const BUSY: u64 = 1 << 3;
561 const CHECKED: u64 = 1 << 4;
562 const EDITABLE: u64 = 1 << 7;
563 const ENABLED: u64 = 1 << 8;
564 const EXPANDABLE: u64 = 1 << 9;
565 const EXPANDED: u64 = 1 << 10;
566 const FOCUSABLE: u64 = 1 << 11;
567 const FOCUSED: u64 = 1 << 12;
568 const MODAL: u64 = 1 << 16;
569 const SELECTED: u64 = 1 << 23;
570 const SENSITIVE: u64 = 1 << 24;
571 const SHOWING: u64 = 1 << 25;
572 const VISIBLE: u64 = 1 << 30;
573 const INDETERMINATE: u64 = 1 << 32;
574 const REQUIRED: u64 = 1 << 33;
575
576 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
577 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
578
579 let checked = match role {
580 Role::CheckBox | Role::RadioButton | Role::MenuItem => {
581 if (bits & INDETERMINATE) != 0 {
582 Some(Toggled::Mixed)
583 } else if (bits & CHECKED) != 0 {
584 Some(Toggled::On)
585 } else {
586 Some(Toggled::Off)
587 }
588 }
589 _ => None,
590 };
591
592 let expanded = if (bits & EXPANDABLE) != 0 {
593 Some((bits & EXPANDED) != 0)
594 } else {
595 None
596 };
597
598 StateSet {
599 enabled,
600 visible,
601 focused: (bits & FOCUSED) != 0,
602 checked,
603 selected: (bits & SELECTED) != 0,
604 expanded,
605 editable: (bits & EDITABLE) != 0,
606 focusable: (bits & FOCUSABLE) != 0,
607 modal: (bits & MODAL) != 0,
608 required: (bits & REQUIRED) != 0,
609 busy: (bits & BUSY) != 0,
610 }
611 }
612
613 fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
615 let registry = AccessibleRef {
616 bus_name: "org.a11y.atspi.Registry".to_string(),
617 path: "/org/a11y/atspi/accessible/root".to_string(),
618 };
619 let children = self.get_atspi_children(®istry)?;
620
621 for child in &children {
622 if child.path == "/org/a11y/atspi/null" {
623 continue;
624 }
625 if let Ok(proxy) =
627 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
628 {
629 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
630 if app_pid as u32 == pid {
631 return Ok(child.clone());
632 }
633 }
634 }
635 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
637 if app_pid == pid {
638 return Ok(child.clone());
639 }
640 }
641 }
642
643 Err(Error::Platform {
644 code: -1,
645 message: format!("No application found with PID {}", pid),
646 })
647 }
648
649 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
651 let proxy = self
652 .make_proxy(
653 "org.freedesktop.DBus",
654 "/org/freedesktop/DBus",
655 "org.freedesktop.DBus",
656 )
657 .ok()?;
658 let reply = proxy
659 .call_method("GetConnectionUnixProcessID", &(bus_name,))
660 .ok()?;
661 let pid: u32 = reply.body().deserialize().ok()?;
662 if pid > 0 {
663 Some(pid)
664 } else {
665 None
666 }
667 }
668
669 fn do_atspi_action(&self, aref: &AccessibleRef, action_name: &str) -> Result<()> {
671 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
672 let n_actions = proxy
674 .get_property::<i32>("NActions")
675 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
676 .unwrap_or(0);
677
678 for i in 0..n_actions {
679 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
680 if let Ok(name) = reply.body().deserialize::<String>() {
681 if name.eq_ignore_ascii_case(action_name) {
684 let _ =
685 proxy
686 .call_method("DoAction", &(i,))
687 .map_err(|e| Error::Platform {
688 code: -1,
689 message: format!("DoAction failed: {}", e),
690 })?;
691 return Ok(());
692 }
693 }
694 }
695 }
696
697 Err(Error::Platform {
698 code: -1,
699 message: format!("Action '{}' not found", action_name),
700 })
701 }
702
703 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
705 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
707 {
708 if let Ok(pid) = proxy.get_property::<i32>("Id") {
709 if pid > 0 {
710 return Some(pid as u32);
711 }
712 }
713 }
714
715 if let Ok(proxy) = self.make_proxy(
717 "org.freedesktop.DBus",
718 "/org/freedesktop/DBus",
719 "org.freedesktop.DBus",
720 ) {
721 if let Ok(reply) =
722 proxy.call_method("GetConnectionUnixProcessID", &(aref.bus_name.as_str(),))
723 {
724 if let Ok(pid) = reply.body().deserialize::<u32>() {
725 if pid > 0 {
726 return Some(pid);
727 }
728 }
729 }
730 }
731
732 None
733 }
734
735 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
737 let role_name = self.get_role_name(aref).unwrap_or_default();
738 let by_name = if !role_name.is_empty() {
739 map_atspi_role(&role_name)
740 } else {
741 Role::Unknown
742 };
743 let coarse = if by_name != Role::Unknown {
744 by_name
745 } else {
746 let role_num = self.get_role_number(aref).unwrap_or(0);
748 map_atspi_role_number(role_num)
749 };
750 if coarse == Role::TextArea && !self.is_multi_line(aref) {
752 Role::TextField
753 } else {
754 coarse
755 }
756 }
757
758 fn matches_ref(
761 &self,
762 aref: &AccessibleRef,
763 simple: &xa11y_core::selector::SimpleSelector,
764 ) -> bool {
765 let needs_role =
767 simple.role.is_some() || simple.filters.iter().any(|f| f.attr == AttrName::Role);
768 let role = if needs_role {
769 Some(self.resolve_role(aref))
770 } else {
771 None
772 };
773
774 if let Some(expected) = simple.role {
775 if role != Some(expected) {
776 return false;
777 }
778 }
779
780 for filter in &simple.filters {
781 let attr_value: Option<String> = match filter.attr {
782 AttrName::Role => role.map(|r| r.to_snake_case().to_string()),
783 AttrName::Name => {
784 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
785 if name.is_none() && role == Some(Role::StaticText) {
787 self.get_value(aref)
788 } else {
789 name
790 }
791 }
792 AttrName::Value => self.get_value(aref),
793 AttrName::Description => self.get_description(aref).ok().filter(|s| !s.is_empty()),
794 };
795
796 let matches = match &filter.op {
797 MatchOp::Exact => attr_value.as_deref() == Some(filter.value.as_str()),
798 MatchOp::Contains => {
799 let fl = filter.value.to_lowercase();
800 attr_value
801 .as_deref()
802 .is_some_and(|v| v.to_lowercase().contains(&fl))
803 }
804 MatchOp::StartsWith => {
805 let fl = filter.value.to_lowercase();
806 attr_value
807 .as_deref()
808 .is_some_and(|v| v.to_lowercase().starts_with(&fl))
809 }
810 MatchOp::EndsWith => {
811 let fl = filter.value.to_lowercase();
812 attr_value
813 .as_deref()
814 .is_some_and(|v| v.to_lowercase().ends_with(&fl))
815 }
816 };
817
818 if !matches {
819 return false;
820 }
821 }
822
823 true
824 }
825
826 fn collect_matching_refs(
832 &self,
833 parent: &AccessibleRef,
834 simple: &xa11y_core::selector::SimpleSelector,
835 depth: u32,
836 max_depth: u32,
837 limit: Option<usize>,
838 ) -> Result<Vec<AccessibleRef>> {
839 if depth > max_depth {
840 return Ok(vec![]);
841 }
842
843 let children = self.get_atspi_children(parent)?;
844
845 let mut to_search: Vec<AccessibleRef> = Vec::new();
847 for child in children {
848 if child.path == "/org/a11y/atspi/null"
849 || child.bus_name.is_empty()
850 || child.path.is_empty()
851 {
852 continue;
853 }
854
855 let child_role = self.get_role_name(&child).unwrap_or_default();
856 if child_role == "application" {
857 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
858 for gc in grandchildren {
859 if gc.path == "/org/a11y/atspi/null"
860 || gc.bus_name.is_empty()
861 || gc.path.is_empty()
862 {
863 continue;
864 }
865 let gc_role = self.get_role_name(&gc).unwrap_or_default();
866 if gc_role == "application" {
867 continue;
868 }
869 to_search.push(gc);
870 }
871 continue;
872 }
873 to_search.push(child);
874 }
875
876 let per_child: Vec<Vec<AccessibleRef>> = to_search
878 .par_iter()
879 .map(|child| {
880 let mut child_results = Vec::new();
881 if self.matches_ref(child, simple) {
882 child_results.push(child.clone());
883 }
884 if let Ok(sub) =
885 self.collect_matching_refs(child, simple, depth + 1, max_depth, limit)
886 {
887 child_results.extend(sub);
888 }
889 child_results
890 })
891 .collect();
892
893 let mut results = Vec::new();
895 for batch in per_child {
896 for r in batch {
897 results.push(r);
898 if let Some(limit) = limit {
899 if results.len() >= limit {
900 return Ok(results);
901 }
902 }
903 }
904 }
905 Ok(results)
906 }
907}
908
909impl Provider for LinuxProvider {
910 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
911 match element {
912 None => {
913 let registry = AccessibleRef {
915 bus_name: "org.a11y.atspi.Registry".to_string(),
916 path: "/org/a11y/atspi/accessible/root".to_string(),
917 };
918 let children = self.get_atspi_children(®istry)?;
919
920 let valid: Vec<(&AccessibleRef, String)> = children
922 .iter()
923 .filter(|c| c.path != "/org/a11y/atspi/null")
924 .filter_map(|c| {
925 let name = self.get_name(c).unwrap_or_default();
926 if name.is_empty() {
927 None
928 } else {
929 Some((c, name))
930 }
931 })
932 .collect();
933
934 let results: Vec<ElementData> = valid
935 .par_iter()
936 .map(|(child, app_name)| {
937 let pid = self.get_app_pid(child);
938 let mut data = self.build_element_data(child, pid);
939 data.name = Some(app_name.clone());
940 data
941 })
942 .collect();
943
944 Ok(results)
945 }
946 Some(element_data) => {
947 let aref = self.get_cached(element_data.handle)?;
948 let children = self.get_atspi_children(&aref).unwrap_or_default();
949 let pid = element_data.pid;
950
951 let mut to_build: Vec<AccessibleRef> = Vec::new();
954 for child_ref in &children {
955 if child_ref.path == "/org/a11y/atspi/null"
956 || child_ref.bus_name.is_empty()
957 || child_ref.path.is_empty()
958 {
959 continue;
960 }
961 let child_role = self.get_role_name(child_ref).unwrap_or_default();
962 if child_role == "application" {
963 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
964 for gc_ref in grandchildren {
965 if gc_ref.path == "/org/a11y/atspi/null"
966 || gc_ref.bus_name.is_empty()
967 || gc_ref.path.is_empty()
968 {
969 continue;
970 }
971 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
972 if gc_role == "application" {
973 continue;
974 }
975 to_build.push(gc_ref);
976 }
977 continue;
978 }
979 to_build.push(child_ref.clone());
980 }
981
982 let results: Vec<ElementData> = to_build
983 .par_iter()
984 .map(|r| self.build_element_data(r, pid))
985 .collect();
986
987 Ok(results)
988 }
989 }
990 }
991
992 fn find_elements(
993 &self,
994 root: Option<&ElementData>,
995 selector: &Selector,
996 limit: Option<usize>,
997 max_depth: Option<u32>,
998 ) -> Result<Vec<ElementData>> {
999 if selector.segments.is_empty() {
1000 return Ok(vec![]);
1001 }
1002
1003 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1004
1005 let first = &selector.segments[0].simple;
1008
1009 let phase1_limit = if selector.segments.len() == 1 {
1010 limit
1011 } else {
1012 None
1013 };
1014 let phase1_limit = match (phase1_limit, first.nth) {
1015 (Some(l), Some(n)) => Some(l.max(n)),
1016 (_, Some(n)) => Some(n),
1017 (l, None) => l,
1018 };
1019
1020 let phase1_depth = if root.is_none() && first.role == Some(Role::Application) {
1022 0
1023 } else {
1024 max_depth_val
1025 };
1026
1027 let start_ref = match root {
1028 None => AccessibleRef {
1029 bus_name: "org.a11y.atspi.Registry".to_string(),
1030 path: "/org/a11y/atspi/accessible/root".to_string(),
1031 },
1032 Some(el) => self.get_cached(el.handle)?,
1033 };
1034
1035 let mut matching_refs =
1036 self.collect_matching_refs(&start_ref, first, 0, phase1_depth, phase1_limit)?;
1037
1038 let pid_from_root = root.and_then(|r| r.pid);
1039
1040 if selector.segments.len() == 1 {
1042 if let Some(nth) = first.nth {
1043 if nth <= matching_refs.len() {
1044 let aref = &matching_refs[nth - 1];
1045 let pid = if root.is_none() {
1046 self.get_app_pid(aref)
1047 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1048 } else {
1049 pid_from_root
1050 };
1051 return Ok(vec![self.build_element_data(aref, pid)]);
1052 } else {
1053 return Ok(vec![]);
1054 }
1055 }
1056
1057 if let Some(limit) = limit {
1058 matching_refs.truncate(limit);
1059 }
1060
1061 let is_root_search = root.is_none();
1062 return Ok(matching_refs
1063 .par_iter()
1064 .map(|aref| {
1065 let pid = if is_root_search {
1066 self.get_app_pid(aref)
1067 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1068 } else {
1069 pid_from_root
1070 };
1071 self.build_element_data(aref, pid)
1072 })
1073 .collect());
1074 }
1075
1076 let is_root_search = root.is_none();
1079 let mut candidates: Vec<ElementData> = matching_refs
1080 .par_iter()
1081 .map(|aref| {
1082 let pid = if is_root_search {
1083 self.get_app_pid(aref)
1084 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1085 } else {
1086 pid_from_root
1087 };
1088 self.build_element_data(aref, pid)
1089 })
1090 .collect();
1091
1092 for segment in &selector.segments[1..] {
1093 let mut next_candidates = Vec::new();
1094 for candidate in &candidates {
1095 match segment.combinator {
1096 Combinator::Child => {
1097 let children = self.get_children(Some(candidate))?;
1098 for child in children {
1099 if xa11y_core::selector::matches_simple(&child, &segment.simple) {
1100 next_candidates.push(child);
1101 }
1102 }
1103 }
1104 Combinator::Descendant => {
1105 let sub_selector = Selector {
1106 segments: vec![SelectorSegment {
1107 combinator: Combinator::Root,
1108 simple: segment.simple.clone(),
1109 }],
1110 };
1111 let mut sub_results = xa11y_core::selector::find_elements_in_tree(
1112 |el| self.get_children(el),
1113 Some(candidate),
1114 &sub_selector,
1115 None,
1116 Some(max_depth_val),
1117 )?;
1118 next_candidates.append(&mut sub_results);
1119 }
1120 Combinator::Root => unreachable!(),
1121 }
1122 }
1123 let mut seen = HashSet::new();
1124 next_candidates.retain(|e| seen.insert(e.handle));
1125 candidates = next_candidates;
1126 }
1127
1128 if let Some(nth) = selector.segments.last().and_then(|s| s.simple.nth) {
1130 if nth <= candidates.len() {
1131 candidates = vec![candidates.remove(nth - 1)];
1132 } else {
1133 candidates.clear();
1134 }
1135 }
1136
1137 if let Some(limit) = limit {
1138 candidates.truncate(limit);
1139 }
1140
1141 Ok(candidates)
1142 }
1143
1144 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1145 let aref = self.get_cached(element.handle)?;
1146 match self.get_atspi_parent(&aref)? {
1147 Some(parent_ref) => {
1148 let data = self.build_element_data(&parent_ref, element.pid);
1149 Ok(Some(data))
1150 }
1151 None => Ok(None),
1152 }
1153 }
1154
1155 fn perform_action(
1156 &self,
1157 element: &ElementData,
1158 action: Action,
1159 data: Option<ActionData>,
1160 ) -> Result<()> {
1161 let target = self.get_cached(element.handle)?;
1162
1163 match action {
1164 Action::Press => self
1165 .do_atspi_action(&target, "click")
1166 .or_else(|_| self.do_atspi_action(&target, "activate"))
1167 .or_else(|_| self.do_atspi_action(&target, "press"))
1168 .or_else(|_| self.do_atspi_action(&target, "toggle"))
1171 .or_else(|_| self.do_atspi_action(&target, "check")),
1172 Action::Focus => {
1173 if let Ok(proxy) =
1175 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1176 {
1177 if proxy.call_method("GrabFocus", &()).is_ok() {
1178 return Ok(());
1179 }
1180 }
1181 self.do_atspi_action(&target, "focus")
1182 .or_else(|_| self.do_atspi_action(&target, "setFocus"))
1183 }
1184 Action::SetValue => match data {
1185 Some(ActionData::NumericValue(v)) => {
1186 let proxy =
1187 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1188 proxy
1189 .set_property("CurrentValue", v)
1190 .map_err(|e| Error::Platform {
1191 code: -1,
1192 message: format!("SetValue failed: {}", e),
1193 })
1194 }
1195 Some(ActionData::Value(text)) => {
1196 let proxy = self
1197 .make_proxy(
1198 &target.bus_name,
1199 &target.path,
1200 "org.a11y.atspi.EditableText",
1201 )
1202 .map_err(|_| Error::TextValueNotSupported)?;
1203 if proxy.call_method("SetTextContents", &(&*text)).is_ok() {
1205 return Ok(());
1206 }
1207 let _ = proxy.call_method("DeleteText", &(0i32, i32::MAX));
1209 proxy
1210 .call_method("InsertText", &(0i32, &*text, text.len() as i32))
1211 .map_err(|_| Error::TextValueNotSupported)?;
1212 Ok(())
1213 }
1214 _ => Err(Error::Platform {
1215 code: -1,
1216 message: "SetValue requires ActionData".to_string(),
1217 }),
1218 },
1219 Action::Toggle => self
1220 .do_atspi_action(&target, "toggle")
1221 .or_else(|_| self.do_atspi_action(&target, "click"))
1222 .or_else(|_| self.do_atspi_action(&target, "activate"))
1223 .or_else(|_| self.do_atspi_action(&target, "check")),
1225 Action::Expand => self
1226 .do_atspi_action(&target, "expand")
1227 .or_else(|_| self.do_atspi_action(&target, "open")),
1228 Action::Collapse => self
1229 .do_atspi_action(&target, "collapse")
1230 .or_else(|_| self.do_atspi_action(&target, "close")),
1231 Action::Select => self.do_atspi_action(&target, "select"),
1232 Action::ShowMenu => self
1233 .do_atspi_action(&target, "menu")
1234 .or_else(|_| self.do_atspi_action(&target, "showmenu")),
1235 Action::ScrollIntoView => {
1236 let proxy =
1237 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1238 proxy
1239 .call_method("ScrollTo", &(0u32,))
1240 .map_err(|e| Error::Platform {
1241 code: -1,
1242 message: format!("ScrollTo failed: {}", e),
1243 })?;
1244 Ok(())
1245 }
1246 Action::Increment => self.do_atspi_action(&target, "increment").or_else(|_| {
1247 let proxy =
1249 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1250 let current: f64 =
1251 proxy
1252 .get_property("CurrentValue")
1253 .map_err(|e| Error::Platform {
1254 code: -1,
1255 message: format!("Value.CurrentValue failed: {}", e),
1256 })?;
1257 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1258 let step = if step <= 0.0 { 1.0 } else { step };
1259 proxy
1260 .set_property("CurrentValue", current + step)
1261 .map_err(|e| Error::Platform {
1262 code: -1,
1263 message: format!("Value.SetCurrentValue failed: {}", e),
1264 })
1265 }),
1266 Action::Decrement => self.do_atspi_action(&target, "decrement").or_else(|_| {
1267 let proxy =
1268 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1269 let current: f64 =
1270 proxy
1271 .get_property("CurrentValue")
1272 .map_err(|e| Error::Platform {
1273 code: -1,
1274 message: format!("Value.CurrentValue failed: {}", e),
1275 })?;
1276 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1277 let step = if step <= 0.0 { 1.0 } else { step };
1278 proxy
1279 .set_property("CurrentValue", current - step)
1280 .map_err(|e| Error::Platform {
1281 code: -1,
1282 message: format!("Value.SetCurrentValue failed: {}", e),
1283 })
1284 }),
1285 Action::Blur => {
1286 if let Ok(Some(parent_ref)) = self.get_atspi_parent(&target) {
1288 if parent_ref.path != "/org/a11y/atspi/null" {
1289 if let Ok(p) = self.make_proxy(
1290 &parent_ref.bus_name,
1291 &parent_ref.path,
1292 "org.a11y.atspi.Component",
1293 ) {
1294 let _ = p.call_method("GrabFocus", &());
1295 return Ok(());
1296 }
1297 }
1298 }
1299 Ok(())
1300 }
1301
1302 Action::ScrollDown | Action::ScrollRight => {
1303 let amount = match data {
1304 Some(ActionData::ScrollAmount(amount)) => amount,
1305 _ => {
1306 return Err(Error::Platform {
1307 code: -1,
1308 message: "Scroll requires ActionData::ScrollAmount".to_string(),
1309 })
1310 }
1311 };
1312 let is_vertical = matches!(action, Action::ScrollDown);
1313 let (pos_name, neg_name) = if is_vertical {
1314 ("scroll down", "scroll up")
1315 } else {
1316 ("scroll right", "scroll left")
1317 };
1318 let action_name = if amount >= 0.0 { pos_name } else { neg_name };
1319 let count = (amount.abs() as u32).max(1);
1321 for _ in 0..count {
1322 if self.do_atspi_action(&target, action_name).is_err() {
1323 let proxy = self.make_proxy(
1325 &target.bus_name,
1326 &target.path,
1327 "org.a11y.atspi.Component",
1328 )?;
1329 let scroll_type: u32 = if is_vertical {
1330 if amount >= 0.0 {
1331 3
1332 } else {
1333 2
1334 } } else if amount >= 0.0 {
1336 5
1337 } else {
1338 4
1339 }; proxy
1341 .call_method("ScrollTo", &(scroll_type,))
1342 .map_err(|e| Error::Platform {
1343 code: -1,
1344 message: format!("ScrollTo failed: {}", e),
1345 })?;
1346 return Ok(());
1347 }
1348 }
1349 Ok(())
1350 }
1351
1352 Action::SetTextSelection => {
1353 let (start, end) = match data {
1354 Some(ActionData::TextSelection { start, end }) => (start, end),
1355 _ => {
1356 return Err(Error::Platform {
1357 code: -1,
1358 message: "SetTextSelection requires ActionData::TextSelection"
1359 .to_string(),
1360 })
1361 }
1362 };
1363 let proxy =
1364 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1365 if proxy
1367 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1368 .is_err()
1369 {
1370 proxy
1371 .call_method("AddSelection", &(start as i32, end as i32))
1372 .map_err(|e| Error::Platform {
1373 code: -1,
1374 message: format!("Text.AddSelection failed: {}", e),
1375 })?;
1376 }
1377 Ok(())
1378 }
1379
1380 Action::TypeText => {
1381 let text = match data {
1382 Some(ActionData::Value(text)) => text,
1383 _ => {
1384 return Err(Error::Platform {
1385 code: -1,
1386 message: "TypeText requires ActionData::Value".to_string(),
1387 })
1388 }
1389 };
1390 let text_proxy =
1393 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1394 let insert_pos = text_proxy
1395 .as_ref()
1396 .ok()
1397 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1398 .unwrap_or(-1); let proxy = self
1401 .make_proxy(
1402 &target.bus_name,
1403 &target.path,
1404 "org.a11y.atspi.EditableText",
1405 )
1406 .map_err(|_| Error::TextValueNotSupported)?;
1407 let pos = if insert_pos >= 0 {
1408 insert_pos
1409 } else {
1410 i32::MAX
1411 };
1412 proxy
1413 .call_method("InsertText", &(pos, &*text, text.len() as i32))
1414 .map_err(|e| Error::Platform {
1415 code: -1,
1416 message: format!("EditableText.InsertText failed: {}", e),
1417 })?;
1418 Ok(())
1419 }
1420 }
1421 }
1422
1423 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1424 let pid = element.pid.ok_or(Error::Platform {
1425 code: -1,
1426 message: "Element has no PID for subscribe".to_string(),
1427 })?;
1428 let app_name = element.name.clone().unwrap_or_default();
1429 self.subscribe_impl(app_name, pid, pid)
1430 }
1431}
1432
1433impl LinuxProvider {
1436 fn subscribe_impl(&self, app_name: String, app_pid: u32, pid: u32) -> Result<Subscription> {
1438 let (tx, rx) = std::sync::mpsc::channel();
1439 let poll_provider = LinuxProvider::new()?;
1440 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1441 let stop_clone = stop.clone();
1442
1443 let handle = std::thread::spawn(move || {
1444 let mut prev_focused: Option<String> = None;
1445 let mut prev_element_count: usize = 0;
1446
1447 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1448 std::thread::sleep(Duration::from_millis(100));
1449
1450 let app_ref = match poll_provider.find_app_by_pid(pid) {
1452 Ok(r) => r,
1453 Err(_) => continue,
1454 };
1455 let app_data = poll_provider.build_element_data(&app_ref, Some(pid));
1456
1457 let mut stack = vec![app_data];
1459 let mut element_count: usize = 0;
1460 let mut focused_element: Option<ElementData> = None;
1461 let mut visited = HashSet::new();
1462
1463 while let Some(el) = stack.pop() {
1464 let path_key = format!("{:?}:{}", el.raw, el.handle);
1465 if !visited.insert(path_key) {
1466 continue;
1467 }
1468 element_count += 1;
1469 if el.states.focused && focused_element.is_none() {
1470 focused_element = Some(el.clone());
1471 }
1472 if let Ok(children) = poll_provider.get_children(Some(&el)) {
1473 stack.extend(children);
1474 }
1475 }
1476
1477 let focused_name = focused_element.as_ref().and_then(|e| e.name.clone());
1478 if focused_name != prev_focused {
1479 if prev_focused.is_some() {
1480 let _ = tx.send(Event {
1481 event_type: EventType::FocusChanged,
1482 app_name: app_name.clone(),
1483 app_pid,
1484 target: focused_element,
1485 state_flag: None,
1486 state_value: None,
1487 text_change: None,
1488 timestamp: std::time::Instant::now(),
1489 });
1490 }
1491 prev_focused = focused_name;
1492 }
1493
1494 if element_count != prev_element_count && prev_element_count > 0 {
1495 let _ = tx.send(Event {
1496 event_type: EventType::StructureChanged,
1497 app_name: app_name.clone(),
1498 app_pid,
1499 target: None,
1500 state_flag: None,
1501 state_value: None,
1502 text_change: None,
1503 timestamp: std::time::Instant::now(),
1504 });
1505 }
1506 prev_element_count = element_count;
1507 }
1508 });
1509
1510 let cancel = CancelHandle::new(move || {
1511 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1512 let _ = handle.join();
1513 });
1514
1515 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1516 }
1517}
1518
1519fn role_has_value(role: Role) -> bool {
1522 !matches!(
1523 role,
1524 Role::Application
1525 | Role::Window
1526 | Role::Dialog
1527 | Role::Group
1528 | Role::MenuBar
1529 | Role::Toolbar
1530 | Role::TabGroup
1531 | Role::SplitGroup
1532 | Role::Table
1533 | Role::TableRow
1534 | Role::Separator
1535 )
1536}
1537
1538fn role_has_actions(role: Role) -> bool {
1541 matches!(
1542 role,
1543 Role::Button
1544 | Role::CheckBox
1545 | Role::RadioButton
1546 | Role::MenuItem
1547 | Role::Link
1548 | Role::ComboBox
1549 | Role::TextField
1550 | Role::TextArea
1551 | Role::SpinButton
1552 | Role::Tab
1553 | Role::TreeItem
1554 | Role::ListItem
1555 | Role::ScrollBar
1556 | Role::Slider
1557 | Role::Menu
1558 | Role::Image
1559 | Role::Unknown
1560 )
1561}
1562
1563fn map_atspi_role(role_name: &str) -> Role {
1565 match role_name.to_lowercase().as_str() {
1566 "application" => Role::Application,
1567 "window" | "frame" => Role::Window,
1568 "dialog" | "file chooser" => Role::Dialog,
1569 "alert" | "notification" => Role::Alert,
1570 "push button" | "push button menu" => Role::Button,
1571 "check box" | "check menu item" => Role::CheckBox,
1572 "radio button" | "radio menu item" => Role::RadioButton,
1573 "entry" | "password text" => Role::TextField,
1574 "spin button" | "spinbutton" => Role::SpinButton,
1575 "text" | "textbox" => Role::TextArea,
1579 "label" | "static" | "caption" => Role::StaticText,
1580 "combo box" | "combobox" => Role::ComboBox,
1581 "list" | "list box" | "listbox" => Role::List,
1583 "list item" => Role::ListItem,
1584 "menu" => Role::Menu,
1585 "menu item" | "tearoff menu item" => Role::MenuItem,
1586 "menu bar" => Role::MenuBar,
1587 "page tab" => Role::Tab,
1588 "page tab list" => Role::TabGroup,
1589 "table" | "tree table" => Role::Table,
1590 "table row" => Role::TableRow,
1591 "table cell" | "table column header" | "table row header" => Role::TableCell,
1592 "tool bar" => Role::Toolbar,
1593 "scroll bar" => Role::ScrollBar,
1594 "slider" => Role::Slider,
1595 "image" | "icon" | "desktop icon" => Role::Image,
1596 "link" => Role::Link,
1597 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1598 "progress bar" => Role::ProgressBar,
1599 "tree item" => Role::TreeItem,
1600 "document web" | "document frame" => Role::WebArea,
1601 "heading" => Role::Heading,
1602 "separator" => Role::Separator,
1603 "split pane" => Role::SplitGroup,
1604 "tooltip" | "tool tip" => Role::Tooltip,
1605 "status bar" | "statusbar" => Role::Status,
1606 "landmark" | "navigation" => Role::Navigation,
1607 _ => Role::Unknown,
1608 }
1609}
1610
1611fn map_atspi_role_number(role: u32) -> Role {
1614 match role {
1615 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, 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, _ => Role::Unknown,
1677 }
1678}
1679
1680fn map_atspi_action(action_name: &str) -> Option<Action> {
1682 match action_name.to_lowercase().as_str() {
1683 "click" | "activate" | "press" | "invoke" => Some(Action::Press),
1684 "toggle" | "check" | "uncheck" => Some(Action::Toggle),
1685 "expand" | "open" => Some(Action::Expand),
1686 "collapse" | "close" => Some(Action::Collapse),
1687 "select" => Some(Action::Select),
1688 "menu" | "showmenu" | "popup" | "show menu" => Some(Action::ShowMenu),
1689 "increment" => Some(Action::Increment),
1690 "decrement" => Some(Action::Decrement),
1691 _ => None,
1692 }
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697 use super::*;
1698
1699 #[test]
1700 fn test_role_mapping() {
1701 assert_eq!(map_atspi_role("push button"), Role::Button);
1702 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1703 assert_eq!(map_atspi_role("entry"), Role::TextField);
1704 assert_eq!(map_atspi_role("label"), Role::StaticText);
1705 assert_eq!(map_atspi_role("window"), Role::Window);
1706 assert_eq!(map_atspi_role("frame"), Role::Window);
1707 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1708 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1709 assert_eq!(map_atspi_role("slider"), Role::Slider);
1710 assert_eq!(map_atspi_role("panel"), Role::Group);
1711 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1712 }
1713
1714 #[test]
1715 fn test_action_mapping() {
1716 assert_eq!(map_atspi_action("click"), Some(Action::Press));
1717 assert_eq!(map_atspi_action("activate"), Some(Action::Press));
1718 assert_eq!(map_atspi_action("toggle"), Some(Action::Toggle));
1719 assert_eq!(map_atspi_action("expand"), Some(Action::Expand));
1720 assert_eq!(map_atspi_action("collapse"), Some(Action::Collapse));
1721 assert_eq!(map_atspi_action("select"), Some(Action::Select));
1722 assert_eq!(map_atspi_action("increment"), Some(Action::Increment));
1723 assert_eq!(map_atspi_action("foobar"), None);
1724 }
1725}