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