1use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6
7use rayon::prelude::*;
8use xa11y_core::{
9 ElementData, ElementParts, Error, Provider, Rect, Result, Role, ShellSurfaceKind, StateParts,
10 StateSet, Subscription, Toggled,
11};
12use zbus::blocking::{Connection, Proxy};
13
14use crate::window::WindowManager;
15
16static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
18
19fn state_attr_to_string(name: &str, s: &StateSet) -> Option<String> {
22 match name {
23 "enabled" => Some(s.enabled.to_string()),
24 "visible" => Some(s.visible.to_string()),
25 "focused" => Some(s.focused.to_string()),
26 "active" => Some(s.active.to_string()),
27 "focusable" => Some(s.focusable.to_string()),
28 "selected" => Some(s.selected.to_string()),
29 "editable" => Some(s.editable.to_string()),
30 "modal" => Some(s.modal.to_string()),
31 "required" => Some(s.required.to_string()),
32 "busy" => Some(s.busy.to_string()),
33 "expanded" => s.expanded.map(|b| b.to_string()),
34 "checked" => s.checked.map(|c| {
35 match c {
36 Toggled::On => "on",
37 Toggled::Off => "off",
38 Toggled::Mixed => "mixed",
39 }
40 .to_string()
41 }),
42 _ => None,
43 }
44}
45
46pub struct LinuxProvider {
48 a11y_bus: Connection,
49 handle_cache: Mutex<HashMap<u64, AccessibleRef>>,
51 action_indices: Mutex<HashMap<u64, HashMap<String, i32>>>,
54 window_manager: WindowManager,
57}
58
59#[derive(Debug, Clone)]
61pub(crate) struct AccessibleRef {
62 pub(crate) bus_name: String,
63 pub(crate) path: String,
64}
65
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75pub(crate) struct Interfaces {
76 pub(crate) component: bool,
77 pub(crate) text: bool,
78 pub(crate) value: bool,
79}
80
81impl Interfaces {
82 pub(crate) fn from_names<S: AsRef<str>>(names: &[S]) -> Self {
83 let has = |iface: &str| names.iter().any(|n| n.as_ref() == iface);
84 Self {
85 component: has("org.a11y.atspi.Component"),
86 text: has("org.a11y.atspi.Text"),
87 value: has("org.a11y.atspi.Value"),
88 }
89 }
90}
91
92pub(crate) fn read_interfaces(accessible: &Proxy<'_>) -> Result<Interfaces> {
95 let reply = accessible
96 .call_method("GetInterfaces", &())
97 .map_err(|e| Error::Platform {
98 code: -1,
99 message: format!("GetInterfaces failed: {}", e),
100 })?;
101 let names: Vec<String> = reply.body().deserialize().map_err(|e| Error::Platform {
102 code: -1,
103 message: format!("GetInterfaces deserialize failed: {}", e),
104 })?;
105 Ok(Interfaces::from_names(&names))
106}
107
108impl LinuxProvider {
109 pub fn new() -> Result<Self> {
114 let a11y_bus = Self::connect_a11y_bus()?;
115 Ok(Self {
116 a11y_bus,
117 handle_cache: Mutex::new(HashMap::new()),
118 action_indices: Mutex::new(HashMap::new()),
119 window_manager: WindowManager::new(),
120 })
121 }
122
123 pub(crate) fn connect_a11y_bus() -> Result<Connection> {
124 if let Ok(session) = Connection::session() {
128 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
129 .map_err(|e| Error::Platform {
130 code: -1,
131 message: format!("Failed to create a11y bus proxy: {}", e),
132 })?;
133
134 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
135 if let Ok(address) = addr_reply.body().deserialize::<String>() {
136 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
137 if let Ok(Ok(conn)) =
138 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
139 {
140 return Ok(conn);
141 }
142 }
143 }
144 }
145
146 return Ok(session);
148 }
149
150 Connection::session().map_err(|e| Error::Platform {
151 code: -1,
152 message: format!("Failed to connect to D-Bus session bus: {}", e),
153 })
154 }
155
156 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
157 zbus::blocking::proxy::Builder::<Proxy>::new(&self.a11y_bus)
160 .destination(bus_name.to_owned())
161 .map_err(|e| Error::Platform {
162 code: -1,
163 message: format!("Failed to set proxy destination: {}", e),
164 })?
165 .path(path.to_owned())
166 .map_err(|e| Error::Platform {
167 code: -1,
168 message: format!("Failed to set proxy path: {}", e),
169 })?
170 .interface(interface.to_owned())
171 .map_err(|e| Error::Platform {
172 code: -1,
173 message: format!("Failed to set proxy interface: {}", e),
174 })?
175 .cache_properties(zbus::proxy::CacheProperties::No)
176 .build()
177 .map_err(|e| Error::Platform {
178 code: -1,
179 message: format!("Failed to create proxy: {}", e),
180 })
181 }
182
183 fn get_interfaces(&self, aref: &AccessibleRef) -> Result<Interfaces> {
186 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
187 read_interfaces(&proxy)
188 }
189
190 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
192 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
193 let reply = proxy
194 .call_method("GetRole", &())
195 .map_err(|e| Error::Platform {
196 code: -1,
197 message: format!("GetRole failed: {}", e),
198 })?;
199 reply
200 .body()
201 .deserialize::<u32>()
202 .map_err(|e| Error::Platform {
203 code: -1,
204 message: format!("GetRole deserialize failed: {}", e),
205 })
206 }
207
208 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
210 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
211 let reply = proxy
212 .call_method("GetRoleName", &())
213 .map_err(|e| Error::Platform {
214 code: -1,
215 message: format!("GetRoleName failed: {}", e),
216 })?;
217 reply
218 .body()
219 .deserialize::<String>()
220 .map_err(|e| Error::Platform {
221 code: -1,
222 message: format!("GetRoleName deserialize failed: {}", e),
223 })
224 }
225
226 fn check_chromium_a11y_enabled(
243 &self,
244 parent: &AccessibleRef,
245 role_hint: Option<Role>,
246 ) -> Result<()> {
247 let app_root = AccessibleRef {
248 bus_name: parent.bus_name.clone(),
249 path: "/org/a11y/atspi/accessible/root".to_string(),
250 };
251 let toolkit = match self
252 .make_proxy(
253 &app_root.bus_name,
254 &app_root.path,
255 "org.a11y.atspi.Application",
256 )
257 .ok()
258 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
259 {
260 Some(t) => t,
261 None => return Ok(()),
262 };
263 if !toolkit.eq_ignore_ascii_case("Chromium") {
264 return Ok(());
265 }
266 let role = role_hint.unwrap_or_else(|| self.resolve_role(parent));
267 if role != Role::Window {
268 return Ok(());
269 }
270 let app_name = self.get_name(&app_root).unwrap_or_default();
271 Err(Error::AccessibilityNotEnabled {
272 app: app_name,
273 instructions: "Chromium/Electron app exposes an empty accessibility tree on Linux. \
274 Relaunch with `--force-renderer-accessibility` (or set the env var \
275 `ACCESSIBILITY_ENABLED=1`) so the renderer accessibility bridge is initialised."
276 .to_string(),
277 })
278 }
279
280 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
282 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
283 proxy
284 .get_property::<String>("Name")
285 .map_err(|e| Error::Platform {
286 code: -1,
287 message: format!("Get Name property failed: {}", e),
288 })
289 }
290
291 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
293 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
294 proxy
295 .get_property::<String>("Description")
296 .map_err(|e| Error::Platform {
297 code: -1,
298 message: format!("Get Description property failed: {}", e),
299 })
300 }
301
302 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
306 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
307 let reply = proxy
308 .call_method("GetChildren", &())
309 .map_err(|e| children_call_error(e, aref))?;
310 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
311 reply.body().deserialize().map_err(|e| Error::Platform {
312 code: -1,
313 message: format!("GetChildren deserialize failed: {}", e),
314 })?;
315 Ok(children
316 .into_iter()
317 .map(|(bus_name, path)| AccessibleRef {
318 bus_name,
319 path: path.to_string(),
320 })
321 .collect())
322 }
323
324 fn live_atspi_children(&self, aref: &AccessibleRef) -> Result<Option<Vec<AccessibleRef>>> {
327 match self.get_atspi_children(aref) {
328 Ok(children) => Ok(Some(children)),
329 Err(Error::ElementStale { .. }) => Ok(None),
330 Err(err) => Err(err),
331 }
332 }
333
334 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
336 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
337 let reply = proxy
338 .call_method("GetState", &())
339 .map_err(|e| Error::Platform {
340 code: -1,
341 message: format!("GetState failed: {}", e),
342 })?;
343 reply
344 .body()
345 .deserialize::<Vec<u32>>()
346 .map_err(|e| Error::Platform {
347 code: -1,
348 message: format!("GetState deserialize failed: {}", e),
349 })
350 }
351
352 fn scan_role_number(&self, aref: &AccessibleRef) -> Result<Option<u32>> {
358 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
359 let reply = match proxy.call_method("GetRole", &()) {
360 Ok(reply) => reply,
361 Err(e) if is_peer_gone(&e) => return Ok(None),
362 Err(e) => {
363 return Err(Error::Platform {
364 code: -1,
365 message: format!("GetRole failed: {}", e),
366 })
367 }
368 };
369 reply
370 .body()
371 .deserialize::<u32>()
372 .map(Some)
373 .map_err(|e| Error::Platform {
374 code: -1,
375 message: format!("GetRole deserialize failed: {}", e),
376 })
377 }
378
379 #[allow(
386 clippy::option_option,
387 reason = "The two Nones mean different things \
388 — peer gone versus member absent — and collapsing them would erase \
389 exactly the distinction this function exists to make."
390 )]
391 fn scan_attributes(
392 &self,
393 aref: &AccessibleRef,
394 ) -> Result<Option<Option<HashMap<String, String>>>> {
395 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
396 let reply = match proxy.call_method("GetAttributes", &()) {
397 Ok(reply) => reply,
398 Err(e) if is_peer_gone(&e) => return Ok(None),
399 Err(e) if is_absent_member(&e) => return Ok(Some(None)),
400 Err(e) => {
401 return Err(Error::Platform {
402 code: -1,
403 message: format!("GetAttributes failed: {}", e),
404 })
405 }
406 };
407 reply
408 .body()
409 .deserialize::<HashMap<String, String>>()
410 .map(|a| Some(Some(a)))
411 .map_err(|e| Error::Platform {
412 code: -1,
413 message: format!("GetAttributes deserialize failed: {}", e),
414 })
415 }
416
417 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
423 let state_bits = self.get_state(aref).unwrap_or_default();
424 let bits: u64 = if state_bits.len() >= 2 {
425 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
426 } else if state_bits.len() == 1 {
427 state_bits[0] as u64
428 } else {
429 0
430 };
431 const MULTI_LINE: u64 = 1 << 17;
433 (bits & MULTI_LINE) != 0
434 }
435
436 fn refine_text_role(&self, coarse: Role, aref: &AccessibleRef) -> Role {
445 match coarse {
446 Role::TextArea | Role::TextField => {
447 if self.is_multi_line(aref) {
448 Role::TextArea
449 } else {
450 Role::TextField
451 }
452 }
453 other => other,
454 }
455 }
456
457 fn get_extents(&self, aref: &AccessibleRef, ifaces: Interfaces) -> Option<Rect> {
461 if !ifaces.component {
462 return None;
463 }
464 let proxy = self
465 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
466 .ok()?;
467 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
470 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
471 if w <= 0 && h <= 0 {
472 return None;
473 }
474 Some(
479 Rect {
480 x,
481 y,
482 width: w.max(0) as u32,
483 height: h.max(0) as u32,
484 }
485 .to_logical(crate::scale::coordinate_scale()),
486 )
487 }
488
489 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
495 let mut actions = Vec::new();
496 let mut indices = HashMap::new();
497
498 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
500 let n_actions = proxy
502 .get_property::<i32>("NActions")
503 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
504 .unwrap_or(0);
505 for i in 0..n_actions {
506 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
507 if let Ok(name) = reply.body().deserialize::<String>() {
508 if let Some(action_name) = map_atspi_action_name(&name) {
509 if !actions.contains(&action_name) {
510 indices.insert(action_name.clone(), i);
511 actions.push(action_name);
512 }
513 }
514 }
515 }
516 }
517 }
518
519 (actions, indices)
527 }
528
529 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
537 let app_root = AccessibleRef {
538 bus_name: aref.bus_name.clone(),
539 path: "/org/a11y/atspi/accessible/root".to_string(),
540 };
541 self.make_proxy(
542 &app_root.bus_name,
543 &app_root.path,
544 "org.a11y.atspi.Application",
545 )
546 .ok()
547 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
548 .map(|t| t.eq_ignore_ascii_case("GTK"))
549 .unwrap_or(false)
550 }
551
552 fn find_gtk_press_fallback(
565 &self,
566 outer: &AccessibleRef,
567 outer_name: &str,
568 ) -> Option<(AccessibleRef, i32)> {
569 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
570 queue.push_back((outer.clone(), 0));
571 let mut visited: usize = 0;
572 let mut shallowest_depth: Option<u32> = None;
573 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
574
575 while let Some((node, depth)) = queue.pop_front() {
576 if let Some(best) = shallowest_depth {
578 if depth > best {
579 continue;
580 }
581 }
582 if visited > GTK_FALLBACK_MAX_NODES {
583 break;
584 }
585
586 let role_name = if depth == 0 {
587 String::new()
588 } else {
589 visited += 1;
590 self.get_role_name(&node).unwrap_or_default().to_lowercase()
591 };
592
593 if depth > 0 && is_actionable_atspi_role(&role_name) {
594 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
595 match shallowest_depth {
596 Some(d) if depth < d => {
597 shallowest_depth = Some(depth);
598 hits.clear();
599 hits.push((node.clone(), idx));
600 }
601 Some(d) if depth == d => hits.push((node.clone(), idx)),
602 Some(_) => {}
603 None => {
604 shallowest_depth = Some(depth);
605 hits.push((node.clone(), idx));
606 }
607 }
608 }
609 }
610
611 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
616 || (depth > 0 && is_never_descend_atspi_role(&role_name));
617 if stop_descending {
618 continue;
619 }
620 if let Ok(children) = self.get_atspi_children(&node) {
621 for c in children {
622 queue.push_back((c, depth + 1));
623 }
624 }
625 }
626
627 if hits.len() == 1 {
628 Some(hits.into_iter().next().unwrap())
629 } else {
630 None
631 }
632 }
633
634 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
637 let (_, index_map) = self.get_actions(aref);
638 let idx = *index_map.get("press")?;
639 if !self.is_showing_visible_sensitive(aref) {
640 return None;
641 }
642 if !outer_name.is_empty() {
646 let inner_name = self.get_name(aref).unwrap_or_default();
647 if !inner_name.is_empty() && inner_name != outer_name {
648 return None;
649 }
650 }
651 Some(idx)
652 }
653
654 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
659 let raw = self.get_state(aref).unwrap_or_default();
660 let bits: u64 = if raw.len() >= 2 {
661 (raw[0] as u64) | ((raw[1] as u64) << 32)
662 } else if raw.len() == 1 {
663 raw[0] as u64
664 } else {
665 0
666 };
667 const SENSITIVE: u64 = 1 << 24;
668 const SHOWING: u64 = 1 << 25;
669 const VISIBLE: u64 = 1 << 30;
670 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
671 }
672
673 fn get_value(&self, aref: &AccessibleRef, ifaces: Interfaces) -> Option<String> {
677 if ifaces.text {
681 let text_value = self.get_text_content(aref);
682 if text_value.is_some() {
683 return text_value;
684 }
685 }
686 if ifaces.value {
688 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
689 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
690 return Some(val.to_string());
691 }
692 }
693 }
694 None
695 }
696
697 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
699 let proxy = self
700 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
701 .ok()?;
702 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
703 if char_count > 0 {
704 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
705 let text: String = reply.body().deserialize().ok()?;
706 if !text.is_empty() {
707 return Some(text);
708 }
709 }
710 None
711 }
712
713 fn cache_element(&self, aref: AccessibleRef) -> u64 {
715 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
716 self.handle_cache
717 .lock()
718 .unwrap_or_else(|e| e.into_inner())
719 .insert(handle, aref);
720 handle
721 }
722
723 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
725 self.handle_cache
726 .lock()
727 .unwrap_or_else(|e| e.into_inner())
728 .get(&handle)
729 .cloned()
730 .ok_or(Error::ElementStale {
731 selector: format!("handle:{}", handle),
732 })
733 }
734
735 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
740 let ((role_name, role_num), ifaces) = rayon::join(
747 || {
748 (
749 self.get_role_name(aref).unwrap_or_default(),
750 self.get_role_number(aref).unwrap_or(0),
751 )
752 },
753 || self.get_interfaces(aref).unwrap_or_default(),
754 );
755 let coarse_role = {
756 let by_name = if !role_name.is_empty() {
757 map_atspi_role(&role_name)
758 } else {
759 Role::Unknown
760 };
761 let coarse = if by_name != Role::Unknown {
762 by_name
763 } else {
764 map_atspi_role_number(role_num)
765 };
766 self.refine_text_role(coarse, aref)
767 };
768 let prefetched_menu_actions = if coarse_role == Role::Menu {
775 Some(self.get_actions(aref))
776 } else {
777 None
778 };
779 let role = refine_menu_role(
780 coarse_role,
781 prefetched_menu_actions
782 .as_ref()
783 .map(|(actions, _)| actions.as_slice())
784 .unwrap_or(&[]),
785 );
786
787 let (
791 ((mut name, value), description),
792 (
793 ((states, state_bits), bounds),
794 ((actions, action_index_map), (numeric_value, min_value, max_value)),
795 ),
796 ) = rayon::join(
797 || {
798 rayon::join(
799 || {
800 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
801 let value = if role_has_value(role) {
802 self.get_value(aref, ifaces)
803 } else {
804 None
805 };
806 (name, value)
807 },
808 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
809 )
810 },
811 || {
812 rayon::join(
813 || {
814 rayon::join(
815 || self.parse_states_with_bits(aref, role),
816 || {
817 if role != Role::Application {
818 self.get_extents(aref, ifaces)
819 } else {
820 None
821 }
822 },
823 )
824 },
825 || {
826 rayon::join(
827 || {
828 if let Some(actions) = prefetched_menu_actions.as_ref() {
829 actions.clone()
830 } else if role_has_actions(role) {
831 self.get_actions(aref)
832 } else {
833 (vec![], HashMap::new())
834 }
835 },
836 || {
837 if ifaces.value
838 && matches!(
839 role,
840 Role::Slider
841 | Role::ProgressBar
842 | Role::ScrollBar
843 | Role::SpinButton
844 )
845 {
846 if let Ok(proxy) = self.make_proxy(
847 &aref.bus_name,
848 &aref.path,
849 "org.a11y.atspi.Value",
850 ) {
851 (
852 proxy.get_property::<f64>("CurrentValue").ok(),
853 proxy.get_property::<f64>("MinimumValue").ok(),
854 proxy.get_property::<f64>("MaximumValue").ok(),
855 )
856 } else {
857 (None, None, None)
858 }
859 } else {
860 (None, None, None)
861 }
862 },
863 )
864 },
865 )
866 },
867 );
868
869 if name.is_none() && role == Role::StaticText {
872 if let Some(ref v) = value {
873 name = Some(v.clone());
874 }
875 }
876
877 let raw = {
897 let raw_role = if role_name.is_empty() {
898 format!("role_num:{}", role_num)
899 } else {
900 role_name
901 };
902 let mut raw = HashMap::new();
903 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
904 raw.insert(
905 "bus_name".into(),
906 serde_json::Value::String(aref.bus_name.clone()),
907 );
908 raw.insert(
909 "object_path".into(),
910 serde_json::Value::String(aref.path.clone()),
911 );
912 if let Some(ref n) = name {
915 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
916 }
917 if let Some(ref v) = value {
918 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
919 }
920 if let Some(ref d) = description {
921 raw.insert(
922 "atspi_description".into(),
923 serde_json::Value::String(d.clone()),
924 );
925 }
926 if (state_bits & (1 << 6)) != 0 {
932 raw.insert("atspi_defunct".into(), serde_json::Value::Bool(true));
933 }
934 raw
935 };
936
937 let name = xa11y_core::text::strip_bidi_opt(name);
940 let value = xa11y_core::text::strip_bidi_opt(value);
941 let description = xa11y_core::text::strip_bidi_opt(description);
942
943 let handle = self.cache_element(aref.clone());
944 if !action_index_map.is_empty() {
945 self.action_indices
946 .lock()
947 .unwrap_or_else(|e| e.into_inner())
948 .insert(handle, action_index_map);
949 }
950
951 let mut data: ElementData = ElementParts {
952 role,
953 name,
954 value,
955 description,
956 bounds,
957 actions,
958 states,
959 numeric_value,
960 min_value,
961 max_value,
962 stable_id: Some(aref.path.clone()),
963 pid,
964 raw,
965 handle,
966 }
967 .into();
968
969 if matches!(role, Role::Window | Role::Dialog) && self.is_top_level_window_ref(aref) {
970 let facts = self.window_manager.facts(&data);
971 if facts.bounds.is_some() {
972 data.bounds = facts.bounds;
973 }
974 for action in &facts.actions {
975 if !data.actions.iter().any(|existing| existing == action) {
976 data.actions.push((*action).to_string());
977 }
978 }
979 if facts.minimized.is_some() {
980 data.states.minimized = facts.minimized;
981 }
982 if facts.maximized.is_some() {
983 data.states.maximized = facts.maximized;
984 }
985 if facts.fullscreen.is_some() {
986 data.states.fullscreen = facts.fullscreen;
987 }
988 if let Some(backend) = facts.backend {
989 data.raw.insert(
990 "window_backend".to_string(),
991 serde_json::Value::String(backend.to_string()),
992 );
993 }
994 if let Some(version) = facts.protocol_version {
995 data.raw.insert(
996 "window_protocol_version".to_string(),
997 serde_json::Value::Number(version.into()),
998 );
999 }
1000 let capabilities = [
1001 "activate",
1002 "minimize",
1003 "maximize",
1004 "enter_fullscreen",
1005 "restore",
1006 "close",
1007 "move_to",
1008 "resize_to",
1009 ]
1010 .into_iter()
1011 .map(|action| {
1012 let status = if facts.backend.is_none() {
1013 "unknown"
1014 } else if facts.actions.contains(&action) {
1015 "supported"
1016 } else {
1017 "unsupported"
1018 };
1019 (
1020 action.to_string(),
1021 serde_json::Value::String(status.to_string()),
1022 )
1023 })
1024 .collect();
1025 data.raw.insert(
1026 "window_capabilities".to_string(),
1027 serde_json::Value::Object(capabilities),
1028 );
1029 }
1030
1031 data
1032 }
1033
1034 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
1036 let proxy = self.make_proxy(
1038 &aref.bus_name,
1039 &aref.path,
1040 "org.freedesktop.DBus.Properties",
1041 )?;
1042 let reply = proxy
1043 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
1044 .map_err(|e| Error::Platform {
1045 code: -1,
1046 message: format!("Get Parent property failed: {}", e),
1047 })?;
1048 let variant: zbus::zvariant::OwnedValue =
1050 reply.body().deserialize().map_err(|e| Error::Platform {
1051 code: -1,
1052 message: format!("Parent deserialize variant failed: {}", e),
1053 })?;
1054 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
1055 zbus::zvariant::Value::from(variant).try_into().map_err(
1056 |e: zbus::zvariant::Error| Error::Platform {
1057 code: -1,
1058 message: format!("Parent deserialize struct failed: {}", e),
1059 },
1060 )?;
1061 let path_str = path.as_str();
1062 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
1063 return Ok(None);
1064 }
1065 if path_str == "/org/a11y/atspi/accessible/root" {
1067 return Ok(None);
1068 }
1069 Ok(Some(AccessibleRef {
1070 bus_name: bus,
1071 path: path_str.to_string(),
1072 }))
1073 }
1074
1075 fn state_bits(&self, aref: &AccessibleRef) -> (u64, bool) {
1085 match self.get_state(aref) {
1086 Ok(state_bits) if state_bits.len() >= 2 => (
1087 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32),
1088 true,
1089 ),
1090 Ok(state_bits) if state_bits.len() == 1 => (state_bits[0] as u64, true),
1091 Ok(_) => (0, true),
1092 Err(_) => (0, false),
1093 }
1094 }
1095
1096 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
1098 self.parse_states_with_bits(aref, role).0
1099 }
1100
1101 fn parse_states_with_bits(&self, aref: &AccessibleRef, role: Role) -> (StateSet, u64) {
1105 let (bits, state_read_ok) = self.state_bits(aref);
1106
1107 const ACTIVE: u64 = 1 << 1;
1112 const BUSY: u64 = 1 << 3;
1113 const CHECKED: u64 = 1 << 4;
1114 const EDITABLE: u64 = 1 << 7;
1115 const ENABLED: u64 = 1 << 8;
1116 const EXPANDABLE: u64 = 1 << 9;
1117 const EXPANDED: u64 = 1 << 10;
1118 const FOCUSABLE: u64 = 1 << 11;
1119 const FOCUSED: u64 = 1 << 12;
1120 const ICONIFIED: u64 = 1 << 15;
1121 const MODAL: u64 = 1 << 16;
1122 const SELECTED: u64 = 1 << 23;
1123 const SENSITIVE: u64 = 1 << 24;
1124 const SHOWING: u64 = 1 << 25;
1125 const VISIBLE: u64 = 1 << 30;
1126 const INDETERMINATE: u64 = 1 << 32;
1127 const REQUIRED: u64 = 1 << 33;
1128
1129 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
1130 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
1131
1132 let checked = match role {
1133 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
1134 if (bits & INDETERMINATE) != 0 {
1135 Some(Toggled::Mixed)
1136 } else if (bits & CHECKED) != 0 {
1137 Some(Toggled::On)
1138 } else {
1139 Some(Toggled::Off)
1140 }
1141 }
1142 _ => None,
1143 };
1144
1145 let expanded = if (bits & EXPANDABLE) != 0 {
1146 Some((bits & EXPANDED) != 0)
1147 } else {
1148 None
1149 };
1150
1151 let states = StateParts {
1152 enabled,
1153 visible,
1154 focused: (bits & FOCUSED) != 0,
1155 active: (bits & ACTIVE) != 0,
1158 checked,
1159 selected: (bits & SELECTED) != 0,
1160 expanded,
1161 editable: (bits & EDITABLE) != 0,
1162 focusable: (bits & FOCUSABLE) != 0,
1163 modal: (bits & MODAL) != 0,
1164 required: (bits & REQUIRED) != 0,
1165 busy: (bits & BUSY) != 0,
1166 minimized: if matches!(role, Role::Window | Role::Dialog) {
1176 if state_read_ok {
1177 Some((bits & ICONIFIED) != 0)
1178 } else {
1179 None
1180 }
1181 } else {
1182 None
1183 },
1184 maximized: None,
1185 fullscreen: None,
1186 }
1187 .into();
1188 (states, bits)
1189 }
1190
1191 pub(crate) fn find_apps_by_pid(&self, pid: u32) -> Result<Vec<AccessibleRef>> {
1196 let mut matches = Vec::new();
1197 let registry = AccessibleRef {
1198 bus_name: "org.a11y.atspi.Registry".to_string(),
1199 path: "/org/a11y/atspi/accessible/root".to_string(),
1200 };
1201 let children = self.get_atspi_children(®istry)?;
1202
1203 for child in &children {
1204 if child.path == "/org/a11y/atspi/null" {
1205 continue;
1206 }
1207 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
1213 if app_pid == pid {
1214 matches.push(child.clone());
1215 continue;
1216 }
1217 }
1218 if let Ok(proxy) =
1220 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
1221 {
1222 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
1223 if app_pid as u32 == pid {
1224 matches.push(child.clone());
1225 }
1226 }
1227 }
1228 }
1229
1230 Ok(matches)
1231 }
1232
1233 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
1235 let proxy = self
1236 .make_proxy(
1237 "org.freedesktop.DBus",
1238 "/org/freedesktop/DBus",
1239 "org.freedesktop.DBus",
1240 )
1241 .ok()?;
1242 let reply = proxy
1243 .call_method("GetConnectionUnixProcessID", &(bus_name,))
1244 .ok()?;
1245 let pid: u32 = reply.body().deserialize().ok()?;
1246 if pid > 0 {
1247 Some(pid)
1248 } else {
1249 None
1250 }
1251 }
1252
1253 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
1255 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
1256 let reply = proxy
1257 .call_method("DoAction", &(index,))
1258 .map_err(|e| Error::Platform {
1259 code: -1,
1260 message: format!("DoAction({}) failed: {}", index, e),
1261 })?;
1262 let performed: bool = reply.body().deserialize().map_err(|e| Error::Platform {
1266 code: -1,
1267 message: format!("DoAction({}) reply decode failed: {}", index, e),
1268 })?;
1269 if !performed {
1270 return Err(Error::Platform {
1271 code: -1,
1272 message: format!(
1273 "DoAction({}) returned false; the accessibility provider declined the action",
1274 index
1275 ),
1276 });
1277 }
1278 Ok(())
1279 }
1280
1281 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
1283 self.action_indices
1284 .lock()
1285 .unwrap_or_else(|e| e.into_inner())
1286 .get(&handle)
1287 .and_then(|map| map.get(action).copied())
1288 .ok_or_else(|| Error::ActionNotSupported {
1289 action: action.to_string(),
1290 role: Role::Unknown, })
1292 }
1293
1294 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
1300 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
1301 return Some(pid);
1302 }
1303
1304 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
1311 {
1312 if let Ok(pid) = proxy.get_property::<i32>("Id") {
1313 if pid > 0 && std::path::Path::new(&format!("/proc/{pid}")).exists() {
1314 return Some(pid as u32);
1315 }
1316 }
1317 }
1318
1319 None
1320 }
1321
1322 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
1325 let role_name = self.get_role_name(aref).unwrap_or_default();
1326 let by_name = if !role_name.is_empty() {
1327 map_atspi_role(&role_name)
1328 } else {
1329 Role::Unknown
1330 };
1331 let coarse = if by_name != Role::Unknown {
1332 by_name
1333 } else {
1334 let role_num = self.get_role_number(aref).unwrap_or(0);
1336 map_atspi_role_number(role_num)
1337 };
1338 let role = self.refine_text_role(coarse, aref);
1339 if role == Role::Menu {
1340 let (actions, _) = self.get_actions(aref);
1341 refine_menu_role(role, &actions)
1342 } else {
1343 role
1344 }
1345 }
1346
1347 fn parent_role_is_application(&self, parent: &AccessibleRef) -> Result<bool> {
1359 let role_name = self.get_role_name(parent)?;
1360 let role = if role_name.is_empty() {
1361 map_atspi_role_number(self.get_role_number(parent)?)
1362 } else {
1363 map_atspi_role(&role_name)
1364 };
1365 Ok(role == Role::Application)
1366 }
1367
1368 fn ensure_top_level_window_target(&self, element: &ElementData, action: &str) -> Result<()> {
1369 let target = self.get_cached(element.handle)?;
1381 let Some(parent) = self.get_atspi_parent(&target)? else {
1382 return Ok(());
1383 };
1384 if self.parent_role_is_application(&parent)? {
1389 Ok(())
1390 } else {
1391 Err(Error::ActionNotSupported {
1392 action: action.to_string(),
1393 role: element.role,
1394 })
1395 }
1396 }
1397
1398 fn is_top_level_window_ref(&self, target: &AccessibleRef) -> bool {
1399 match self.get_atspi_parent(target) {
1400 Ok(None) => true,
1401 Ok(Some(parent)) => self.parent_role_is_application(&parent).unwrap_or(false),
1402 Err(_) => false,
1403 }
1404 }
1405
1406 fn matches_ref(
1420 &self,
1421 aref: &AccessibleRef,
1422 simple: &xa11y_core::selector::SimpleSelector,
1423 pid: Option<u32>,
1424 ) -> bool {
1425 let needs_role = simple.role.is_some()
1428 || simple
1429 .filters
1430 .iter()
1431 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1432 let role = if needs_role {
1433 Some(self.resolve_role(aref))
1434 } else {
1435 None
1436 };
1437
1438 if let Some(ref role_match) = simple.role {
1439 match role_match {
1440 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1441 if role != Some(*expected) {
1442 return false;
1443 }
1444 }
1445 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1446 let raw_role = self.get_role_name(aref).unwrap_or_default();
1447 if raw_role != *platform_role {
1448 return false;
1449 }
1450 }
1451 }
1452 }
1453
1454 let mut state_set: Option<StateSet> = None;
1455 let mut ifaces: Option<Interfaces> = None;
1458 let mut value_of = |aref: &AccessibleRef| {
1459 let ifaces =
1460 *ifaces.get_or_insert_with(|| self.get_interfaces(aref).unwrap_or_default());
1461 self.get_value(aref, ifaces)
1462 };
1463
1464 for filter in &simple.filters {
1465 let attr = filter.attr.as_str();
1466 let resolved: Option<Option<String>> = match attr {
1467 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1468 "name" => {
1469 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1470 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1473 value_of(aref)
1474 } else {
1475 name
1476 };
1477 Some(resolved)
1478 }
1479 "value" => Some(value_of(aref)),
1480 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1481 "enabled" | "visible" | "focused" | "active" | "focusable" | "selected"
1482 | "editable" | "modal" | "required" | "busy" | "expanded" | "checked" => {
1483 let s = state_set.get_or_insert_with(|| {
1484 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1488 });
1489 Some(state_attr_to_string(attr, s))
1490 }
1491 _ => None, };
1493
1494 match resolved {
1495 Some(value) => {
1496 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1497 {
1498 return false;
1499 }
1500 }
1501 None => {
1502 let data = self.build_element_data(aref, pid);
1509 return xa11y_core::selector::matches_simple(&data, simple);
1510 }
1511 }
1512 }
1513
1514 true
1515 }
1516
1517 fn collect_matching_refs_group(
1527 &self,
1528 parent: &AccessibleRef,
1529 clauses: &[&xa11y_core::selector::SimpleSelector],
1530 depth: u32,
1531 max_depth: u32,
1532 limit: Option<usize>,
1533 pid: Option<u32>,
1534 ) -> Result<Vec<(usize, AccessibleRef)>> {
1535 if depth > max_depth {
1536 return Ok(vec![]);
1537 }
1538
1539 let Some(children) = self.live_atspi_children(parent)? else {
1540 return Ok(Vec::new());
1541 };
1542
1543 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1551 let mut to_search: Vec<AccessibleRef> = Vec::new();
1552 for child in children {
1553 if child.path == "/org/a11y/atspi/null"
1554 || child.bus_name.is_empty()
1555 || child.path.is_empty()
1556 {
1557 continue;
1558 }
1559
1560 if !parent_is_registry {
1561 let child_role = self.get_role_name(&child).unwrap_or_default();
1562 if child_role == "application" {
1563 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1564 for gc in grandchildren {
1565 if gc.path == "/org/a11y/atspi/null"
1566 || gc.bus_name.is_empty()
1567 || gc.path.is_empty()
1568 {
1569 continue;
1570 }
1571 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1572 if gc_role == "application" {
1573 continue;
1574 }
1575 to_search.push(gc);
1576 }
1577 continue;
1578 }
1579 }
1580 to_search.push(child);
1581 }
1582
1583 if to_search.is_empty() {
1586 self.check_chromium_a11y_enabled(parent, None)?;
1587 }
1588
1589 type ChildBatch = (Vec<(usize, AccessibleRef)>, Option<Error>);
1604 let per_child: Vec<ChildBatch> = to_search
1605 .par_iter()
1606 .map(|child| {
1607 let mut child_results: Vec<(usize, AccessibleRef)> = Vec::new();
1608 for (idx, simple) in clauses.iter().enumerate() {
1609 if self.matches_ref(child, simple, pid) {
1610 child_results.push((idx, child.clone()));
1611 }
1612 }
1613 match self.collect_matching_refs_group(
1614 child,
1615 clauses,
1616 depth + 1,
1617 max_depth,
1618 limit,
1619 pid,
1620 ) {
1621 Ok(sub) => {
1622 child_results.extend(sub);
1623 (child_results, None)
1624 }
1625 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1626 Err(_) => (child_results, None),
1627 }
1628 })
1629 .collect();
1630
1631 let mut results = Vec::new();
1643 for (batch, maybe_err) in per_child {
1644 if let Some(err) = maybe_err {
1645 return Err(err);
1646 }
1647 for r in batch {
1648 results.push(r);
1649 if let Some(limit) = limit {
1650 if results.len() >= limit {
1651 return Ok(results);
1652 }
1653 }
1654 }
1655 }
1656 Ok(results)
1657 }
1658}
1659
1660impl Provider for LinuxProvider {
1661 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1662 match element {
1663 None => {
1664 self.list_apps()
1667 }
1668 Some(element_data) => {
1669 let aref = self.get_cached(element_data.handle)?;
1670 let Some(children) = self.live_atspi_children(&aref)? else {
1674 return Ok(Vec::new());
1675 };
1676 let pid = element_data.pid;
1677
1678 let mut to_build: Vec<AccessibleRef> = Vec::new();
1681 for child_ref in &children {
1682 if child_ref.path == "/org/a11y/atspi/null"
1683 || child_ref.bus_name.is_empty()
1684 || child_ref.path.is_empty()
1685 {
1686 continue;
1687 }
1688 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1689 if child_role == "application" {
1690 let Some(grandchildren) = self.live_atspi_children(child_ref)? else {
1693 continue;
1694 };
1695 for gc_ref in grandchildren {
1696 if gc_ref.path == "/org/a11y/atspi/null"
1697 || gc_ref.bus_name.is_empty()
1698 || gc_ref.path.is_empty()
1699 {
1700 continue;
1701 }
1702 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1703 if gc_role == "application" {
1704 continue;
1705 }
1706 to_build.push(gc_ref);
1707 }
1708 continue;
1709 }
1710 to_build.push(child_ref.clone());
1711 }
1712
1713 if to_build.is_empty() {
1714 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1715 }
1716
1717 let results: Vec<ElementData> = to_build
1718 .par_iter()
1719 .map(|r| self.build_element_data(r, pid))
1720 .collect();
1721
1722 Ok(results)
1723 }
1724 }
1725 }
1726
1727 fn find_elements_group(
1728 &self,
1729 root: &ElementData,
1730 group: &xa11y_core::selector::SelectorGroup,
1731 limit: Option<usize>,
1732 max_depth: Option<u32>,
1733 ) -> Result<Vec<ElementData>> {
1734 if group.clauses.is_empty() {
1735 return Ok(vec![]);
1736 }
1737 if group.clauses.iter().any(|c| c.segments.is_empty()) {
1740 return Ok(vec![]);
1741 }
1742
1743 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1753
1754 let firsts: Vec<&xa11y_core::selector::SimpleSelector> = group
1755 .clauses
1756 .iter()
1757 .map(|c| &c.segments[0].simple)
1758 .collect();
1759
1760 let start_ref = self.get_cached(root.handle)?;
1761
1762 let phase1_limit = if group.clauses.len() == 1 {
1771 let clause = &group.clauses[0];
1772 let first = firsts[0];
1773 let outer = if clause.segments.len() == 1 {
1774 limit
1775 } else {
1776 None
1777 };
1778 match (outer, first.nth) {
1779 (Some(l), Some(n)) => Some(l.max(n)),
1780 (_, Some(n)) => Some(n),
1781 (l, None) => l,
1782 }
1783 } else {
1784 None
1785 };
1786
1787 let pid_from_root = root.pid;
1788 let phase1: Vec<(usize, AccessibleRef)> = self.collect_matching_refs_group(
1789 &start_ref,
1790 &firsts,
1791 0,
1792 max_depth_val,
1793 phase1_limit,
1794 pid_from_root,
1795 )?;
1796
1797 let mut by_clause: Vec<Vec<(usize, AccessibleRef)>> =
1801 (0..group.clauses.len()).map(|_| Vec::new()).collect();
1802 for (walk_pos, (clause_idx, aref)) in phase1.into_iter().enumerate() {
1803 by_clause[clause_idx].push((walk_pos, aref));
1804 }
1805
1806 let mut merged: Vec<(usize, ElementData)> = Vec::new();
1814 for (clause_idx, hits) in by_clause.into_iter().enumerate() {
1815 if hits.is_empty() {
1816 continue;
1817 }
1818 let clause = &group.clauses[clause_idx];
1819 let phase1_data: Vec<(usize, ElementData)> = hits
1821 .par_iter()
1822 .map(|(pos, aref)| {
1823 (*pos, self.build_element_data(aref, pid_from_root))
1827 })
1828 .collect();
1829
1830 if clause.segments.len() == 1 {
1831 let mut candidates = phase1_data;
1833 if let Some(nth) = clause.segments[0].simple.nth {
1834 if nth <= candidates.len() {
1835 let kept = candidates.remove(nth - 1);
1836 candidates.clear();
1837 candidates.push(kept);
1838 } else {
1839 candidates.clear();
1840 }
1841 }
1842 merged.extend(candidates);
1843 continue;
1844 }
1845
1846 for (anchor_pos, head) in phase1_data {
1851 let narrowed = self.narrow_multi_segment(
1852 vec![head],
1853 &clause.segments[1..],
1854 max_depth_val,
1855 None,
1856 )?;
1857 for n in narrowed {
1858 merged.push((anchor_pos, n));
1859 }
1860 }
1861 }
1862
1863 merged.sort_by_key(|(pos, _)| *pos);
1867 let mut seen: HashSet<(String, String)> = HashSet::new();
1868 let mut out: Vec<ElementData> = Vec::with_capacity(merged.len());
1869 for (_, data) in merged {
1870 if let Ok(aref) = self.get_cached(data.handle) {
1874 if !seen.insert((aref.bus_name.clone(), aref.path.clone())) {
1875 continue;
1876 }
1877 }
1878 out.push(data);
1879 }
1880 if let Some(l) = limit {
1881 out.truncate(l);
1882 }
1883 Ok(out)
1884 }
1885
1886 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1887 let aref = self.get_cached(element.handle)?;
1888 match self.get_atspi_parent(&aref)? {
1889 Some(parent_ref) => {
1890 let data = self.build_element_data(&parent_ref, element.pid);
1891 Ok(Some(data))
1892 }
1893 None => Ok(None),
1894 }
1895 }
1896
1897 fn list_shell_surfaces(&self) -> Result<Vec<(ShellSurfaceKind, ElementData)>> {
1934 let mut surfaces = Vec::new();
1935 'apps: for app in self.list_apps()? {
1936 let app_ref = self.get_cached(app.handle)?;
1937 let top_levels = match self.get_atspi_children(&app_ref) {
1938 Ok(t) => t,
1939 Err(_) => continue,
1943 };
1944 for top in top_levels
1945 .iter()
1946 .filter(|t| t.path != "/org/a11y/atspi/null")
1947 {
1948 let Some(role_number) = self
1959 .scan_role_number(top)
1960 .map_err(|e| classify_failure(&app, top, e))?
1961 else {
1962 continue 'apps;
1967 };
1968 let Some(attributes) = self
1977 .scan_attributes(top)
1978 .map_err(|e| classify_failure(&app, top, e))?
1979 else {
1980 continue 'apps;
1981 };
1982 let Some(attributes) = attributes else {
1983 continue;
1984 };
1985 if is_dock_frame(role_number, &attributes) {
1986 surfaces.push((
1987 ShellSurfaceKind::Panel,
1988 self.build_element_data(top, app.pid),
1989 ));
1990 }
1991 }
1992 }
1993 Ok(surfaces)
1994 }
1995
1996 fn splits_app_across_entries(&self) -> bool {
2001 true
2002 }
2003
2004 fn app_roots(&self, app: &ElementData) -> Result<Vec<ElementData>> {
2005 let Some(pid) = app.pid else {
2006 return Ok(vec![app.clone()]);
2007 };
2008 let mut roots: Vec<_> = self
2009 .list_apps()?
2010 .into_iter()
2011 .filter(|candidate| candidate.pid == Some(pid))
2012 .collect();
2013 if roots.is_empty() {
2014 roots.push(app.clone());
2015 }
2016 Ok(roots)
2017 }
2018
2019 fn list_apps(&self) -> Result<Vec<ElementData>> {
2026 let registry = AccessibleRef {
2027 bus_name: "org.a11y.atspi.Registry".to_string(),
2028 path: "/org/a11y/atspi/accessible/root".to_string(),
2029 };
2030 let children = self.get_atspi_children(®istry)?;
2031
2032 let valid: Vec<(&AccessibleRef, String)> = children
2034 .iter()
2035 .filter(|c| c.path != "/org/a11y/atspi/null")
2036 .filter_map(|c| {
2037 let name = self.get_name(c).unwrap_or_default();
2038 if name.is_empty() {
2039 None
2040 } else {
2041 Some((c, name))
2042 }
2043 })
2044 .collect();
2045
2046 let results: Vec<ElementData> = valid
2047 .par_iter()
2048 .map(|(child, app_name)| {
2049 let pid = self.get_app_pid(child);
2050 let mut data = self.build_element_data(child, pid);
2051 data.name = Some(app_name.clone());
2052 data
2053 })
2054 .collect();
2055
2056 Ok(results)
2057 }
2058
2059 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
2071 let registry = AccessibleRef {
2072 bus_name: "org.a11y.atspi.Registry".to_string(),
2073 path: "/org/a11y/atspi/accessible/root".to_string(),
2074 };
2075 let children = self.get_atspi_children(®istry)?;
2076
2077 let mut total = 0usize;
2078 let mut unresolved = 0usize;
2079 for child in children.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
2080 total += 1;
2081 match self.get_app_pid(child) {
2082 Some(p) if p == pid => {
2083 let mut data = self.build_element_data(child, Some(p));
2084 let name = self.get_name(child).unwrap_or_default();
2086 if !name.is_empty() {
2087 data.name = Some(name);
2088 }
2089 return Ok(data);
2090 }
2091 Some(_) => {}
2092 None => unresolved += 1,
2093 }
2094 }
2095 Err(
2096 Error::selector_not_matched(format!("application[pid={pid}]")).diagnose(
2097 xa11y_core::Diagnosis::new().last_observed(format!(
2098 "{total} AT-SPI registry entries examined, {unresolved} without a \
2099 resolvable pid"
2100 )),
2101 ),
2102 )
2103 }
2104
2105 fn focused_app(&self) -> Result<ElementData> {
2118 const ACTIVE: u64 = 1 << 1;
2120
2121 let registry = AccessibleRef {
2122 bus_name: "org.a11y.atspi.Registry".to_string(),
2123 path: "/org/a11y/atspi/accessible/root".to_string(),
2124 };
2125 let apps = self.get_atspi_children(®istry)?;
2126
2127 for app in apps.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
2128 let windows = match self.get_atspi_children(app) {
2129 Ok(w) => w,
2130 Err(_) => continue,
2133 };
2134 let has_active_window = windows
2135 .iter()
2136 .filter(|w| w.path != "/org/a11y/atspi/null")
2137 .any(|w| self.state_bits(w).0 & ACTIVE != 0);
2138 if has_active_window {
2139 let pid = self.get_app_pid(app);
2140 let mut data = self.build_element_data(app, pid);
2141 let name = self.get_name(app).unwrap_or_default();
2142 if !name.is_empty() {
2143 data.name = Some(name);
2144 }
2145 return Ok(data);
2146 }
2147 }
2148
2149 Err(Error::selector_not_matched("focused application"))
2150 }
2151
2152 fn press(&self, element: &ElementData) -> Result<()> {
2153 let target = self.get_cached(element.handle)?;
2154 if let Ok(index) = self.get_action_index(element.handle, "press") {
2156 return self.do_atspi_action_by_index(&target, index);
2157 }
2158 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
2177 let outer_name = self.get_name(&target).unwrap_or_default();
2178 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
2179 return self.do_atspi_action_by_index(&inner, index);
2180 }
2181 }
2182 Err(Error::ActionNotSupported {
2183 action: "press".to_string(),
2184 role: element.role,
2185 })
2186 }
2187
2188 fn focus(&self, element: &ElementData) -> Result<()> {
2189 let target = self.get_cached(element.handle)?;
2190 let proxy = self
2202 .make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
2203 .map_err(|e| Error::Platform {
2204 code: -1,
2205 message: format!("Component proxy while focusing failed: {}", e),
2206 })?;
2207 let reply = grab_focus_result(
2208 proxy.call_method("GrabFocus", &()),
2209 "focusing",
2210 "focus",
2211 element.role,
2212 )?;
2213 let grabbed: bool = reply.body().deserialize().map_err(|e| Error::Platform {
2214 code: -1,
2215 message: format!("Component.GrabFocus reply while focusing failed: {}", e),
2216 })?;
2217 if grabbed {
2218 return Ok(());
2219 }
2220 Err(Error::ActionNotSupported {
2223 action: "focus".to_string(),
2224 role: element.role,
2225 })
2226 }
2227
2228 fn blur(&self, element: &ElementData) -> Result<()> {
2229 let target = self.get_cached(element.handle)?;
2230 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
2234 if parent_ref.path != "/org/a11y/atspi/null" {
2235 let p = self.make_proxy(
2236 &parent_ref.bus_name,
2237 &parent_ref.path,
2238 "org.a11y.atspi.Component",
2239 )?;
2240 p.call_method("GrabFocus", &())
2241 .map_err(|e| Error::Platform {
2242 code: -1,
2243 message: format!("Component.GrabFocus on parent failed: {}", e),
2244 })?;
2245 return Ok(());
2246 }
2247 }
2248 Err(Error::ActionNotSupported {
2249 action: "blur".to_string(),
2250 role: element.role,
2251 })
2252 }
2253
2254 fn toggle(&self, element: &ElementData) -> Result<()> {
2255 let target = self.get_cached(element.handle)?;
2256 if let Ok(index) = self.get_action_index(element.handle, "toggle") {
2261 return self.do_atspi_action_by_index(&target, index);
2262 }
2263 if matches!(
2286 element.role,
2287 Role::CheckBox | Role::RadioButton | Role::Switch
2288 ) {
2289 if let Ok(index) = self.get_action_index(element.handle, "press") {
2290 return self.do_atspi_action_by_index(&target, index);
2291 }
2292 }
2293 Err(Error::ActionNotSupported {
2294 action: "toggle".to_string(),
2295 role: element.role,
2296 })
2297 }
2298
2299 fn select(&self, element: &ElementData) -> Result<()> {
2300 let target = self.get_cached(element.handle)?;
2301 let index = self
2302 .get_action_index(element.handle, "select")
2303 .map_err(|_| Error::ActionNotSupported {
2304 action: "select".to_string(),
2305 role: element.role,
2306 })?;
2307 self.do_atspi_action_by_index(&target, index)
2308 }
2309
2310 fn expand(&self, element: &ElementData) -> Result<()> {
2311 let target = self.get_cached(element.handle)?;
2312 let index = self
2313 .get_action_index(element.handle, "expand")
2314 .map_err(|_| Error::ActionNotSupported {
2315 action: "expand".to_string(),
2316 role: element.role,
2317 })?;
2318 self.do_atspi_action_by_index(&target, index)
2319 }
2320
2321 fn collapse(&self, element: &ElementData) -> Result<()> {
2322 let target = self.get_cached(element.handle)?;
2323 let index = self
2324 .get_action_index(element.handle, "collapse")
2325 .map_err(|_| Error::ActionNotSupported {
2326 action: "collapse".to_string(),
2327 role: element.role,
2328 })?;
2329 self.do_atspi_action_by_index(&target, index)
2330 }
2331
2332 fn show_menu(&self, element: &ElementData) -> Result<()> {
2333 let target = self.get_cached(element.handle)?;
2334 let index = self
2335 .get_action_index(element.handle, "show_menu")
2336 .map_err(|_| Error::ActionNotSupported {
2337 action: "show_menu".to_string(),
2338 role: element.role,
2339 })?;
2340 self.do_atspi_action_by_index(&target, index)
2341 }
2342
2343 fn increment(&self, element: &ElementData) -> Result<()> {
2344 let target = self.get_cached(element.handle)?;
2345 if let Ok(index) = self.get_action_index(element.handle, "increment") {
2347 return self.do_atspi_action_by_index(&target, index);
2348 }
2349 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
2350 let current: f64 = proxy
2351 .get_property("CurrentValue")
2352 .map_err(|e| Error::Platform {
2353 code: -1,
2354 message: format!("Value.CurrentValue failed: {}", e),
2355 })?;
2356 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
2357 let step = if step <= 0.0 { 1.0 } else { step };
2358 proxy
2359 .set_property("CurrentValue", current + step)
2360 .map_err(|e| Error::Platform {
2361 code: -1,
2362 message: format!("Value.SetCurrentValue failed: {}", e),
2363 })
2364 }
2365
2366 fn decrement(&self, element: &ElementData) -> Result<()> {
2367 let target = self.get_cached(element.handle)?;
2368 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
2369 return self.do_atspi_action_by_index(&target, index);
2370 }
2371 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
2372 let current: f64 = proxy
2373 .get_property("CurrentValue")
2374 .map_err(|e| Error::Platform {
2375 code: -1,
2376 message: format!("Value.CurrentValue failed: {}", e),
2377 })?;
2378 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
2379 let step = if step <= 0.0 { 1.0 } else { step };
2380 proxy
2381 .set_property("CurrentValue", current - step)
2382 .map_err(|e| Error::Platform {
2383 code: -1,
2384 message: format!("Value.SetCurrentValue failed: {}", e),
2385 })
2386 }
2387
2388 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
2389 let target = self.get_cached(element.handle)?;
2390 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
2391 proxy
2392 .call_method("ScrollTo", &(0u32,))
2393 .map_err(|e| Error::Platform {
2394 code: -1,
2395 message: format!("ScrollTo failed: {}", e),
2396 })?;
2397 Ok(())
2398 }
2399
2400 fn activate(&self, element: &ElementData) -> Result<()> {
2411 self.ensure_top_level_window_target(element, "activate")?;
2435 if self.window_manager.uses_native() {
2436 return self.window_manager.activate(element);
2437 }
2438 let target = self.get_cached(element.handle)?;
2439 let proxy = self
2440 .make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
2441 .map_err(|e| Error::Platform {
2442 code: -1,
2443 message: format!("Component proxy while activating failed: {}", e),
2444 })?;
2445 let reply = grab_focus_result(
2446 proxy.call_method("GrabFocus", &()),
2447 "activating",
2448 "activate",
2449 element.role,
2450 )?;
2451 let grabbed: bool = reply.body().deserialize().map_err(|e| Error::Platform {
2452 code: -1,
2453 message: format!("Component.GrabFocus reply while activating failed: {}", e),
2454 })?;
2455 if grabbed {
2456 return Ok(());
2457 }
2458 Err(Error::ActionNotSupported {
2459 action: "activate".to_string(),
2460 role: element.role,
2461 })
2462 }
2463
2464 fn minimize(&self, element: &ElementData) -> Result<()> {
2465 self.ensure_top_level_window_target(element, "minimize")?;
2466 self.window_manager.minimize(element)
2467 }
2468
2469 fn maximize(&self, element: &ElementData) -> Result<()> {
2470 self.ensure_top_level_window_target(element, "maximize")?;
2471 self.window_manager.maximize(element)
2472 }
2473
2474 fn enter_fullscreen(&self, element: &ElementData) -> Result<()> {
2475 self.ensure_top_level_window_target(element, "enter_fullscreen")?;
2476 self.window_manager.enter_fullscreen(element)
2477 }
2478
2479 fn restore(&self, element: &ElementData) -> Result<()> {
2480 self.ensure_top_level_window_target(element, "restore")?;
2481 self.window_manager.restore(element)
2482 }
2483
2484 fn close(&self, element: &ElementData) -> Result<()> {
2485 self.ensure_top_level_window_target(element, "close")?;
2486 self.window_manager.close(element)
2487 }
2488
2489 fn move_to(&self, element: &ElementData, x: i32, y: i32) -> Result<()> {
2490 self.ensure_top_level_window_target(element, "move_to")?;
2491 if self.window_manager.uses_native() {
2492 return self.window_manager.move_to(element, x, y);
2493 }
2494 let target = self.get_cached(element.handle)?;
2495 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
2496 let scale = crate::scale::coordinate_scale();
2497 let point = xa11y_core::Point::new(x, y).to_physical(scale);
2498 let reply = proxy
2500 .call_method("SetPosition", &(point.x, point.y, 0u32))
2501 .map_err(|e| {
2502 if is_absent_member(&e) {
2503 Error::ActionNotSupported {
2504 action: "move_to".to_string(),
2505 role: element.role,
2506 }
2507 } else {
2508 Error::Platform {
2509 code: -1,
2510 message: format!("Component.SetPosition while moving window failed: {e}"),
2511 }
2512 }
2513 })?;
2514 let moved: bool = reply.body().deserialize().map_err(|e| Error::Platform {
2515 code: -1,
2516 message: format!("Component.SetPosition reply while moving window failed: {e}"),
2517 })?;
2518 if moved {
2519 Ok(())
2520 } else {
2521 Err(Error::ActionNotSupported {
2522 action: "move_to".to_string(),
2523 role: element.role,
2524 })
2525 }
2526 }
2527
2528 fn resize_to(&self, element: &ElementData, w: u32, h: u32) -> Result<()> {
2529 self.ensure_top_level_window_target(element, "resize_to")?;
2530 if self.window_manager.uses_native() {
2531 return self.window_manager.resize_to(element, w, h);
2532 }
2533 let target = self.get_cached(element.handle)?;
2534 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
2535 let physical = Rect {
2536 x: 0,
2537 y: 0,
2538 width: w,
2539 height: h,
2540 }
2541 .to_physical(crate::scale::coordinate_scale());
2542 let width = i32::try_from(physical.width).map_err(|_| Error::InvalidActionData {
2543 message: format!("resize width {w} is too large for AT-SPI"),
2544 })?;
2545 let height = i32::try_from(physical.height).map_err(|_| Error::InvalidActionData {
2546 message: format!("resize height {h} is too large for AT-SPI"),
2547 })?;
2548 let reply = proxy
2549 .call_method("SetSize", &(width, height))
2550 .map_err(|e| {
2551 if is_absent_member(&e) {
2552 Error::ActionNotSupported {
2553 action: "resize_to".to_string(),
2554 role: element.role,
2555 }
2556 } else {
2557 Error::Platform {
2558 code: -1,
2559 message: format!("Component.SetSize while resizing window failed: {e}"),
2560 }
2561 }
2562 })?;
2563 let resized: bool = reply.body().deserialize().map_err(|e| Error::Platform {
2564 code: -1,
2565 message: format!("Component.SetSize reply while resizing window failed: {e}"),
2566 })?;
2567 if resized {
2568 Ok(())
2569 } else {
2570 Err(Error::ActionNotSupported {
2571 action: "resize_to".to_string(),
2572 role: element.role,
2573 })
2574 }
2575 }
2576
2577 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
2578 let target = self.get_cached(element.handle)?;
2579 let proxy = self
2580 .make_proxy(
2581 &target.bus_name,
2582 &target.path,
2583 "org.a11y.atspi.EditableText",
2584 )
2585 .map_err(|_| Error::TextValueNotSupported)?;
2586 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
2588 return Ok(());
2589 }
2590 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
2596 let msg = e.to_string();
2597 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
2598 Error::TextValueNotSupported
2599 } else {
2600 Error::Platform {
2601 code: -1,
2602 message: format!("EditableText.{} failed: {}", op, msg),
2603 }
2604 }
2605 };
2606 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
2607 return Err(classify_editable_text_error("DeleteText", e));
2608 }
2609 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
2610 return Err(classify_editable_text_error("InsertText", e));
2611 }
2612 Ok(())
2613 }
2614
2615 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
2616 let target = self.get_cached(element.handle)?;
2617 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
2618 proxy
2619 .set_property("CurrentValue", value)
2620 .map_err(|e| Error::Platform {
2621 code: -1,
2622 message: format!("SetValue failed: {}", e),
2623 })
2624 }
2625
2626 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
2627 let target = self.get_cached(element.handle)?;
2628 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
2631 let insert_pos = text_proxy
2632 .as_ref()
2633 .ok()
2634 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
2635 .unwrap_or(-1); let proxy = self
2638 .make_proxy(
2639 &target.bus_name,
2640 &target.path,
2641 "org.a11y.atspi.EditableText",
2642 )
2643 .map_err(|_| Error::TextValueNotSupported)?;
2644 let pos = if insert_pos >= 0 {
2645 insert_pos
2646 } else {
2647 i32::MAX
2648 };
2649 proxy
2650 .call_method("InsertText", &(pos, text, text.len() as i32))
2651 .map_err(|e| Error::Platform {
2652 code: -1,
2653 message: format!("EditableText.InsertText failed: {}", e),
2654 })?;
2655 Ok(())
2656 }
2657
2658 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
2659 let target = self.get_cached(element.handle)?;
2660 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
2661 proxy
2662 .call_method("SetSelection", &(0i32, start as i32, end as i32))
2663 .map_err(|e| Error::Platform {
2664 code: -1,
2665 message: format!("Text.SetSelection failed: {}", e),
2666 })?;
2667 Ok(())
2668 }
2669
2670 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
2671 match action {
2672 "press" => self.press(element),
2673 "focus" => self.focus(element),
2674 "blur" => self.blur(element),
2675 "toggle" => self.toggle(element),
2676 "select" => self.select(element),
2677 "expand" => self.expand(element),
2678 "collapse" => self.collapse(element),
2679 "show_menu" => self.show_menu(element),
2680 "increment" => self.increment(element),
2681 "decrement" => self.decrement(element),
2682 "scroll_into_view" => self.scroll_into_view(element),
2683 "activate" => self.activate(element),
2684 "minimize" => self.minimize(element),
2685 "maximize" => self.maximize(element),
2686 "enter_fullscreen" => self.enter_fullscreen(element),
2687 "restore" => self.restore(element),
2688 "close" => self.close(element),
2689 "move_to" => Err(Error::InvalidActionData {
2693 message: "perform_action(\"move_to\") requires coordinates; call move_to(x, y)"
2694 .to_string(),
2695 }),
2696 "resize_to" => Err(Error::InvalidActionData {
2697 message: "perform_action(\"resize_to\") requires dimensions; call \
2698 resize_to(width, height)"
2699 .to_string(),
2700 }),
2701 _ => Err(Error::ActionNotSupported {
2702 action: action.to_string(),
2703 role: element.role,
2704 }),
2705 }
2706 }
2707
2708 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
2709 let pid = element.pid.ok_or(Error::Platform {
2710 code: -1,
2711 message: "Element has no PID for subscribe".to_string(),
2712 })?;
2713 let app_name = element.name.clone().unwrap_or_default();
2714 crate::events::subscribe_for_pid(self, pid, app_name)
2715 }
2716}
2717
2718fn role_has_value(role: Role) -> bool {
2721 !matches!(
2722 role,
2723 Role::Application
2724 | Role::Window
2725 | Role::Dialog
2726 | Role::Group
2727 | Role::MenuBar
2728 | Role::Toolbar
2729 | Role::TabGroup
2730 | Role::SplitGroup
2731 | Role::Table
2732 | Role::TableRow
2733 | Role::Separator
2734 )
2735}
2736
2737fn role_has_actions(role: Role) -> bool {
2749 matches!(
2750 role,
2751 Role::Button
2752 | Role::CheckBox
2753 | Role::RadioButton
2754 | Role::MenuItem
2755 | Role::Link
2756 | Role::ComboBox
2757 | Role::TextField
2758 | Role::TextArea
2759 | Role::SpinButton
2760 | Role::Tab
2761 | Role::TreeItem
2762 | Role::ListItem
2763 | Role::ScrollBar
2764 | Role::Slider
2765 | Role::Menu
2766 | Role::Image
2767 | Role::Unknown
2768 )
2769}
2770
2771fn grab_focus_result(
2783 call: std::result::Result<zbus::Message, zbus::Error>,
2784 verb: &str,
2785 action: &str,
2786 role: Role,
2787) -> Result<zbus::Message> {
2788 call.map_err(|e| match e {
2789 zbus::Error::MethodError(name, _, _) => {
2790 grab_focus_error(name.as_str().to_string(), verb, action, role)
2791 }
2792 other => Error::Platform {
2793 code: -1,
2794 message: format!("Component.GrabFocus while {verb} failed: {other}"),
2795 },
2796 })
2797}
2798
2799fn grab_focus_error(name: String, verb: &str, action: &str, role: Role) -> Error {
2802 if name == "org.freedesktop.DBus.Error.NotSupported" {
2803 Error::ActionNotSupported {
2804 action: action.to_string(),
2805 role,
2806 }
2807 } else {
2808 Error::Platform {
2809 code: -1,
2810 message: format!("Component.GrabFocus while {verb} failed: {name}"),
2811 }
2812 }
2813}
2814
2815pub(crate) fn map_atspi_role(role_name: &str) -> Role {
2817 match role_name.to_lowercase().as_str() {
2818 "application" => Role::Application,
2819 "window" | "frame" => Role::Window,
2820 "dialog" | "file chooser" => Role::Dialog,
2821 "alert" | "notification" | "info bar" => Role::Alert,
2822 "push button" | "push button menu" => Role::Button,
2823 "toggle button" => Role::Switch,
2824 "check box" | "check menu item" => Role::CheckBox,
2825 "radio button" | "radio menu item" => Role::RadioButton,
2826 "entry" | "password text" => Role::TextField,
2827 "spin button" | "spinbutton" => Role::SpinButton,
2828 "text" | "textbox" | "embedded" | "terminal" => Role::TextArea,
2834 "label" | "static" | "caption" => Role::StaticText,
2835 "combo box" | "combobox" => Role::ComboBox,
2836 "list" | "list box" | "listbox" => Role::List,
2838 "list item" => Role::ListItem,
2839 "menu" | "popup menu" => Role::Menu,
2840 "menu item" | "tearoff menu item" => Role::MenuItem,
2841 "menu bar" => Role::MenuBar,
2842 "page tab" => Role::Tab,
2843 "page tab list" => Role::TabGroup,
2844 "table" | "tree table" => Role::Table,
2845 "table row" => Role::TableRow,
2846 "table cell"
2849 | "table column header"
2850 | "table row header"
2851 | "column header"
2852 | "row header" => Role::TableCell,
2853 "tool bar" => Role::Toolbar,
2854 "scroll bar" => Role::ScrollBar,
2855 "slider" => Role::Slider,
2856 "image" | "icon" | "desktop icon" => Role::Image,
2857 "link" => Role::Link,
2858 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" | "paragraph"
2859 | "header" | "footer" | "grouping" | "block quote" | "redundant object" | "tree"
2860 | "comment" | "title bar" | "calendar" => Role::Group,
2861 "progress bar" | "level bar" => Role::ProgressBar,
2862 "tree item" => Role::TreeItem,
2863 "document web"
2864 | "document frame"
2865 | "document spreadsheet"
2866 | "document presentation"
2867 | "document text"
2868 | "document email" => Role::WebArea,
2869 "heading" => Role::Heading,
2870 "separator" => Role::Separator,
2871 "split pane" => Role::SplitGroup,
2872 "tooltip" | "tool tip" => Role::Tooltip,
2873 "status bar" | "statusbar" => Role::Status,
2874 "landmark" | "navigation" => Role::Navigation,
2875 _ => xa11y_core::unknown_role(role_name),
2876 }
2877}
2878
2879const ATSPI_ROLE_FRAME: u32 = 23;
2886
2887pub(crate) fn map_atspi_role_number(role: u32) -> Role {
2890 match role {
2891 2 => Role::Alert, 5 => Role::Group, 7 => Role::CheckBox, 8 => Role::CheckBox, 10 => Role::TableCell, 11 => Role::ComboBox, 13 => Role::Image, 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, 41 => Role::Menu, 42 => Role::ProgressBar, 43 => Role::Button, 44 => Role::RadioButton, 45 => Role::RadioButton, 47 => Role::TableCell, 48 => Role::ScrollBar, 49 => Role::Group, 50 => Role::Separator, 51 => Role::Slider, 52 => Role::SpinButton, 53 => Role::SplitGroup, 54 => Role::Status, 55 => Role::Table, 56 => Role::TableCell, 57 => Role::TableCell, 58 => Role::TableCell, 59 => Role::MenuItem, 60 => Role::TextArea, 61 => Role::TextArea, 62 => Role::Switch, 63 => Role::Toolbar, 64 => Role::Tooltip, 65 => Role::Group, 66 => Role::Table, 67 => Role::Unknown, 68 => Role::Group, 69 => Role::Window, 71 => Role::Group, 72 => Role::Group, 73 => Role::Group, 75 => Role::Application, 78 => Role::TextArea, 79 => Role::TextField, 81 => Role::StaticText, 82 => Role::WebArea, 83 => Role::Heading, 85 => Role::Group, 86 => Role::Group, 87 => Role::Group, 88 => Role::Link, 90 => Role::TableRow, 91 => Role::TreeItem, 92 => Role::WebArea, 93 => Role::WebArea, 94 => Role::WebArea, 95 => Role::WebArea, 96 => Role::WebArea, 97 => Role::Group, 98 => Role::List, 99 => Role::Group, 101 => Role::Alert, 102 => Role::Alert, 103 => Role::ProgressBar, 104 => Role::Group, 105 => Role::Group, 110 => Role::Navigation, 116 => Role::StaticText, 129 => Role::Button, _ => xa11y_core::unknown_role(&format!("AT-SPI role number {role}")),
2985 }
2986}
2987
2988const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
2992
2993const GTK_FALLBACK_MAX_NODES: usize = 200;
2996
2997fn is_actionable_atspi_role(role: &str) -> bool {
3002 matches!(
3003 role,
3004 "push button"
3005 | "toggle button"
3006 | "check box"
3007 | "radio button"
3008 | "menu item"
3009 | "check menu item"
3010 | "radio menu item"
3011 | "link"
3012 | "page tab"
3013 | "tab"
3014 | "list item"
3015 | "tree item"
3016 )
3017}
3018
3019fn is_never_descend_atspi_role(role: &str) -> bool {
3024 matches!(
3025 role,
3026 "label" | "separator" | "image" | "icon" | "static" | "caption"
3027 )
3028}
3029
3030fn is_dock_frame(role_number: u32, attributes: &HashMap<String, String>) -> bool {
3044 role_number == ATSPI_ROLE_FRAME && attributes.get("window-type").is_some_and(|t| t == "dock")
3045}
3046
3047fn children_call_error(err: zbus::Error, aref: &AccessibleRef) -> Error {
3052 match &err {
3053 zbus::Error::MethodError(name, ..)
3054 if name.as_str() == "org.freedesktop.DBus.Error.UnknownObject" =>
3055 {
3056 Error::ElementStale {
3057 selector: format!("{}{}", aref.bus_name, aref.path),
3058 }
3059 }
3060 _ => Error::Platform {
3061 code: -1,
3062 message: format!("GetChildren failed: {err}"),
3063 },
3064 }
3065}
3066
3067const ABSENT_MEMBER_ERRORS: [&str; 3] = [
3076 "org.freedesktop.DBus.Error.UnknownMethod",
3077 "org.freedesktop.DBus.Error.UnknownInterface",
3078 "org.freedesktop.DBus.Error.UnknownObject",
3079];
3080
3081const PEER_GONE_ERRORS: [&str; 2] = [
3096 "org.freedesktop.DBus.Error.ServiceUnknown",
3097 "org.freedesktop.DBus.Error.NameHasNoOwner",
3098];
3099
3100fn is_peer_gone(err: &zbus::Error) -> bool {
3106 match err {
3107 zbus::Error::MethodError(name, ..) => PEER_GONE_ERRORS.contains(&name.as_str()),
3108 _ => false,
3109 }
3110}
3111
3112fn is_absent_member(err: &zbus::Error) -> bool {
3121 match err {
3122 zbus::Error::MethodError(name, ..) => ABSENT_MEMBER_ERRORS.contains(&name.as_str()),
3123 _ => false,
3124 }
3125}
3126
3127fn classify_failure(app: &ElementData, top: &AccessibleRef, err: Error) -> Error {
3135 let name = app.name.as_deref().unwrap_or("<unnamed>");
3136 let pid = app.pid.map(|p| format!(" pid={p}")).unwrap_or_default();
3137 Error::Platform {
3138 code: match &err {
3139 Error::Platform { code, .. } => *code,
3140 _ => -1,
3141 },
3142 message: format!(
3143 "could not classify top-level {} of application \"{name}\"{pid} as a shell \
3144 surface: {err}",
3145 top.path
3146 ),
3147 }
3148}
3149
3150fn map_atspi_action_name(action_name: &str) -> Option<String> {
3164 let lower = action_name.to_lowercase();
3173 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
3174 let canonical = match collapsed.as_str() {
3175 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
3176 "toggle" | "check" | "uncheck" => "toggle",
3177 "expand" | "open" => "expand",
3178 "collapse" | "close" => "collapse",
3179 "select" => "select",
3180 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
3181 "increment" => "increment",
3182 "decrement" => "decrement",
3183 _ => return None,
3184 };
3185 Some(canonical.to_string())
3186}
3187
3188fn refine_menu_role(role: Role, actions: &[String]) -> Role {
3196 if role == Role::Menu && actions.iter().any(|action| action == "press") {
3197 Role::MenuItem
3198 } else {
3199 role
3200 }
3201}
3202
3203#[cfg(test)]
3204mod tests {
3205 use super::*;
3206
3207 #[test]
3208 fn test_role_mapping() {
3209 assert_eq!(map_atspi_role("push button"), Role::Button);
3210 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
3211 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
3212 assert_eq!(map_atspi_role("entry"), Role::TextField);
3213 assert_eq!(map_atspi_role("label"), Role::StaticText);
3214 assert_eq!(map_atspi_role("window"), Role::Window);
3215 assert_eq!(map_atspi_role("frame"), Role::Window);
3216 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
3217 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
3218 assert_eq!(map_atspi_role("slider"), Role::Slider);
3219 assert_eq!(map_atspi_role("panel"), Role::Group);
3220 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
3221 }
3222
3223 #[test]
3224 fn menu_with_native_activation_is_a_menu_item() {
3225 assert_eq!(
3226 refine_menu_role(Role::Menu, &["press".to_string()]),
3227 Role::MenuItem
3228 );
3229 }
3230
3231 #[test]
3232 fn non_activatable_menu_stays_a_container() {
3233 assert_eq!(refine_menu_role(Role::Menu, &[]), Role::Menu);
3234 assert_eq!(
3235 refine_menu_role(Role::Menu, &["show_menu".to_string()]),
3236 Role::Menu
3237 );
3238 }
3239
3240 #[test]
3241 fn activation_does_not_change_other_roles() {
3242 assert_eq!(
3243 refine_menu_role(Role::MenuBar, &["press".to_string()]),
3244 Role::MenuBar
3245 );
3246 assert_eq!(
3247 refine_menu_role(Role::MenuItem, &["press".to_string()]),
3248 Role::MenuItem
3249 );
3250 }
3251
3252 #[test]
3253 fn test_numeric_role_mapping() {
3254 assert_eq!(map_atspi_role_number(62), Role::Switch);
3257 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
3260 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
3262
3263 #[test]
3264 fn grab_focus_notsupported_is_action_not_supported() {
3265 let declined = grab_focus_error(
3271 "org.freedesktop.DBus.Error.NotSupported".into(),
3272 "focusing",
3273 "focus",
3274 Role::Button,
3275 );
3276 match declined {
3277 Error::ActionNotSupported { action, role } => {
3278 assert_eq!(action, "focus");
3279 assert_eq!(role, Role::Button);
3280 }
3281 other => panic!("expected ActionNotSupported, got {other:?}"),
3282 }
3283
3284 let broken = grab_focus_error(
3285 "org.freedesktop.DBus.Error.UnknownMethod".into(),
3286 "activating",
3287 "activate",
3288 Role::Window,
3289 );
3290 match broken {
3291 Error::Platform { code, .. } => assert_eq!(code, -1),
3292 other => panic!("expected Platform error, got {other:?}"),
3293 }
3294 }
3295
3296 const ATSPI_ROLE_ENUM: &[(u32, &str)] = &[
3300 (0, "invalid"),
3301 (1, "accelerator label"),
3302 (2, "alert"),
3303 (3, "animation"),
3304 (4, "arrow"),
3305 (5, "calendar"),
3306 (6, "canvas"),
3307 (7, "check box"),
3308 (8, "check menu item"),
3309 (9, "color chooser"),
3310 (10, "column header"),
3311 (11, "combo box"),
3312 (12, "date editor"),
3313 (13, "desktop icon"),
3314 (14, "desktop frame"),
3315 (15, "dial"),
3316 (16, "dialog"),
3317 (17, "directory pane"),
3318 (18, "drawing area"),
3319 (19, "file chooser"),
3320 (20, "filler"),
3321 (21, "focus traversable"),
3322 (22, "font chooser"),
3323 (23, "frame"),
3324 (24, "glass pane"),
3325 (25, "html container"),
3326 (26, "icon"),
3327 (27, "image"),
3328 (28, "internal frame"),
3329 (29, "label"),
3330 (30, "layered pane"),
3331 (31, "list"),
3332 (32, "list item"),
3333 (33, "menu"),
3334 (34, "menu bar"),
3335 (35, "menu item"),
3336 (36, "option pane"),
3337 (37, "page tab"),
3338 (38, "page tab list"),
3339 (39, "panel"),
3340 (40, "password text"),
3341 (41, "popup menu"),
3342 (42, "progress bar"),
3343 (43, "push button"),
3344 (44, "radio button"),
3345 (45, "radio menu item"),
3346 (46, "root pane"),
3347 (47, "row header"),
3348 (48, "scroll bar"),
3349 (49, "scroll pane"),
3350 (50, "separator"),
3351 (51, "slider"),
3352 (52, "spin button"),
3353 (53, "split pane"),
3354 (54, "status bar"),
3355 (55, "table"),
3356 (56, "table cell"),
3357 (57, "table column header"),
3358 (58, "table row header"),
3359 (59, "tearoff menu item"),
3360 (60, "terminal"),
3361 (61, "text"),
3362 (62, "toggle button"),
3363 (63, "tool bar"),
3364 (64, "tool tip"),
3365 (65, "tree"),
3366 (66, "tree table"),
3367 (67, "unknown"),
3368 (68, "viewport"),
3369 (69, "window"),
3370 (70, "extended"),
3371 (71, "header"),
3372 (72, "footer"),
3373 (73, "paragraph"),
3374 (74, "ruler"),
3375 (75, "application"),
3376 (76, "autocomplete"),
3377 (77, "editbar"),
3378 (78, "embedded"),
3379 (79, "entry"),
3380 (80, "chart"),
3381 (81, "caption"),
3382 (82, "document frame"),
3383 (83, "heading"),
3384 (84, "page"),
3385 (85, "section"),
3386 (86, "redundant object"),
3387 (87, "form"),
3388 (88, "link"),
3389 (89, "input method window"),
3390 (90, "table row"),
3391 (91, "tree item"),
3392 (92, "document spreadsheet"),
3393 (93, "document presentation"),
3394 (94, "document text"),
3395 (95, "document web"),
3396 (96, "document email"),
3397 (97, "comment"),
3398 (98, "list box"),
3399 (99, "grouping"),
3400 (100, "image map"),
3401 (101, "notification"),
3402 (102, "info bar"),
3403 (103, "level bar"),
3404 (104, "title bar"),
3405 (105, "block quote"),
3406 (106, "audio"),
3407 (107, "video"),
3408 (108, "definition"),
3409 (109, "article"),
3410 (110, "landmark"),
3411 (111, "log"),
3412 (112, "marquee"),
3413 (113, "math"),
3414 (114, "rating"),
3415 (115, "timer"),
3416 (116, "static"),
3417 (117, "math fraction"),
3418 (118, "math root"),
3419 (119, "subscript"),
3420 (120, "superscript"),
3421 (121, "description list"),
3422 (122, "description term"),
3423 (123, "description value"),
3424 (124, "footnote"),
3425 (125, "content deletion"),
3426 (126, "content insertion"),
3427 (127, "mark"),
3428 (128, "suggestion"),
3429 (129, "push button menu"),
3430 ];
3431
3432 #[test]
3433 fn numeric_and_name_role_maps_agree() {
3434 for &(num, name) in ATSPI_ROLE_ENUM {
3441 let by_num = map_atspi_role_number(num);
3442 let by_name = map_atspi_role(name);
3443 assert_eq!(
3444 by_num, by_name,
3445 "AT-SPI role {num} ({name:?}) diverges: numeric map gives \
3446 {by_num:?}, name map gives {by_name:?}"
3447 );
3448 }
3449 }
3450
3451 #[test]
3452 fn test_action_name_mapping() {
3453 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
3454 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
3455 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
3456 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
3457 assert_eq!(
3459 map_atspi_action_name("doDefault"),
3460 Some("press".to_string())
3461 );
3462 assert_eq!(
3463 map_atspi_action_name("do_default"),
3464 Some("press".to_string())
3465 );
3466 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
3467 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
3468 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
3469 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
3470 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
3471 assert_eq!(
3472 map_atspi_action_name("collapse"),
3473 Some("collapse".to_string())
3474 );
3475 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
3476 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
3477 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
3478 assert_eq!(
3479 map_atspi_action_name("showmenu"),
3480 Some("show_menu".to_string())
3481 );
3482 assert_eq!(
3483 map_atspi_action_name("popup"),
3484 Some("show_menu".to_string())
3485 );
3486 assert_eq!(
3487 map_atspi_action_name("show menu"),
3488 Some("show_menu".to_string())
3489 );
3490 assert_eq!(
3493 map_atspi_action_name("showContextMenu"),
3494 Some("show_menu".to_string())
3495 );
3496 assert_eq!(
3497 map_atspi_action_name("show_context_menu"),
3498 Some("show_menu".to_string())
3499 );
3500 assert_eq!(
3501 map_atspi_action_name("increment"),
3502 Some("increment".to_string())
3503 );
3504 assert_eq!(
3505 map_atspi_action_name("decrement"),
3506 Some("decrement".to_string())
3507 );
3508 assert_eq!(map_atspi_action_name("foobar"), None);
3509 }
3510
3511 #[test]
3514 fn test_action_name_aliases_roundtrip() {
3515 let atspi_names = [
3516 "click",
3517 "activate",
3518 "press",
3519 "invoke",
3520 "doDefault",
3521 "do_default",
3522 "toggle",
3523 "check",
3524 "uncheck",
3525 "expand",
3526 "open",
3527 "collapse",
3528 "close",
3529 "select",
3530 "menu",
3531 "showmenu",
3532 "showContextMenu",
3533 "show_context_menu",
3534 "popup",
3535 "show menu",
3536 "increment",
3537 "decrement",
3538 ];
3539 for name in atspi_names {
3540 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
3541 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
3542 });
3543 let back = map_atspi_action_name(&canonical)
3545 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
3546 assert_eq!(
3547 canonical, back,
3548 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
3549 name, canonical, back, canonical
3550 );
3551 }
3552 }
3553
3554 #[test]
3556 fn test_action_name_case_insensitive() {
3557 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
3558 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
3559 assert_eq!(
3560 map_atspi_action_name("Increment"),
3561 Some("increment".to_string())
3562 );
3563 }
3564
3565 #[test]
3570 fn test_gtk_fallback_actionable_roles() {
3571 for role in [
3572 "push button",
3573 "toggle button",
3574 "check box",
3575 "radio button",
3576 "menu item",
3577 "link",
3578 "tab",
3579 "list item",
3580 "tree item",
3581 ] {
3582 assert!(
3583 is_actionable_atspi_role(role),
3584 "{role:?} should be actionable"
3585 );
3586 }
3587 }
3588
3589 #[test]
3594 fn test_gtk_fallback_non_actionable_roles() {
3595 for role in [
3596 "label",
3597 "panel",
3598 "filler",
3599 "section",
3600 "group",
3601 "image",
3602 "separator",
3603 "static",
3604 "frame",
3605 "window",
3606 ] {
3607 assert!(
3608 !is_actionable_atspi_role(role),
3609 "{role:?} must not be treated as actionable"
3610 );
3611 }
3612 }
3613
3614 #[test]
3618 fn test_gtk_fallback_never_descend_roles() {
3619 for role in ["label", "separator", "image", "icon", "static", "caption"] {
3620 assert!(
3621 is_never_descend_atspi_role(role),
3622 "{role:?} must block BFS descent"
3623 );
3624 }
3625 for role in ["panel", "filler", "section", "group", "frame"] {
3628 assert!(
3629 !is_never_descend_atspi_role(role),
3630 "container role {role:?} must remain descendable"
3631 );
3632 }
3633 }
3634
3635 fn attrs(pairs: &[(&str, &str)]) -> HashMap<String, String> {
3636 pairs
3637 .iter()
3638 .map(|(k, v)| (k.to_string(), v.to_string()))
3639 .collect()
3640 }
3641
3642 #[test]
3646 fn test_dock_frame_detection() {
3647 assert!(is_dock_frame(
3648 ATSPI_ROLE_FRAME,
3649 &attrs(&[("toolkit", "GTK"), ("window-type", "dock")])
3650 ));
3651 assert!(!is_dock_frame(
3653 ATSPI_ROLE_FRAME,
3654 &attrs(&[("window-type", "normal")])
3655 ));
3656 assert!(!is_dock_frame(
3658 ATSPI_ROLE_FRAME,
3659 &attrs(&[("toolkit", "GTK")])
3660 ));
3661 assert!(!is_dock_frame(ATSPI_ROLE_FRAME, &HashMap::new()));
3662 }
3663
3664 #[test]
3668 fn test_frame_role_number_is_the_atspi_frame_role() {
3669 let (number, name) = ATSPI_ROLE_ENUM
3670 .iter()
3671 .find(|(_, name)| *name == "frame")
3672 .copied()
3673 .expect("the AtspiRole table must carry `frame`");
3674 assert_eq!(number, ATSPI_ROLE_FRAME, "{name} moved");
3675 }
3676
3677 #[test]
3683 fn test_dock_frame_requires_frame_role() {
3684 let dock = attrs(&[("window-type", "dock")]);
3685 for (number, name) in ATSPI_ROLE_ENUM
3686 .iter()
3687 .filter(|(number, _)| *number != ATSPI_ROLE_FRAME)
3688 {
3689 assert!(
3690 !is_dock_frame(*number, &dock),
3691 "role {number} ({name:?}) must not be reported as a shell surface"
3692 );
3693 }
3694 }
3695
3696 fn method_error(name: &str) -> zbus::Error {
3699 let call = zbus::Message::method_call("/org/a11y/atspi/accessible/7", "GetAttributes")
3700 .expect("method-call builder")
3701 .serial(std::num::NonZeroU32::new(1).expect("nonzero"))
3702 .build(&())
3703 .expect("build method call");
3704 let reply = zbus::Message::error(&call.header(), name)
3705 .expect("error builder")
3706 .serial(std::num::NonZeroU32::new(2).expect("nonzero"))
3707 .build(&"boom")
3708 .expect("build error reply");
3709 zbus::Error::from(reply)
3710 }
3711
3712 #[test]
3713 fn children_errors_classify_only_unknown_object_as_stale() {
3714 let aref = AccessibleRef {
3715 bus_name: ":1.42".into(),
3716 path: "/org/a11y/atspi/accessible/7".into(),
3717 };
3718 let err = children_call_error(
3719 method_error("org.freedesktop.DBus.Error.UnknownObject"),
3720 &aref,
3721 );
3722 assert!(matches!(err, Error::ElementStale { selector }
3723 if selector.contains(&aref.bus_name) && selector.contains(&aref.path)));
3724 for name in [
3725 "org.freedesktop.DBus.Error.UnknownMethod",
3726 "org.freedesktop.DBus.Error.UnknownInterface",
3727 "org.freedesktop.DBus.Error.NoReply",
3728 "org.freedesktop.DBus.Error.Timeout",
3729 "org.freedesktop.DBus.Error.AccessDenied",
3730 "org.freedesktop.DBus.Error.ServiceUnknown",
3731 "org.freedesktop.DBus.Error.NameHasNoOwner",
3732 ] {
3733 assert!(
3734 matches!(children_call_error(method_error(name), &aref),
3735 Error::Platform { message, .. } if message.contains(name)),
3736 "{name}"
3737 );
3738 }
3739 assert!(matches!(
3741 children_call_error(
3742 zbus::Error::Failure("org.freedesktop.DBus.Error.UnknownObject".into()),
3743 &aref
3744 ),
3745 Error::Platform { .. }
3746 ));
3747 }
3748
3749 struct LiveTreePeer {
3753 name: &'static str,
3754 role: &'static str,
3755 children: Vec<&'static str>,
3756 children_error: Option<&'static str>,
3757 }
3758
3759 #[zbus::interface(name = "org.a11y.atspi.Accessible")]
3760 impl LiveTreePeer {
3761 fn get_children(
3762 &self,
3763 ) -> zbus::fdo::Result<Vec<(String, zbus::zvariant::OwnedObjectPath)>> {
3764 if let Some(error) = self.children_error {
3765 return Err(match error {
3766 "UnknownObject" => zbus::fdo::Error::UnknownObject("destroyed".into()),
3767 "NoReply" => zbus::fdo::Error::NoReply("unresponsive".into()),
3768 _ => panic!("unsupported test error"),
3769 });
3770 }
3771 Ok(self
3772 .children
3773 .iter()
3774 .map(|path| {
3775 (
3776 "org.test.LiveTree".into(),
3777 zbus::zvariant::OwnedObjectPath::try_from(*path).expect("test path"),
3778 )
3779 })
3780 .collect())
3781 }
3782
3783 fn get_role_name(&self) -> &str {
3784 self.role
3785 }
3786
3787 #[zbus(property)]
3788 fn name(&self) -> &str {
3789 self.name
3790 }
3791 }
3792
3793 fn live_tree_peer(
3794 error: &'static str,
3795 vanished_role: &'static str,
3796 ) -> (Connection, std::sync::Arc<LinuxProvider>, ElementData) {
3797 let (server_socket, client_socket) =
3798 std::os::unix::net::UnixStream::pair().expect("socket pair");
3799 let server = std::thread::spawn(move || {
3800 zbus::blocking::connection::Builder::async_io_unix_stream(server_socket)
3801 .server(zbus::Guid::generate())
3802 .expect("server GUID")
3803 .p2p()
3804 .serve_at(
3805 "/root",
3806 LiveTreePeer {
3807 name: "App",
3808 role: "application",
3809 children: vec!["/vanished", "/survivor"],
3810 children_error: None,
3811 },
3812 )
3813 .expect("root interface")
3814 .serve_at(
3815 "/vanished",
3816 LiveTreePeer {
3817 name: "Vanished",
3818 role: vanished_role,
3819 children: vec![],
3820 children_error: Some(error),
3821 },
3822 )
3823 .expect("vanished interface")
3824 .serve_at(
3825 "/survivor",
3826 LiveTreePeer {
3827 name: "Survivor",
3828 role: "push button",
3829 children: vec![],
3830 children_error: None,
3831 },
3832 )
3833 .expect("survivor interface")
3834 .build()
3835 .expect("server connection")
3836 });
3837 let connection = zbus::blocking::connection::Builder::async_io_unix_stream(client_socket)
3838 .p2p()
3839 .build()
3840 .expect("client connection");
3841 let server = server.join().expect("server thread");
3842 let provider = std::sync::Arc::new(LinuxProvider {
3843 a11y_bus: connection,
3844 handle_cache: Mutex::new(HashMap::new()),
3845 action_indices: Mutex::new(HashMap::new()),
3846 window_manager: WindowManager::new(),
3847 });
3848 let root_ref = AccessibleRef {
3849 bus_name: "org.test.LiveTree".into(),
3850 path: "/root".into(),
3851 };
3852 let root = provider.build_element_data(&root_ref, None);
3853 (server, provider, root)
3854 }
3855
3856 #[test]
3857 fn dump_tree_and_locators_survive_a_vanished_subtree() {
3858 let (_server, provider, root) = live_tree_peer("UnknownObject", "panel");
3859 let app = xa11y_core::App::from_data(provider.clone(), root);
3860 let tree = app.tree(None).expect("tree prunes vanished descendants");
3861 assert!(tree
3862 .children
3863 .iter()
3864 .any(|child| child.name.as_deref() == Some("Survivor")));
3865 let dump = app.dump(None).expect("dump survives UnknownObject");
3866 assert!(dump.contains("Survivor"));
3867 assert_eq!(
3868 app.locator("button")
3869 .elements()
3870 .expect("locator survives")
3871 .len(),
3872 1
3873 );
3874 let vanished = app
3875 .children()
3876 .expect("children")
3877 .into_iter()
3878 .find(|child| child.name.as_deref() == Some("Vanished"))
3879 .expect("vanished snapshot");
3880 assert!(vanished
3881 .children()
3882 .expect("destroyed node has no live children")
3883 .is_empty());
3884 assert!(matches!(
3885 provider.get_atspi_children(&AccessibleRef {
3886 bus_name: "org.test.LiveTree".into(),
3887 path: "/vanished".into(),
3888 }),
3889 Err(Error::ElementStale { .. })
3890 ));
3891 }
3892
3893 #[test]
3894 fn nested_application_disappearance_does_not_drop_live_siblings() {
3895 let (_server, provider, root) = live_tree_peer("UnknownObject", "application");
3896 let app = xa11y_core::App::from_data(provider, root);
3897 let dump = app.dump(None).expect("nested application pruned");
3898 assert!(dump.contains("Survivor"));
3899 assert!(!dump.contains("Vanished"));
3900 }
3901
3902 #[test]
3903 fn dump_and_tree_preserve_real_children_failures() {
3904 let (_server, provider, root) = live_tree_peer("NoReply", "panel");
3905 let app = xa11y_core::App::from_data(provider, root);
3906 for result in [app.tree(None).map(|_| ()), app.dump(None).map(|_| ())] {
3907 assert!(matches!(result, Err(Error::Platform { message, .. })
3908 if message.contains("NoReply")));
3909 }
3910 }
3911
3912 #[test]
3918 fn an_unimplemented_member_reads_as_absence() {
3919 for name in ABSENT_MEMBER_ERRORS {
3920 assert!(
3921 is_absent_member(&method_error(name)),
3922 "{name} must read as absence"
3923 );
3924 }
3925 }
3926
3927 #[test]
3935 fn a_departed_peer_is_absence_not_failure() {
3936 for name in [
3937 "org.freedesktop.DBus.Error.ServiceUnknown",
3938 "org.freedesktop.DBus.Error.NameHasNoOwner",
3939 ] {
3940 assert!(
3941 is_peer_gone(&method_error(name)),
3942 "{name} means the peer left"
3943 );
3944 assert!(!is_absent_member(&method_error(name)), "{name}");
3947 }
3948 }
3949
3950 #[test]
3951 fn a_hung_or_broken_peer_is_still_a_failure() {
3952 for name in [
3953 "org.freedesktop.DBus.Error.NoReply",
3954 "org.freedesktop.DBus.Error.Timeout",
3955 "org.freedesktop.DBus.Error.Disconnected",
3956 "org.freedesktop.DBus.Error.AccessDenied",
3957 "org.freedesktop.DBus.Error.UnknownMethod",
3958 ] {
3959 assert!(
3960 !is_peer_gone(&method_error(name)),
3961 "{name} must not be read as the peer having left"
3962 );
3963 }
3964 assert!(!is_peer_gone(&zbus::Error::InvalidReply));
3965 }
3966
3967 #[test]
3968 fn other_dbus_failures_are_not_absence() {
3969 for name in [
3970 "org.freedesktop.DBus.Error.ServiceUnknown",
3971 "org.freedesktop.DBus.Error.NoReply",
3972 "org.freedesktop.DBus.Error.Timeout",
3973 "org.freedesktop.DBus.Error.Disconnected",
3974 "org.freedesktop.DBus.Error.AccessDenied",
3975 "org.freedesktop.DBus.Error.UnknownProperty",
3976 ] {
3977 assert!(
3978 !is_absent_member(&method_error(name)),
3979 "{name} must stay a failure"
3980 );
3981 }
3982 assert!(!is_absent_member(&zbus::Error::InvalidReply));
3984 }
3985
3986 #[test]
3991 fn a_classification_failure_names_the_app_and_the_top_level() {
3992 let mut app = ElementData::for_role(Role::Application);
3993 app.name = Some("xfce4-panel".to_string());
3994 app.pid = Some(4242);
3995 let top = AccessibleRef {
3996 bus_name: ":1.42".to_string(),
3997 path: "/org/a11y/atspi/accessible/7".to_string(),
3998 };
3999 let err = classify_failure(
4000 &app,
4001 &top,
4002 Error::Platform {
4003 code: -1,
4004 message: "GetRole failed: connection closed".to_string(),
4005 },
4006 );
4007 let Error::Platform { code, message } = err else {
4008 panic!("a D-Bus failure must stay a platform error");
4009 };
4010 assert_eq!(code, -1);
4011 assert!(message.contains("xfce4-panel"), "{message}");
4012 assert!(message.contains("pid=4242"), "{message}");
4013 assert!(
4014 message.contains("/org/a11y/atspi/accessible/7"),
4015 "{message}"
4016 );
4017 assert!(message.contains("GetRole failed"), "{message}");
4019 }
4020
4021 #[test]
4024 fn a_classification_failure_survives_an_unnamed_app() {
4025 let app = ElementData::for_role(Role::Application);
4026 let top = AccessibleRef {
4027 bus_name: ":1.9".to_string(),
4028 path: "/org/a11y/atspi/accessible/3".to_string(),
4029 };
4030 let err = classify_failure(
4031 &app,
4032 &top,
4033 Error::Platform {
4034 code: -1,
4035 message: "GetAttributes failed".to_string(),
4036 },
4037 );
4038 let msg = err.to_string();
4039 assert!(msg.contains("<unnamed>"), "{msg}");
4040 assert!(msg.contains("/org/a11y/atspi/accessible/3"), "{msg}");
4041 }
4042
4043 #[derive(Default)]
4046 struct InterfaceProbes {
4047 text: std::sync::atomic::AtomicUsize,
4048 value: std::sync::atomic::AtomicUsize,
4049 }
4050
4051 impl InterfaceProbes {
4052 fn text(&self) -> usize {
4053 self.text.load(Ordering::SeqCst)
4054 }
4055 fn value(&self) -> usize {
4056 self.value.load(Ordering::SeqCst)
4057 }
4058 }
4059
4060 struct InterfacePeer {
4062 name: &'static str,
4063 role: &'static str,
4064 children: Vec<&'static str>,
4065 interfaces: Vec<&'static str>,
4066 }
4067
4068 #[zbus::interface(name = "org.a11y.atspi.Accessible")]
4069 impl InterfacePeer {
4070 fn get_children(&self) -> Vec<(String, zbus::zvariant::OwnedObjectPath)> {
4071 self.children
4072 .iter()
4073 .map(|path| {
4074 (
4075 "org.test.Ifaces".into(),
4076 zbus::zvariant::OwnedObjectPath::try_from(*path).expect("test path"),
4077 )
4078 })
4079 .collect()
4080 }
4081
4082 fn get_role_name(&self) -> &str {
4083 self.role
4084 }
4085
4086 fn get_interfaces(&self) -> Vec<String> {
4087 self.interfaces.iter().map(|s| s.to_string()).collect()
4088 }
4089
4090 #[zbus(property)]
4091 fn name(&self) -> &str {
4092 self.name
4093 }
4094 }
4095
4096 struct FakeText {
4100 text: &'static str,
4101 probes: std::sync::Arc<InterfaceProbes>,
4102 }
4103
4104 #[zbus::interface(name = "org.a11y.atspi.Text")]
4105 impl FakeText {
4106 fn get_text(&self, start: i32, end: i32) -> String {
4107 self.probes.text.fetch_add(1, Ordering::SeqCst);
4108 self.text
4109 .chars()
4110 .skip(start.max(0) as usize)
4111 .take((end - start).max(0) as usize)
4112 .collect()
4113 }
4114
4115 #[zbus(property)]
4116 fn character_count(&self) -> i32 {
4117 self.probes.text.fetch_add(1, Ordering::SeqCst);
4118 self.text.chars().count() as i32
4119 }
4120 }
4121
4122 struct FakeValue {
4123 value: f64,
4124 probes: std::sync::Arc<InterfaceProbes>,
4125 }
4126
4127 #[zbus::interface(name = "org.a11y.atspi.Value")]
4128 impl FakeValue {
4129 #[zbus(property)]
4130 fn current_value(&self) -> f64 {
4131 self.probes.value.fetch_add(1, Ordering::SeqCst);
4132 self.value
4133 }
4134
4135 #[zbus(property)]
4136 fn minimum_value(&self) -> f64 {
4137 self.probes.value.fetch_add(1, Ordering::SeqCst);
4138 0.0
4139 }
4140
4141 #[zbus(property)]
4142 fn maximum_value(&self) -> f64 {
4143 self.probes.value.fetch_add(1, Ordering::SeqCst);
4144 100.0
4145 }
4146 }
4147
4148 struct InterfaceFixture {
4149 _server: Connection,
4150 provider: std::sync::Arc<LinuxProvider>,
4151 root: ElementData,
4152 plain: std::sync::Arc<InterfaceProbes>,
4154 entry: std::sync::Arc<InterfaceProbes>,
4157 slider: std::sync::Arc<InterfaceProbes>,
4159 }
4160
4161 fn interface_peer() -> InterfaceFixture {
4162 const ACC: &str = "org.a11y.atspi.Accessible";
4163 const TEXT: &str = "org.a11y.atspi.Text";
4164 const VALUE: &str = "org.a11y.atspi.Value";
4165 let plain = std::sync::Arc::new(InterfaceProbes::default());
4166 let entry = std::sync::Arc::new(InterfaceProbes::default());
4167 let slider = std::sync::Arc::new(InterfaceProbes::default());
4168 let (server_socket, client_socket) =
4169 std::os::unix::net::UnixStream::pair().expect("socket pair");
4170 let objects = [
4171 (
4172 "/plain",
4173 "push button",
4174 "OK",
4175 vec![ACC],
4176 "",
4177 7.0,
4178 plain.clone(),
4179 ),
4180 (
4181 "/entry",
4182 "entry",
4183 "Entry",
4184 vec![ACC, TEXT, VALUE],
4185 "hello",
4186 0.0,
4187 entry.clone(),
4188 ),
4189 (
4190 "/slider",
4191 "slider",
4192 "Volume",
4193 vec![ACC, VALUE],
4194 "",
4195 42.0,
4196 slider.clone(),
4197 ),
4198 ];
4199 let server = std::thread::spawn(move || {
4200 let mut builder =
4201 zbus::blocking::connection::Builder::async_io_unix_stream(server_socket)
4202 .server(zbus::Guid::generate())
4203 .expect("server GUID")
4204 .p2p()
4205 .serve_at(
4206 "/root",
4207 InterfacePeer {
4208 name: "App",
4209 role: "application",
4210 children: vec!["/plain", "/entry", "/slider"],
4211 interfaces: vec![ACC],
4212 },
4213 )
4214 .expect("root interface");
4215 for (path, role, name, interfaces, text, value, probes) in objects {
4216 builder = builder
4217 .serve_at(
4218 path,
4219 InterfacePeer {
4220 name,
4221 role,
4222 children: vec![],
4223 interfaces,
4224 },
4225 )
4226 .expect("accessible")
4227 .serve_at(
4228 path,
4229 FakeText {
4230 text,
4231 probes: probes.clone(),
4232 },
4233 )
4234 .expect("text")
4235 .serve_at(path, FakeValue { value, probes })
4236 .expect("value");
4237 }
4238 builder.build().expect("server connection")
4239 });
4240 let connection = zbus::blocking::connection::Builder::async_io_unix_stream(client_socket)
4241 .p2p()
4242 .build()
4243 .expect("client connection");
4244 let server = server.join().expect("server thread");
4245 let provider = std::sync::Arc::new(LinuxProvider {
4246 a11y_bus: connection,
4247 handle_cache: Mutex::new(HashMap::new()),
4248 action_indices: Mutex::new(HashMap::new()),
4249 window_manager: WindowManager::new(),
4250 });
4251 let root = provider.build_element_data(
4252 &AccessibleRef {
4253 bus_name: "org.test.Ifaces".into(),
4254 path: "/root".into(),
4255 },
4256 None,
4257 );
4258 InterfaceFixture {
4259 _server: server,
4260 provider,
4261 root,
4262 plain,
4263 entry,
4264 slider,
4265 }
4266 }
4267
4268 #[test]
4273 fn tree_reads_only_probe_interfaces_the_node_reports() {
4274 let fx = interface_peer();
4275 let app = xa11y_core::App::from_data(fx.provider.clone(), fx.root.clone());
4276 let tree = app.tree(None).expect("tree");
4277 let child = |name: &str| {
4278 tree.children
4279 .iter()
4280 .find(|c| c.name.as_deref() == Some(name))
4281 .unwrap_or_else(|| panic!("{name} in tree"))
4282 };
4283
4284 assert_eq!(child("OK").value, None);
4285 assert_eq!(fx.plain.text(), 0, "Text read on a node without Text");
4286 assert_eq!(fx.plain.value(), 0, "Value read on a node without Value");
4287
4288 assert_eq!(child("Entry").value.as_deref(), Some("hello"));
4290 assert!(fx.entry.text() > 0);
4291
4292 assert_eq!(child("Volume").value.as_deref(), Some("42"));
4293 let slider = app
4294 .children()
4295 .expect("children")
4296 .into_iter()
4297 .find(|c| c.name.as_deref() == Some("Volume"))
4298 .expect("slider element");
4299 assert_eq!(slider.numeric_value, Some(42.0));
4300 assert_eq!(slider.max_value, Some(100.0));
4301 assert_eq!(fx.slider.text(), 0, "Text read on a Value-only node");
4302
4303 assert_eq!(
4306 app.locator(r#"[value="hello"]"#)
4307 .elements()
4308 .expect("value locator")
4309 .len(),
4310 1
4311 );
4312 assert_eq!(
4313 fx.plain.text(),
4314 0,
4315 "matcher read Text on a node without Text"
4316 );
4317 assert_eq!(
4318 fx.plain.value(),
4319 0,
4320 "matcher read Value on a node without Value"
4321 );
4322 }
4323}