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 get_name(&self, aref: &AccessibleRef) -> Result<String> {
166 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
167 proxy
168 .get_property::<String>("Name")
169 .map_err(|e| Error::Platform {
170 code: -1,
171 message: format!("Get Name property failed: {}", e),
172 })
173 }
174
175 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
177 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
178 proxy
179 .get_property::<String>("Description")
180 .map_err(|e| Error::Platform {
181 code: -1,
182 message: format!("Get Description property failed: {}", e),
183 })
184 }
185
186 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
190 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
191 let reply = proxy
192 .call_method("GetChildren", &())
193 .map_err(|e| Error::Platform {
194 code: -1,
195 message: format!("GetChildren failed: {}", e),
196 })?;
197 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
198 reply.body().deserialize().map_err(|e| Error::Platform {
199 code: -1,
200 message: format!("GetChildren deserialize failed: {}", e),
201 })?;
202 Ok(children
203 .into_iter()
204 .map(|(bus_name, path)| AccessibleRef {
205 bus_name,
206 path: path.to_string(),
207 })
208 .collect())
209 }
210
211 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
213 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
214 let reply = proxy
215 .call_method("GetState", &())
216 .map_err(|e| Error::Platform {
217 code: -1,
218 message: format!("GetState failed: {}", e),
219 })?;
220 reply
221 .body()
222 .deserialize::<Vec<u32>>()
223 .map_err(|e| Error::Platform {
224 code: -1,
225 message: format!("GetState deserialize failed: {}", e),
226 })
227 }
228
229 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
235 let state_bits = self.get_state(aref).unwrap_or_default();
236 let bits: u64 = if state_bits.len() >= 2 {
237 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
238 } else if state_bits.len() == 1 {
239 state_bits[0] as u64
240 } else {
241 0
242 };
243 const MULTI_LINE: u64 = 1 << 17;
245 (bits & MULTI_LINE) != 0
246 }
247
248 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
252 if !self.has_interface(aref, "Component") {
253 return None;
254 }
255 let proxy = self
256 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
257 .ok()?;
258 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
261 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
262 if w <= 0 && h <= 0 {
263 return None;
264 }
265 Some(Rect {
266 x,
267 y,
268 width: w.max(0) as u32,
269 height: h.max(0) as u32,
270 })
271 }
272
273 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
279 let mut actions = Vec::new();
280 let mut indices = HashMap::new();
281
282 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
284 let n_actions = proxy
286 .get_property::<i32>("NActions")
287 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
288 .unwrap_or(0);
289 for i in 0..n_actions {
290 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
291 if let Ok(name) = reply.body().deserialize::<String>() {
292 if let Some(action_name) = map_atspi_action_name(&name) {
293 if !actions.contains(&action_name) {
294 indices.insert(action_name.clone(), i);
295 actions.push(action_name);
296 }
297 }
298 }
299 }
300 }
301 }
302
303 if !actions.contains(&"focus".to_string()) {
305 if let Ok(proxy) =
306 self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
307 {
308 if proxy.call_method("GetExtents", &(0u32,)).is_ok() {
310 actions.push("focus".to_string());
311 }
312 }
313 }
314
315 (actions, indices)
316 }
317
318 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
321 let text_value = self.get_text_content(aref);
325 if text_value.is_some() {
326 return text_value;
327 }
328 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
330 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
331 return Some(val.to_string());
332 }
333 }
334 None
335 }
336
337 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
339 let proxy = self
340 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
341 .ok()?;
342 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
343 if char_count > 0 {
344 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
345 let text: String = reply.body().deserialize().ok()?;
346 if !text.is_empty() {
347 return Some(text);
348 }
349 }
350 None
351 }
352
353 fn cache_element(&self, aref: AccessibleRef) -> u64 {
355 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
356 self.handle_cache.lock().unwrap().insert(handle, aref);
357 handle
358 }
359
360 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
362 self.handle_cache
363 .lock()
364 .unwrap()
365 .get(&handle)
366 .cloned()
367 .ok_or(Error::ElementStale {
368 selector: format!("handle:{}", handle),
369 })
370 }
371
372 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
377 let role_name = self.get_role_name(aref).unwrap_or_default();
378 let role_num = self.get_role_number(aref).unwrap_or(0);
379 let role = {
380 let by_name = if !role_name.is_empty() {
381 map_atspi_role(&role_name)
382 } else {
383 Role::Unknown
384 };
385 let coarse = if by_name != Role::Unknown {
386 by_name
387 } else {
388 map_atspi_role_number(role_num)
389 };
390 if coarse == Role::TextArea && !self.is_multi_line(aref) {
391 Role::TextField
392 } else {
393 coarse
394 }
395 };
396
397 let (
401 ((mut name, value), description),
402 (
403 (states, bounds),
404 ((actions, action_index_map), (numeric_value, min_value, max_value)),
405 ),
406 ) = rayon::join(
407 || {
408 rayon::join(
409 || {
410 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
411 let value = if role_has_value(role) {
412 self.get_value(aref)
413 } else {
414 None
415 };
416 (name, value)
417 },
418 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
419 )
420 },
421 || {
422 rayon::join(
423 || {
424 rayon::join(
425 || self.parse_states(aref, role),
426 || {
427 if role != Role::Application {
428 self.get_extents(aref)
429 } else {
430 None
431 }
432 },
433 )
434 },
435 || {
436 rayon::join(
437 || {
438 if role_has_actions(role) {
439 self.get_actions(aref)
440 } else {
441 (vec![], HashMap::new())
442 }
443 },
444 || {
445 if matches!(
446 role,
447 Role::Slider
448 | Role::ProgressBar
449 | Role::ScrollBar
450 | Role::SpinButton
451 ) {
452 if let Ok(proxy) = self.make_proxy(
453 &aref.bus_name,
454 &aref.path,
455 "org.a11y.atspi.Value",
456 ) {
457 (
458 proxy.get_property::<f64>("CurrentValue").ok(),
459 proxy.get_property::<f64>("MinimumValue").ok(),
460 proxy.get_property::<f64>("MaximumValue").ok(),
461 )
462 } else {
463 (None, None, None)
464 }
465 } else {
466 (None, None, None)
467 }
468 },
469 )
470 },
471 )
472 },
473 );
474
475 if name.is_none() && role == Role::StaticText {
478 if let Some(ref v) = value {
479 name = Some(v.clone());
480 }
481 }
482
483 let raw = {
484 let raw_role = if role_name.is_empty() {
485 format!("role_num:{}", role_num)
486 } else {
487 role_name
488 };
489 {
490 let mut raw = HashMap::new();
491 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
492 raw.insert(
493 "bus_name".into(),
494 serde_json::Value::String(aref.bus_name.clone()),
495 );
496 raw.insert(
497 "object_path".into(),
498 serde_json::Value::String(aref.path.clone()),
499 );
500 raw
501 }
502 };
503
504 let handle = self.cache_element(aref.clone());
505 if !action_index_map.is_empty() {
506 self.action_indices
507 .lock()
508 .unwrap()
509 .insert(handle, action_index_map);
510 }
511
512 let mut data = ElementData {
513 role,
514 name,
515 value,
516 description,
517 bounds,
518 actions,
519 states,
520 numeric_value,
521 min_value,
522 max_value,
523 pid,
524 stable_id: Some(aref.path.clone()),
525 attributes: HashMap::new(),
526 raw,
527 handle,
528 };
529 data.populate_attributes();
530 data
531 }
532
533 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
535 let proxy = self.make_proxy(
537 &aref.bus_name,
538 &aref.path,
539 "org.freedesktop.DBus.Properties",
540 )?;
541 let reply = proxy
542 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
543 .map_err(|e| Error::Platform {
544 code: -1,
545 message: format!("Get Parent property failed: {}", e),
546 })?;
547 let variant: zbus::zvariant::OwnedValue =
549 reply.body().deserialize().map_err(|e| Error::Platform {
550 code: -1,
551 message: format!("Parent deserialize variant failed: {}", e),
552 })?;
553 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
554 zbus::zvariant::Value::from(variant).try_into().map_err(
555 |e: zbus::zvariant::Error| Error::Platform {
556 code: -1,
557 message: format!("Parent deserialize struct failed: {}", e),
558 },
559 )?;
560 let path_str = path.as_str();
561 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
562 return Ok(None);
563 }
564 if path_str == "/org/a11y/atspi/accessible/root" {
566 return Ok(None);
567 }
568 Ok(Some(AccessibleRef {
569 bus_name: bus,
570 path: path_str.to_string(),
571 }))
572 }
573
574 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
576 let state_bits = self.get_state(aref).unwrap_or_default();
577
578 let bits: u64 = if state_bits.len() >= 2 {
580 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
581 } else if state_bits.len() == 1 {
582 state_bits[0] as u64
583 } else {
584 0
585 };
586
587 const BUSY: u64 = 1 << 3;
589 const CHECKED: u64 = 1 << 4;
590 const EDITABLE: u64 = 1 << 7;
591 const ENABLED: u64 = 1 << 8;
592 const EXPANDABLE: u64 = 1 << 9;
593 const EXPANDED: u64 = 1 << 10;
594 const FOCUSABLE: u64 = 1 << 11;
595 const FOCUSED: u64 = 1 << 12;
596 const MODAL: u64 = 1 << 16;
597 const SELECTED: u64 = 1 << 23;
598 const SENSITIVE: u64 = 1 << 24;
599 const SHOWING: u64 = 1 << 25;
600 const VISIBLE: u64 = 1 << 30;
601 const INDETERMINATE: u64 = 1 << 32;
602 const REQUIRED: u64 = 1 << 33;
603
604 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
605 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
606
607 let checked = match role {
608 Role::CheckBox | Role::RadioButton | Role::MenuItem => {
609 if (bits & INDETERMINATE) != 0 {
610 Some(Toggled::Mixed)
611 } else if (bits & CHECKED) != 0 {
612 Some(Toggled::On)
613 } else {
614 Some(Toggled::Off)
615 }
616 }
617 _ => None,
618 };
619
620 let expanded = if (bits & EXPANDABLE) != 0 {
621 Some((bits & EXPANDED) != 0)
622 } else {
623 None
624 };
625
626 StateSet {
627 enabled,
628 visible,
629 focused: (bits & FOCUSED) != 0,
630 checked,
631 selected: (bits & SELECTED) != 0,
632 expanded,
633 editable: (bits & EDITABLE) != 0,
634 focusable: (bits & FOCUSABLE) != 0,
635 modal: (bits & MODAL) != 0,
636 required: (bits & REQUIRED) != 0,
637 busy: (bits & BUSY) != 0,
638 }
639 }
640
641 fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
643 let registry = AccessibleRef {
644 bus_name: "org.a11y.atspi.Registry".to_string(),
645 path: "/org/a11y/atspi/accessible/root".to_string(),
646 };
647 let children = self.get_atspi_children(®istry)?;
648
649 for child in &children {
650 if child.path == "/org/a11y/atspi/null" {
651 continue;
652 }
653 if let Ok(proxy) =
655 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
656 {
657 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
658 if app_pid as u32 == pid {
659 return Ok(child.clone());
660 }
661 }
662 }
663 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
665 if app_pid == pid {
666 return Ok(child.clone());
667 }
668 }
669 }
670
671 Err(Error::Platform {
672 code: -1,
673 message: format!("No application found with PID {}", pid),
674 })
675 }
676
677 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
679 let proxy = self
680 .make_proxy(
681 "org.freedesktop.DBus",
682 "/org/freedesktop/DBus",
683 "org.freedesktop.DBus",
684 )
685 .ok()?;
686 let reply = proxy
687 .call_method("GetConnectionUnixProcessID", &(bus_name,))
688 .ok()?;
689 let pid: u32 = reply.body().deserialize().ok()?;
690 if pid > 0 {
691 Some(pid)
692 } else {
693 None
694 }
695 }
696
697 fn do_atspi_action_by_name(&self, aref: &AccessibleRef, action_name: &str) -> Result<()> {
700 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
701 let n_actions = proxy
702 .get_property::<i32>("NActions")
703 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
704 .unwrap_or(0);
705 for i in 0..n_actions {
706 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
707 if let Ok(name) = reply.body().deserialize::<String>() {
708 if name.eq_ignore_ascii_case(action_name) {
709 proxy
710 .call_method("DoAction", &(i,))
711 .map_err(|e| Error::Platform {
712 code: -1,
713 message: format!("DoAction failed: {}", e),
714 })?;
715 return Ok(());
716 }
717 }
718 }
719 }
720 Err(Error::Platform {
721 code: -1,
722 message: format!("Action '{}' not found", action_name),
723 })
724 }
725
726 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
728 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
729 proxy
730 .call_method("DoAction", &(index,))
731 .map_err(|e| Error::Platform {
732 code: -1,
733 message: format!("DoAction({}) failed: {}", index, e),
734 })?;
735 Ok(())
736 }
737
738 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
740 self.action_indices
741 .lock()
742 .unwrap()
743 .get(&handle)
744 .and_then(|map| map.get(action).copied())
745 .ok_or_else(|| Error::ActionNotSupported {
746 action: action.to_string(),
747 role: Role::Unknown, })
749 }
750
751 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
753 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
755 {
756 if let Ok(pid) = proxy.get_property::<i32>("Id") {
757 if pid > 0 {
758 return Some(pid as u32);
759 }
760 }
761 }
762
763 if let Ok(proxy) = self.make_proxy(
765 "org.freedesktop.DBus",
766 "/org/freedesktop/DBus",
767 "org.freedesktop.DBus",
768 ) {
769 if let Ok(reply) =
770 proxy.call_method("GetConnectionUnixProcessID", &(aref.bus_name.as_str(),))
771 {
772 if let Ok(pid) = reply.body().deserialize::<u32>() {
773 if pid > 0 {
774 return Some(pid);
775 }
776 }
777 }
778 }
779
780 None
781 }
782
783 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
785 let role_name = self.get_role_name(aref).unwrap_or_default();
786 let by_name = if !role_name.is_empty() {
787 map_atspi_role(&role_name)
788 } else {
789 Role::Unknown
790 };
791 let coarse = if by_name != Role::Unknown {
792 by_name
793 } else {
794 let role_num = self.get_role_number(aref).unwrap_or(0);
796 map_atspi_role_number(role_num)
797 };
798 if coarse == Role::TextArea && !self.is_multi_line(aref) {
800 Role::TextField
801 } else {
802 coarse
803 }
804 }
805
806 fn matches_ref(
809 &self,
810 aref: &AccessibleRef,
811 simple: &xa11y_core::selector::SimpleSelector,
812 ) -> bool {
813 let needs_role = simple.role.is_some() || simple.filters.iter().any(|f| f.attr == "role");
815 let role = if needs_role {
816 Some(self.resolve_role(aref))
817 } else {
818 None
819 };
820
821 if let Some(ref role_match) = simple.role {
822 match role_match {
823 xa11y_core::selector::RoleMatch::Normalized(expected) => {
824 if role != Some(*expected) {
825 return false;
826 }
827 }
828 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
829 let raw_role = self.get_role_name(aref).unwrap_or_default();
831 if raw_role != *platform_role {
832 return false;
833 }
834 }
835 }
836 }
837
838 for filter in &simple.filters {
839 let attr_value: Option<String> = match filter.attr.as_str() {
840 "role" => role.map(|r| r.to_snake_case().to_string()),
841 "name" => {
842 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
843 if name.is_none() && role == Some(Role::StaticText) {
845 self.get_value(aref)
846 } else {
847 name
848 }
849 }
850 "value" => self.get_value(aref),
851 "description" => self.get_description(aref).ok().filter(|s| !s.is_empty()),
852 _ => None,
854 };
855
856 let matches = match &filter.op {
857 MatchOp::Exact => attr_value.as_deref() == Some(filter.value.as_str()),
858 MatchOp::Contains => {
859 let fl = filter.value.to_lowercase();
860 attr_value
861 .as_deref()
862 .is_some_and(|v| v.to_lowercase().contains(&fl))
863 }
864 MatchOp::StartsWith => {
865 let fl = filter.value.to_lowercase();
866 attr_value
867 .as_deref()
868 .is_some_and(|v| v.to_lowercase().starts_with(&fl))
869 }
870 MatchOp::EndsWith => {
871 let fl = filter.value.to_lowercase();
872 attr_value
873 .as_deref()
874 .is_some_and(|v| v.to_lowercase().ends_with(&fl))
875 }
876 };
877
878 if !matches {
879 return false;
880 }
881 }
882
883 true
884 }
885
886 fn collect_matching_refs(
892 &self,
893 parent: &AccessibleRef,
894 simple: &xa11y_core::selector::SimpleSelector,
895 depth: u32,
896 max_depth: u32,
897 limit: Option<usize>,
898 ) -> Result<Vec<AccessibleRef>> {
899 if depth > max_depth {
900 return Ok(vec![]);
901 }
902
903 let children = self.get_atspi_children(parent)?;
904
905 let mut to_search: Vec<AccessibleRef> = Vec::new();
907 for child in children {
908 if child.path == "/org/a11y/atspi/null"
909 || child.bus_name.is_empty()
910 || child.path.is_empty()
911 {
912 continue;
913 }
914
915 let child_role = self.get_role_name(&child).unwrap_or_default();
916 if child_role == "application" {
917 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
918 for gc in grandchildren {
919 if gc.path == "/org/a11y/atspi/null"
920 || gc.bus_name.is_empty()
921 || gc.path.is_empty()
922 {
923 continue;
924 }
925 let gc_role = self.get_role_name(&gc).unwrap_or_default();
926 if gc_role == "application" {
927 continue;
928 }
929 to_search.push(gc);
930 }
931 continue;
932 }
933 to_search.push(child);
934 }
935
936 let per_child: Vec<Vec<AccessibleRef>> = to_search
938 .par_iter()
939 .map(|child| {
940 let mut child_results = Vec::new();
941 if self.matches_ref(child, simple) {
942 child_results.push(child.clone());
943 }
944 if let Ok(sub) =
945 self.collect_matching_refs(child, simple, depth + 1, max_depth, limit)
946 {
947 child_results.extend(sub);
948 }
949 child_results
950 })
951 .collect();
952
953 let mut results = Vec::new();
955 for batch in per_child {
956 for r in batch {
957 results.push(r);
958 if let Some(limit) = limit {
959 if results.len() >= limit {
960 return Ok(results);
961 }
962 }
963 }
964 }
965 Ok(results)
966 }
967}
968
969impl Provider for LinuxProvider {
970 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
971 match element {
972 None => {
973 let registry = AccessibleRef {
975 bus_name: "org.a11y.atspi.Registry".to_string(),
976 path: "/org/a11y/atspi/accessible/root".to_string(),
977 };
978 let children = self.get_atspi_children(®istry)?;
979
980 let valid: Vec<(&AccessibleRef, String)> = children
982 .iter()
983 .filter(|c| c.path != "/org/a11y/atspi/null")
984 .filter_map(|c| {
985 let name = self.get_name(c).unwrap_or_default();
986 if name.is_empty() {
987 None
988 } else {
989 Some((c, name))
990 }
991 })
992 .collect();
993
994 let results: Vec<ElementData> = valid
995 .par_iter()
996 .map(|(child, app_name)| {
997 let pid = self.get_app_pid(child);
998 let mut data = self.build_element_data(child, pid);
999 data.name = Some(app_name.clone());
1000 data
1001 })
1002 .collect();
1003
1004 Ok(results)
1005 }
1006 Some(element_data) => {
1007 let aref = self.get_cached(element_data.handle)?;
1008 let children = self.get_atspi_children(&aref).unwrap_or_default();
1009 let pid = element_data.pid;
1010
1011 let mut to_build: Vec<AccessibleRef> = Vec::new();
1014 for child_ref in &children {
1015 if child_ref.path == "/org/a11y/atspi/null"
1016 || child_ref.bus_name.is_empty()
1017 || child_ref.path.is_empty()
1018 {
1019 continue;
1020 }
1021 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1022 if child_role == "application" {
1023 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1024 for gc_ref in grandchildren {
1025 if gc_ref.path == "/org/a11y/atspi/null"
1026 || gc_ref.bus_name.is_empty()
1027 || gc_ref.path.is_empty()
1028 {
1029 continue;
1030 }
1031 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1032 if gc_role == "application" {
1033 continue;
1034 }
1035 to_build.push(gc_ref);
1036 }
1037 continue;
1038 }
1039 to_build.push(child_ref.clone());
1040 }
1041
1042 let results: Vec<ElementData> = to_build
1043 .par_iter()
1044 .map(|r| self.build_element_data(r, pid))
1045 .collect();
1046
1047 Ok(results)
1048 }
1049 }
1050 }
1051
1052 fn find_elements(
1053 &self,
1054 root: Option<&ElementData>,
1055 selector: &Selector,
1056 limit: Option<usize>,
1057 max_depth: Option<u32>,
1058 ) -> Result<Vec<ElementData>> {
1059 if selector.segments.is_empty() {
1060 return Ok(vec![]);
1061 }
1062
1063 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1064
1065 let first = &selector.segments[0].simple;
1068
1069 let phase1_limit = if selector.segments.len() == 1 {
1070 limit
1071 } else {
1072 None
1073 };
1074 let phase1_limit = match (phase1_limit, first.nth) {
1075 (Some(l), Some(n)) => Some(l.max(n)),
1076 (_, Some(n)) => Some(n),
1077 (l, None) => l,
1078 };
1079
1080 let phase1_depth = if root.is_none()
1082 && matches!(
1083 first.role,
1084 Some(xa11y_core::selector::RoleMatch::Normalized(
1085 Role::Application
1086 ))
1087 ) {
1088 0
1089 } else {
1090 max_depth_val
1091 };
1092
1093 let start_ref = match root {
1094 None => AccessibleRef {
1095 bus_name: "org.a11y.atspi.Registry".to_string(),
1096 path: "/org/a11y/atspi/accessible/root".to_string(),
1097 },
1098 Some(el) => self.get_cached(el.handle)?,
1099 };
1100
1101 let mut matching_refs =
1102 self.collect_matching_refs(&start_ref, first, 0, phase1_depth, phase1_limit)?;
1103
1104 let pid_from_root = root.and_then(|r| r.pid);
1105
1106 if selector.segments.len() == 1 {
1108 if let Some(nth) = first.nth {
1109 if nth <= matching_refs.len() {
1110 let aref = &matching_refs[nth - 1];
1111 let pid = if root.is_none() {
1112 self.get_app_pid(aref)
1113 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1114 } else {
1115 pid_from_root
1116 };
1117 return Ok(vec![self.build_element_data(aref, pid)]);
1118 } else {
1119 return Ok(vec![]);
1120 }
1121 }
1122
1123 if let Some(limit) = limit {
1124 matching_refs.truncate(limit);
1125 }
1126
1127 let is_root_search = root.is_none();
1128 return Ok(matching_refs
1129 .par_iter()
1130 .map(|aref| {
1131 let pid = if is_root_search {
1132 self.get_app_pid(aref)
1133 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1134 } else {
1135 pid_from_root
1136 };
1137 self.build_element_data(aref, pid)
1138 })
1139 .collect());
1140 }
1141
1142 let is_root_search = root.is_none();
1145 let mut candidates: Vec<ElementData> = matching_refs
1146 .par_iter()
1147 .map(|aref| {
1148 let pid = if is_root_search {
1149 self.get_app_pid(aref)
1150 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1151 } else {
1152 pid_from_root
1153 };
1154 self.build_element_data(aref, pid)
1155 })
1156 .collect();
1157
1158 for segment in &selector.segments[1..] {
1159 let mut next_candidates = Vec::new();
1160 for candidate in &candidates {
1161 match segment.combinator {
1162 Combinator::Child => {
1163 let children = self.get_children(Some(candidate))?;
1164 for child in children {
1165 if xa11y_core::selector::matches_simple(&child, &segment.simple) {
1166 next_candidates.push(child);
1167 }
1168 }
1169 }
1170 Combinator::Descendant => {
1171 let sub_selector = Selector {
1172 segments: vec![SelectorSegment {
1173 combinator: Combinator::Root,
1174 simple: segment.simple.clone(),
1175 }],
1176 };
1177 let mut sub_results = xa11y_core::selector::find_elements_in_tree(
1178 |el| self.get_children(el),
1179 Some(candidate),
1180 &sub_selector,
1181 None,
1182 Some(max_depth_val),
1183 )?;
1184 next_candidates.append(&mut sub_results);
1185 }
1186 Combinator::Root => unreachable!(),
1187 }
1188 }
1189 let mut seen = HashSet::new();
1190 next_candidates.retain(|e| seen.insert(e.handle));
1191 candidates = next_candidates;
1192 }
1193
1194 if let Some(nth) = selector.segments.last().and_then(|s| s.simple.nth) {
1196 if nth <= candidates.len() {
1197 candidates = vec![candidates.remove(nth - 1)];
1198 } else {
1199 candidates.clear();
1200 }
1201 }
1202
1203 if let Some(limit) = limit {
1204 candidates.truncate(limit);
1205 }
1206
1207 Ok(candidates)
1208 }
1209
1210 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1211 let aref = self.get_cached(element.handle)?;
1212 match self.get_atspi_parent(&aref)? {
1213 Some(parent_ref) => {
1214 let data = self.build_element_data(&parent_ref, element.pid);
1215 Ok(Some(data))
1216 }
1217 None => Ok(None),
1218 }
1219 }
1220
1221 fn press(&self, element: &ElementData) -> Result<()> {
1222 let target = self.get_cached(element.handle)?;
1223 let index = self
1224 .get_action_index(element.handle, "press")
1225 .map_err(|_| Error::ActionNotSupported {
1226 action: "press".to_string(),
1227 role: element.role,
1228 })?;
1229 self.do_atspi_action_by_index(&target, index)
1230 }
1231
1232 fn focus(&self, element: &ElementData) -> Result<()> {
1233 let target = self.get_cached(element.handle)?;
1234 if let Ok(proxy) =
1236 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1237 {
1238 if proxy.call_method("GrabFocus", &()).is_ok() {
1239 return Ok(());
1240 }
1241 }
1242 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1243 return self.do_atspi_action_by_index(&target, index);
1244 }
1245 Err(Error::ActionNotSupported {
1246 action: "focus".to_string(),
1247 role: element.role,
1248 })
1249 }
1250
1251 fn blur(&self, element: &ElementData) -> Result<()> {
1252 let target = self.get_cached(element.handle)?;
1253 if let Ok(Some(parent_ref)) = self.get_atspi_parent(&target) {
1255 if parent_ref.path != "/org/a11y/atspi/null" {
1256 if let Ok(p) = self.make_proxy(
1257 &parent_ref.bus_name,
1258 &parent_ref.path,
1259 "org.a11y.atspi.Component",
1260 ) {
1261 let _ = p.call_method("GrabFocus", &());
1262 return Ok(());
1263 }
1264 }
1265 }
1266 Ok(())
1267 }
1268
1269 fn toggle(&self, element: &ElementData) -> Result<()> {
1270 let target = self.get_cached(element.handle)?;
1271 let index = self
1272 .get_action_index(element.handle, "toggle")
1273 .map_err(|_| Error::ActionNotSupported {
1274 action: "toggle".to_string(),
1275 role: element.role,
1276 })?;
1277 self.do_atspi_action_by_index(&target, index)
1278 }
1279
1280 fn select(&self, element: &ElementData) -> Result<()> {
1281 let target = self.get_cached(element.handle)?;
1282 let index = self
1283 .get_action_index(element.handle, "select")
1284 .map_err(|_| Error::ActionNotSupported {
1285 action: "select".to_string(),
1286 role: element.role,
1287 })?;
1288 self.do_atspi_action_by_index(&target, index)
1289 }
1290
1291 fn expand(&self, element: &ElementData) -> Result<()> {
1292 let target = self.get_cached(element.handle)?;
1293 let index = self
1294 .get_action_index(element.handle, "expand")
1295 .map_err(|_| Error::ActionNotSupported {
1296 action: "expand".to_string(),
1297 role: element.role,
1298 })?;
1299 self.do_atspi_action_by_index(&target, index)
1300 }
1301
1302 fn collapse(&self, element: &ElementData) -> Result<()> {
1303 let target = self.get_cached(element.handle)?;
1304 let index = self
1305 .get_action_index(element.handle, "collapse")
1306 .map_err(|_| Error::ActionNotSupported {
1307 action: "collapse".to_string(),
1308 role: element.role,
1309 })?;
1310 self.do_atspi_action_by_index(&target, index)
1311 }
1312
1313 fn show_menu(&self, element: &ElementData) -> Result<()> {
1314 let target = self.get_cached(element.handle)?;
1315 let index = self
1316 .get_action_index(element.handle, "show_menu")
1317 .map_err(|_| Error::ActionNotSupported {
1318 action: "show_menu".to_string(),
1319 role: element.role,
1320 })?;
1321 self.do_atspi_action_by_index(&target, index)
1322 }
1323
1324 fn increment(&self, element: &ElementData) -> Result<()> {
1325 let target = self.get_cached(element.handle)?;
1326 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1328 return self.do_atspi_action_by_index(&target, index);
1329 }
1330 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1331 let current: f64 = proxy
1332 .get_property("CurrentValue")
1333 .map_err(|e| Error::Platform {
1334 code: -1,
1335 message: format!("Value.CurrentValue failed: {}", e),
1336 })?;
1337 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1338 let step = if step <= 0.0 { 1.0 } else { step };
1339 proxy
1340 .set_property("CurrentValue", current + step)
1341 .map_err(|e| Error::Platform {
1342 code: -1,
1343 message: format!("Value.SetCurrentValue failed: {}", e),
1344 })
1345 }
1346
1347 fn decrement(&self, element: &ElementData) -> Result<()> {
1348 let target = self.get_cached(element.handle)?;
1349 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1350 return self.do_atspi_action_by_index(&target, index);
1351 }
1352 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1353 let current: f64 = proxy
1354 .get_property("CurrentValue")
1355 .map_err(|e| Error::Platform {
1356 code: -1,
1357 message: format!("Value.CurrentValue failed: {}", e),
1358 })?;
1359 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1360 let step = if step <= 0.0 { 1.0 } else { step };
1361 proxy
1362 .set_property("CurrentValue", current - step)
1363 .map_err(|e| Error::Platform {
1364 code: -1,
1365 message: format!("Value.SetCurrentValue failed: {}", e),
1366 })
1367 }
1368
1369 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1370 let target = self.get_cached(element.handle)?;
1371 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1372 proxy
1373 .call_method("ScrollTo", &(0u32,))
1374 .map_err(|e| Error::Platform {
1375 code: -1,
1376 message: format!("ScrollTo failed: {}", e),
1377 })?;
1378 Ok(())
1379 }
1380
1381 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1382 let target = self.get_cached(element.handle)?;
1383 let proxy = self
1384 .make_proxy(
1385 &target.bus_name,
1386 &target.path,
1387 "org.a11y.atspi.EditableText",
1388 )
1389 .map_err(|_| Error::TextValueNotSupported)?;
1390 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1392 return Ok(());
1393 }
1394 let _ = proxy.call_method("DeleteText", &(0i32, i32::MAX));
1396 proxy
1397 .call_method("InsertText", &(0i32, value, value.len() as i32))
1398 .map_err(|_| Error::TextValueNotSupported)?;
1399 Ok(())
1400 }
1401
1402 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1403 let target = self.get_cached(element.handle)?;
1404 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1405 proxy
1406 .set_property("CurrentValue", value)
1407 .map_err(|e| Error::Platform {
1408 code: -1,
1409 message: format!("SetValue failed: {}", e),
1410 })
1411 }
1412
1413 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1414 let target = self.get_cached(element.handle)?;
1415 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1418 let insert_pos = text_proxy
1419 .as_ref()
1420 .ok()
1421 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1422 .unwrap_or(-1); let proxy = self
1425 .make_proxy(
1426 &target.bus_name,
1427 &target.path,
1428 "org.a11y.atspi.EditableText",
1429 )
1430 .map_err(|_| Error::TextValueNotSupported)?;
1431 let pos = if insert_pos >= 0 {
1432 insert_pos
1433 } else {
1434 i32::MAX
1435 };
1436 proxy
1437 .call_method("InsertText", &(pos, text, text.len() as i32))
1438 .map_err(|e| Error::Platform {
1439 code: -1,
1440 message: format!("EditableText.InsertText failed: {}", e),
1441 })?;
1442 Ok(())
1443 }
1444
1445 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1446 let target = self.get_cached(element.handle)?;
1447 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1448 if proxy
1450 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1451 .is_err()
1452 {
1453 proxy
1454 .call_method("AddSelection", &(start as i32, end as i32))
1455 .map_err(|e| Error::Platform {
1456 code: -1,
1457 message: format!("Text.AddSelection failed: {}", e),
1458 })?;
1459 }
1460 Ok(())
1461 }
1462
1463 fn scroll_down(&self, element: &ElementData, amount: f64) -> Result<()> {
1464 let target = self.get_cached(element.handle)?;
1465 let count = (amount.abs() as u32).max(1);
1466 for _ in 0..count {
1467 if self
1468 .do_atspi_action_by_name(&target, "scroll down")
1469 .is_err()
1470 {
1471 let proxy =
1473 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1474 proxy
1476 .call_method("ScrollTo", &(3u32,))
1477 .map_err(|e| Error::Platform {
1478 code: -1,
1479 message: format!("ScrollTo failed: {}", e),
1480 })?;
1481 return Ok(());
1482 }
1483 }
1484 Ok(())
1485 }
1486
1487 fn scroll_up(&self, element: &ElementData, amount: f64) -> Result<()> {
1488 let target = self.get_cached(element.handle)?;
1489 let count = (amount.abs() as u32).max(1);
1490 for _ in 0..count {
1491 if self.do_atspi_action_by_name(&target, "scroll up").is_err() {
1492 let proxy =
1494 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1495 proxy
1497 .call_method("ScrollTo", &(2u32,))
1498 .map_err(|e| Error::Platform {
1499 code: -1,
1500 message: format!("ScrollTo failed: {}", e),
1501 })?;
1502 return Ok(());
1503 }
1504 }
1505 Ok(())
1506 }
1507
1508 fn scroll_right(&self, element: &ElementData, amount: f64) -> Result<()> {
1509 let target = self.get_cached(element.handle)?;
1510 let count = (amount.abs() as u32).max(1);
1511 for _ in 0..count {
1512 if self
1513 .do_atspi_action_by_name(&target, "scroll right")
1514 .is_err()
1515 {
1516 let proxy =
1518 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1519 proxy
1521 .call_method("ScrollTo", &(5u32,))
1522 .map_err(|e| Error::Platform {
1523 code: -1,
1524 message: format!("ScrollTo failed: {}", e),
1525 })?;
1526 return Ok(());
1527 }
1528 }
1529 Ok(())
1530 }
1531
1532 fn scroll_left(&self, element: &ElementData, amount: f64) -> Result<()> {
1533 let target = self.get_cached(element.handle)?;
1534 let count = (amount.abs() as u32).max(1);
1535 for _ in 0..count {
1536 if self
1537 .do_atspi_action_by_name(&target, "scroll left")
1538 .is_err()
1539 {
1540 let proxy =
1542 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1543 proxy
1545 .call_method("ScrollTo", &(4u32,))
1546 .map_err(|e| Error::Platform {
1547 code: -1,
1548 message: format!("ScrollTo failed: {}", e),
1549 })?;
1550 return Ok(());
1551 }
1552 }
1553 Ok(())
1554 }
1555
1556 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1557 match action {
1558 "press" => self.press(element),
1559 "focus" => self.focus(element),
1560 "blur" => self.blur(element),
1561 "toggle" => self.toggle(element),
1562 "select" => self.select(element),
1563 "expand" => self.expand(element),
1564 "collapse" => self.collapse(element),
1565 "show_menu" => self.show_menu(element),
1566 "increment" => self.increment(element),
1567 "decrement" => self.decrement(element),
1568 "scroll_into_view" => self.scroll_into_view(element),
1569 _ => Err(Error::ActionNotSupported {
1570 action: action.to_string(),
1571 role: element.role,
1572 }),
1573 }
1574 }
1575
1576 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1577 let pid = element.pid.ok_or(Error::Platform {
1578 code: -1,
1579 message: "Element has no PID for subscribe".to_string(),
1580 })?;
1581 let app_name = element.name.clone().unwrap_or_default();
1582 self.subscribe_impl(app_name, pid, pid)
1583 }
1584}
1585
1586impl LinuxProvider {
1589 fn subscribe_impl(&self, app_name: String, app_pid: u32, pid: u32) -> Result<Subscription> {
1591 let (tx, rx) = std::sync::mpsc::channel();
1592 let poll_provider = LinuxProvider::new()?;
1593 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1594 let stop_clone = stop.clone();
1595
1596 let handle = std::thread::spawn(move || {
1597 let mut prev_focused: Option<String> = None;
1598 let mut prev_element_count: usize = 0;
1599
1600 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1601 std::thread::sleep(Duration::from_millis(100));
1602
1603 let app_ref = match poll_provider.find_app_by_pid(pid) {
1605 Ok(r) => r,
1606 Err(_) => continue,
1607 };
1608 let app_data = poll_provider.build_element_data(&app_ref, Some(pid));
1609
1610 let mut stack = vec![app_data];
1612 let mut element_count: usize = 0;
1613 let mut focused_element: Option<ElementData> = None;
1614 let mut visited = HashSet::new();
1615
1616 while let Some(el) = stack.pop() {
1617 let path_key = format!("{:?}:{}", el.raw, el.handle);
1618 if !visited.insert(path_key) {
1619 continue;
1620 }
1621 element_count += 1;
1622 if el.states.focused && focused_element.is_none() {
1623 focused_element = Some(el.clone());
1624 }
1625 if let Ok(children) = poll_provider.get_children(Some(&el)) {
1626 stack.extend(children);
1627 }
1628 }
1629
1630 let focused_name = focused_element.as_ref().and_then(|e| e.name.clone());
1631 if focused_name != prev_focused {
1632 if prev_focused.is_some() {
1633 let _ = tx.send(Event {
1634 event_type: EventType::FocusChanged,
1635 app_name: app_name.clone(),
1636 app_pid,
1637 target: focused_element,
1638 state_flag: None,
1639 state_value: None,
1640 text_change: None,
1641 timestamp: std::time::Instant::now(),
1642 });
1643 }
1644 prev_focused = focused_name;
1645 }
1646
1647 if element_count != prev_element_count && prev_element_count > 0 {
1648 let _ = tx.send(Event {
1649 event_type: EventType::StructureChanged,
1650 app_name: app_name.clone(),
1651 app_pid,
1652 target: None,
1653 state_flag: None,
1654 state_value: None,
1655 text_change: None,
1656 timestamp: std::time::Instant::now(),
1657 });
1658 }
1659 prev_element_count = element_count;
1660 }
1661 });
1662
1663 let cancel = CancelHandle::new(move || {
1664 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1665 let _ = handle.join();
1666 });
1667
1668 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1669 }
1670}
1671
1672fn role_has_value(role: Role) -> bool {
1675 !matches!(
1676 role,
1677 Role::Application
1678 | Role::Window
1679 | Role::Dialog
1680 | Role::Group
1681 | Role::MenuBar
1682 | Role::Toolbar
1683 | Role::TabGroup
1684 | Role::SplitGroup
1685 | Role::Table
1686 | Role::TableRow
1687 | Role::Separator
1688 )
1689}
1690
1691fn role_has_actions(role: Role) -> bool {
1694 matches!(
1695 role,
1696 Role::Button
1697 | Role::CheckBox
1698 | Role::RadioButton
1699 | Role::MenuItem
1700 | Role::Link
1701 | Role::ComboBox
1702 | Role::TextField
1703 | Role::TextArea
1704 | Role::SpinButton
1705 | Role::Tab
1706 | Role::TreeItem
1707 | Role::ListItem
1708 | Role::ScrollBar
1709 | Role::Slider
1710 | Role::Menu
1711 | Role::Image
1712 | Role::Unknown
1713 )
1714}
1715
1716fn map_atspi_role(role_name: &str) -> Role {
1718 match role_name.to_lowercase().as_str() {
1719 "application" => Role::Application,
1720 "window" | "frame" => Role::Window,
1721 "dialog" | "file chooser" => Role::Dialog,
1722 "alert" | "notification" => Role::Alert,
1723 "push button" | "push button menu" => Role::Button,
1724 "toggle button" => Role::Switch,
1725 "check box" | "check menu item" => Role::CheckBox,
1726 "radio button" | "radio menu item" => Role::RadioButton,
1727 "entry" | "password text" => Role::TextField,
1728 "spin button" | "spinbutton" => Role::SpinButton,
1729 "text" | "textbox" => Role::TextArea,
1733 "label" | "static" | "caption" => Role::StaticText,
1734 "combo box" | "combobox" => Role::ComboBox,
1735 "list" | "list box" | "listbox" => Role::List,
1737 "list item" => Role::ListItem,
1738 "menu" => Role::Menu,
1739 "menu item" | "tearoff menu item" => Role::MenuItem,
1740 "menu bar" => Role::MenuBar,
1741 "page tab" => Role::Tab,
1742 "page tab list" => Role::TabGroup,
1743 "table" | "tree table" => Role::Table,
1744 "table row" => Role::TableRow,
1745 "table cell" | "table column header" | "table row header" => Role::TableCell,
1746 "tool bar" => Role::Toolbar,
1747 "scroll bar" => Role::ScrollBar,
1748 "slider" => Role::Slider,
1749 "image" | "icon" | "desktop icon" => Role::Image,
1750 "link" => Role::Link,
1751 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1752 "progress bar" => Role::ProgressBar,
1753 "tree item" => Role::TreeItem,
1754 "document web" | "document frame" => Role::WebArea,
1755 "heading" => Role::Heading,
1756 "separator" => Role::Separator,
1757 "split pane" => Role::SplitGroup,
1758 "tooltip" | "tool tip" => Role::Tooltip,
1759 "status bar" | "statusbar" => Role::Status,
1760 "landmark" | "navigation" => Role::Navigation,
1761 _ => xa11y_core::unknown_role(role_name),
1762 }
1763}
1764
1765fn map_atspi_role_number(role: u32) -> Role {
1768 match role {
1769 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}")),
1831 }
1832}
1833
1834fn map_atspi_action_name(action_name: &str) -> Option<String> {
1848 let lower = action_name.to_lowercase();
1849 let canonical = match lower.as_str() {
1850 "click" | "activate" | "press" | "invoke" => "press",
1851 "toggle" | "check" | "uncheck" => "toggle",
1852 "expand" | "open" => "expand",
1853 "collapse" | "close" => "collapse",
1854 "select" => "select",
1855 "menu" | "showmenu" | "show_menu" | "popup" | "show menu" => "show_menu",
1856 "increment" => "increment",
1857 "decrement" => "decrement",
1858 _ => return None,
1859 };
1860 Some(canonical.to_string())
1861}
1862
1863#[cfg(test)]
1864mod tests {
1865 use super::*;
1866
1867 #[test]
1868 fn test_role_mapping() {
1869 assert_eq!(map_atspi_role("push button"), Role::Button);
1870 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
1871 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1872 assert_eq!(map_atspi_role("entry"), Role::TextField);
1873 assert_eq!(map_atspi_role("label"), Role::StaticText);
1874 assert_eq!(map_atspi_role("window"), Role::Window);
1875 assert_eq!(map_atspi_role("frame"), Role::Window);
1876 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1877 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1878 assert_eq!(map_atspi_role("slider"), Role::Slider);
1879 assert_eq!(map_atspi_role("panel"), Role::Group);
1880 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1881 }
1882
1883 #[test]
1884 fn test_numeric_role_mapping() {
1885 assert_eq!(map_atspi_role_number(62), Role::Switch);
1888 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
1891 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
1893
1894 #[test]
1895 fn test_action_name_mapping() {
1896 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
1897 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
1898 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
1899 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
1900 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
1901 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
1902 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
1903 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
1904 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
1905 assert_eq!(
1906 map_atspi_action_name("collapse"),
1907 Some("collapse".to_string())
1908 );
1909 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
1910 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
1911 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
1912 assert_eq!(
1913 map_atspi_action_name("showmenu"),
1914 Some("show_menu".to_string())
1915 );
1916 assert_eq!(
1917 map_atspi_action_name("popup"),
1918 Some("show_menu".to_string())
1919 );
1920 assert_eq!(
1921 map_atspi_action_name("show menu"),
1922 Some("show_menu".to_string())
1923 );
1924 assert_eq!(
1925 map_atspi_action_name("increment"),
1926 Some("increment".to_string())
1927 );
1928 assert_eq!(
1929 map_atspi_action_name("decrement"),
1930 Some("decrement".to_string())
1931 );
1932 assert_eq!(map_atspi_action_name("foobar"), None);
1933 }
1934
1935 #[test]
1938 fn test_action_name_aliases_roundtrip() {
1939 let atspi_names = [
1940 "click",
1941 "activate",
1942 "press",
1943 "invoke",
1944 "toggle",
1945 "check",
1946 "uncheck",
1947 "expand",
1948 "open",
1949 "collapse",
1950 "close",
1951 "select",
1952 "menu",
1953 "showmenu",
1954 "popup",
1955 "show menu",
1956 "increment",
1957 "decrement",
1958 ];
1959 for name in atspi_names {
1960 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
1961 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
1962 });
1963 let back = map_atspi_action_name(&canonical)
1965 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
1966 assert_eq!(
1967 canonical, back,
1968 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
1969 name, canonical, back, canonical
1970 );
1971 }
1972 }
1973
1974 #[test]
1976 fn test_action_name_case_insensitive() {
1977 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
1978 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
1979 assert_eq!(
1980 map_atspi_action_name("Increment"),
1981 Some("increment".to_string())
1982 );
1983 }
1984}