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 if let Ok(proxy) =
1475 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1476 {
1477 if proxy.call_method("ScrollTo", &(3u32,)).is_ok() {
1479 return Ok(());
1480 }
1481 }
1482 return Err(Error::ActionNotSupported {
1483 action: "scroll_down".to_string(),
1484 role: element.role,
1485 });
1486 }
1487 }
1488 Ok(())
1489 }
1490
1491 fn scroll_up(&self, element: &ElementData, amount: f64) -> Result<()> {
1492 let target = self.get_cached(element.handle)?;
1493 let count = (amount.abs() as u32).max(1);
1494 for _ in 0..count {
1495 if self.do_atspi_action_by_name(&target, "scroll up").is_err() {
1496 if let Ok(proxy) =
1500 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1501 {
1502 if proxy.call_method("ScrollTo", &(2u32,)).is_ok() {
1504 return Ok(());
1505 }
1506 }
1507 return Err(Error::ActionNotSupported {
1508 action: "scroll_up".to_string(),
1509 role: element.role,
1510 });
1511 }
1512 }
1513 Ok(())
1514 }
1515
1516 fn scroll_right(&self, element: &ElementData, amount: f64) -> Result<()> {
1517 let target = self.get_cached(element.handle)?;
1518 let count = (amount.abs() as u32).max(1);
1519 for _ in 0..count {
1520 if self
1521 .do_atspi_action_by_name(&target, "scroll right")
1522 .is_err()
1523 {
1524 if let Ok(proxy) =
1528 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1529 {
1530 if proxy.call_method("ScrollTo", &(5u32,)).is_ok() {
1532 return Ok(());
1533 }
1534 }
1535 return Err(Error::ActionNotSupported {
1536 action: "scroll_right".to_string(),
1537 role: element.role,
1538 });
1539 }
1540 }
1541 Ok(())
1542 }
1543
1544 fn scroll_left(&self, element: &ElementData, amount: f64) -> Result<()> {
1545 let target = self.get_cached(element.handle)?;
1546 let count = (amount.abs() as u32).max(1);
1547 for _ in 0..count {
1548 if self
1549 .do_atspi_action_by_name(&target, "scroll left")
1550 .is_err()
1551 {
1552 if let Ok(proxy) =
1556 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1557 {
1558 if proxy.call_method("ScrollTo", &(4u32,)).is_ok() {
1560 return Ok(());
1561 }
1562 }
1563 return Err(Error::ActionNotSupported {
1564 action: "scroll_left".to_string(),
1565 role: element.role,
1566 });
1567 }
1568 }
1569 Ok(())
1570 }
1571
1572 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1573 match action {
1574 "press" => self.press(element),
1575 "focus" => self.focus(element),
1576 "blur" => self.blur(element),
1577 "toggle" => self.toggle(element),
1578 "select" => self.select(element),
1579 "expand" => self.expand(element),
1580 "collapse" => self.collapse(element),
1581 "show_menu" => self.show_menu(element),
1582 "increment" => self.increment(element),
1583 "decrement" => self.decrement(element),
1584 "scroll_into_view" => self.scroll_into_view(element),
1585 _ => Err(Error::ActionNotSupported {
1586 action: action.to_string(),
1587 role: element.role,
1588 }),
1589 }
1590 }
1591
1592 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1593 let pid = element.pid.ok_or(Error::Platform {
1594 code: -1,
1595 message: "Element has no PID for subscribe".to_string(),
1596 })?;
1597 let app_name = element.name.clone().unwrap_or_default();
1598 self.subscribe_impl(app_name, pid, pid)
1599 }
1600}
1601
1602impl LinuxProvider {
1605 fn subscribe_impl(&self, app_name: String, app_pid: u32, pid: u32) -> Result<Subscription> {
1607 let (tx, rx) = std::sync::mpsc::channel();
1608 let poll_provider = LinuxProvider::new()?;
1609 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1610 let stop_clone = stop.clone();
1611
1612 let handle = std::thread::spawn(move || {
1613 let mut prev_focused: Option<String> = None;
1614 let mut prev_element_count: usize = 0;
1615
1616 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1617 std::thread::sleep(Duration::from_millis(100));
1618
1619 let app_ref = match poll_provider.find_app_by_pid(pid) {
1621 Ok(r) => r,
1622 Err(_) => continue,
1623 };
1624 let app_data = poll_provider.build_element_data(&app_ref, Some(pid));
1625
1626 let mut stack = vec![app_data];
1628 let mut element_count: usize = 0;
1629 let mut focused_element: Option<ElementData> = None;
1630 let mut visited = HashSet::new();
1631
1632 while let Some(el) = stack.pop() {
1633 let path_key = format!("{:?}:{}", el.raw, el.handle);
1634 if !visited.insert(path_key) {
1635 continue;
1636 }
1637 element_count += 1;
1638 if el.states.focused && focused_element.is_none() {
1639 focused_element = Some(el.clone());
1640 }
1641 if let Ok(children) = poll_provider.get_children(Some(&el)) {
1642 stack.extend(children);
1643 }
1644 }
1645
1646 let focused_name = focused_element.as_ref().and_then(|e| e.name.clone());
1647 if focused_name != prev_focused {
1648 if prev_focused.is_some() {
1649 let _ = tx.send(Event {
1650 event_type: EventType::FocusChanged,
1651 app_name: app_name.clone(),
1652 app_pid,
1653 target: focused_element,
1654 state_flag: None,
1655 state_value: None,
1656 text_change: None,
1657 timestamp: std::time::Instant::now(),
1658 });
1659 }
1660 prev_focused = focused_name;
1661 }
1662
1663 if element_count != prev_element_count && prev_element_count > 0 {
1664 let _ = tx.send(Event {
1665 event_type: EventType::StructureChanged,
1666 app_name: app_name.clone(),
1667 app_pid,
1668 target: None,
1669 state_flag: None,
1670 state_value: None,
1671 text_change: None,
1672 timestamp: std::time::Instant::now(),
1673 });
1674 }
1675 prev_element_count = element_count;
1676 }
1677 });
1678
1679 let cancel = CancelHandle::new(move || {
1680 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1681 let _ = handle.join();
1682 });
1683
1684 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1685 }
1686}
1687
1688fn role_has_value(role: Role) -> bool {
1691 !matches!(
1692 role,
1693 Role::Application
1694 | Role::Window
1695 | Role::Dialog
1696 | Role::Group
1697 | Role::MenuBar
1698 | Role::Toolbar
1699 | Role::TabGroup
1700 | Role::SplitGroup
1701 | Role::Table
1702 | Role::TableRow
1703 | Role::Separator
1704 )
1705}
1706
1707fn role_has_actions(role: Role) -> bool {
1710 matches!(
1711 role,
1712 Role::Button
1713 | Role::CheckBox
1714 | Role::RadioButton
1715 | Role::MenuItem
1716 | Role::Link
1717 | Role::ComboBox
1718 | Role::TextField
1719 | Role::TextArea
1720 | Role::SpinButton
1721 | Role::Tab
1722 | Role::TreeItem
1723 | Role::ListItem
1724 | Role::ScrollBar
1725 | Role::Slider
1726 | Role::Menu
1727 | Role::Image
1728 | Role::Unknown
1729 )
1730}
1731
1732fn map_atspi_role(role_name: &str) -> Role {
1734 match role_name.to_lowercase().as_str() {
1735 "application" => Role::Application,
1736 "window" | "frame" => Role::Window,
1737 "dialog" | "file chooser" => Role::Dialog,
1738 "alert" | "notification" => Role::Alert,
1739 "push button" | "push button menu" => Role::Button,
1740 "toggle button" => Role::Switch,
1741 "check box" | "check menu item" => Role::CheckBox,
1742 "radio button" | "radio menu item" => Role::RadioButton,
1743 "entry" | "password text" => Role::TextField,
1744 "spin button" | "spinbutton" => Role::SpinButton,
1745 "text" | "textbox" => Role::TextArea,
1749 "label" | "static" | "caption" => Role::StaticText,
1750 "combo box" | "combobox" => Role::ComboBox,
1751 "list" | "list box" | "listbox" => Role::List,
1753 "list item" => Role::ListItem,
1754 "menu" => Role::Menu,
1755 "menu item" | "tearoff menu item" => Role::MenuItem,
1756 "menu bar" => Role::MenuBar,
1757 "page tab" => Role::Tab,
1758 "page tab list" => Role::TabGroup,
1759 "table" | "tree table" => Role::Table,
1760 "table row" => Role::TableRow,
1761 "table cell" | "table column header" | "table row header" => Role::TableCell,
1762 "tool bar" => Role::Toolbar,
1763 "scroll bar" => Role::ScrollBar,
1764 "slider" => Role::Slider,
1765 "image" | "icon" | "desktop icon" => Role::Image,
1766 "link" => Role::Link,
1767 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1768 "progress bar" => Role::ProgressBar,
1769 "tree item" => Role::TreeItem,
1770 "document web" | "document frame" => Role::WebArea,
1771 "heading" => Role::Heading,
1772 "separator" => Role::Separator,
1773 "split pane" => Role::SplitGroup,
1774 "tooltip" | "tool tip" => Role::Tooltip,
1775 "status bar" | "statusbar" => Role::Status,
1776 "landmark" | "navigation" => Role::Navigation,
1777 _ => xa11y_core::unknown_role(role_name),
1778 }
1779}
1780
1781fn map_atspi_role_number(role: u32) -> Role {
1784 match role {
1785 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}")),
1847 }
1848}
1849
1850fn map_atspi_action_name(action_name: &str) -> Option<String> {
1864 let lower = action_name.to_lowercase();
1865 let canonical = match lower.as_str() {
1866 "click" | "activate" | "press" | "invoke" => "press",
1867 "toggle" | "check" | "uncheck" => "toggle",
1868 "expand" | "open" => "expand",
1869 "collapse" | "close" => "collapse",
1870 "select" => "select",
1871 "menu" | "showmenu" | "show_menu" | "popup" | "show menu" => "show_menu",
1872 "increment" => "increment",
1873 "decrement" => "decrement",
1874 _ => return None,
1875 };
1876 Some(canonical.to_string())
1877}
1878
1879#[cfg(test)]
1880mod tests {
1881 use super::*;
1882
1883 #[test]
1884 fn test_role_mapping() {
1885 assert_eq!(map_atspi_role("push button"), Role::Button);
1886 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
1887 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1888 assert_eq!(map_atspi_role("entry"), Role::TextField);
1889 assert_eq!(map_atspi_role("label"), Role::StaticText);
1890 assert_eq!(map_atspi_role("window"), Role::Window);
1891 assert_eq!(map_atspi_role("frame"), Role::Window);
1892 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1893 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1894 assert_eq!(map_atspi_role("slider"), Role::Slider);
1895 assert_eq!(map_atspi_role("panel"), Role::Group);
1896 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1897 }
1898
1899 #[test]
1900 fn test_numeric_role_mapping() {
1901 assert_eq!(map_atspi_role_number(62), Role::Switch);
1904 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
1907 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
1909
1910 #[test]
1911 fn test_action_name_mapping() {
1912 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
1913 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
1914 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
1915 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
1916 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
1917 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
1918 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
1919 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
1920 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
1921 assert_eq!(
1922 map_atspi_action_name("collapse"),
1923 Some("collapse".to_string())
1924 );
1925 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
1926 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
1927 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
1928 assert_eq!(
1929 map_atspi_action_name("showmenu"),
1930 Some("show_menu".to_string())
1931 );
1932 assert_eq!(
1933 map_atspi_action_name("popup"),
1934 Some("show_menu".to_string())
1935 );
1936 assert_eq!(
1937 map_atspi_action_name("show menu"),
1938 Some("show_menu".to_string())
1939 );
1940 assert_eq!(
1941 map_atspi_action_name("increment"),
1942 Some("increment".to_string())
1943 );
1944 assert_eq!(
1945 map_atspi_action_name("decrement"),
1946 Some("decrement".to_string())
1947 );
1948 assert_eq!(map_atspi_action_name("foobar"), None);
1949 }
1950
1951 #[test]
1954 fn test_action_name_aliases_roundtrip() {
1955 let atspi_names = [
1956 "click",
1957 "activate",
1958 "press",
1959 "invoke",
1960 "toggle",
1961 "check",
1962 "uncheck",
1963 "expand",
1964 "open",
1965 "collapse",
1966 "close",
1967 "select",
1968 "menu",
1969 "showmenu",
1970 "popup",
1971 "show menu",
1972 "increment",
1973 "decrement",
1974 ];
1975 for name in atspi_names {
1976 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
1977 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
1978 });
1979 let back = map_atspi_action_name(&canonical)
1981 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
1982 assert_eq!(
1983 canonical, back,
1984 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
1985 name, canonical, back, canonical
1986 );
1987 }
1988 }
1989
1990 #[test]
1992 fn test_action_name_case_insensitive() {
1993 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
1994 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
1995 assert_eq!(
1996 map_atspi_action_name("Increment"),
1997 Some("increment".to_string())
1998 );
1999 }
2000}