1use std::sync::Mutex;
4use std::time::Duration;
5
6use xa11y_core::{
7 Action, ActionData, AppInfo, AppTarget, CancelHandle, ElementState, Error, Event, EventFilter,
8 EventKind, EventProvider, EventReceiver, Node, PermissionStatus, Provider, QueryOptions, Rect,
9 Result, Role, ScrollDirection, StateSet, Subscription, Toggled, Tree,
10};
11use zbus::blocking::{Connection, Proxy};
12
13pub struct LinuxProvider {
15 a11y_bus: Connection,
16 cached_refs: Mutex<Vec<AccessibleRef>>,
18}
19
20#[derive(Debug, Clone)]
22struct AccessibleRef {
23 bus_name: String,
24 path: String,
25}
26
27impl LinuxProvider {
28 pub fn new() -> Result<Self> {
33 let a11y_bus = Self::connect_a11y_bus()?;
34 Ok(Self {
35 a11y_bus,
36 cached_refs: Mutex::new(Vec::new()),
37 })
38 }
39
40 fn connect_a11y_bus() -> Result<Connection> {
41 if let Ok(session) = Connection::session() {
45 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
46 .map_err(|e| Error::Platform {
47 code: -1,
48 message: format!("Failed to create a11y bus proxy: {}", e),
49 })?;
50
51 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
52 if let Ok(address) = addr_reply.body().deserialize::<String>() {
53 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
54 if let Ok(Ok(conn)) =
55 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
56 {
57 return Ok(conn);
58 }
59 }
60 }
61 }
62
63 return Ok(session);
65 }
66
67 Connection::session().map_err(|e| Error::Platform {
68 code: -1,
69 message: format!("Failed to connect to D-Bus session bus: {}", e),
70 })
71 }
72
73 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
74 Proxy::new(
75 &self.a11y_bus,
76 bus_name.to_owned(),
77 path.to_owned(),
78 interface.to_owned(),
79 )
80 .map_err(|e| Error::Platform {
81 code: -1,
82 message: format!("Failed to create proxy: {}", e),
83 })
84 }
85
86 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
89 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
90 Ok(p) => p,
91 Err(_) => return false,
92 };
93 let reply = match proxy.call_method("GetInterfaces", &()) {
94 Ok(r) => r,
95 Err(_) => return false,
96 };
97 let interfaces: Vec<String> = match reply.body().deserialize() {
98 Ok(v) => v,
99 Err(_) => return false,
100 };
101 interfaces.iter().any(|i| i.contains(iface))
102 }
103
104 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
106 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
107 let reply = proxy
108 .call_method("GetRole", &())
109 .map_err(|e| Error::Platform {
110 code: -1,
111 message: format!("GetRole failed: {}", e),
112 })?;
113 reply
114 .body()
115 .deserialize::<u32>()
116 .map_err(|e| Error::Platform {
117 code: -1,
118 message: format!("GetRole deserialize failed: {}", e),
119 })
120 }
121
122 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
124 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
125 let reply = proxy
126 .call_method("GetRoleName", &())
127 .map_err(|e| Error::Platform {
128 code: -1,
129 message: format!("GetRoleName failed: {}", e),
130 })?;
131 reply
132 .body()
133 .deserialize::<String>()
134 .map_err(|e| Error::Platform {
135 code: -1,
136 message: format!("GetRoleName deserialize failed: {}", e),
137 })
138 }
139
140 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
142 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
143 proxy
144 .get_property::<String>("Name")
145 .map_err(|e| Error::Platform {
146 code: -1,
147 message: format!("Get Name property failed: {}", e),
148 })
149 }
150
151 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
153 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
154 proxy
155 .get_property::<String>("Description")
156 .map_err(|e| Error::Platform {
157 code: -1,
158 message: format!("Get Description property failed: {}", e),
159 })
160 }
161
162 fn get_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
166 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
167 let reply = proxy
168 .call_method("GetChildren", &())
169 .map_err(|e| Error::Platform {
170 code: -1,
171 message: format!("GetChildren failed: {}", e),
172 })?;
173 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
174 reply.body().deserialize().map_err(|e| Error::Platform {
175 code: -1,
176 message: format!("GetChildren deserialize failed: {}", e),
177 })?;
178 Ok(children
179 .into_iter()
180 .map(|(bus_name, path)| AccessibleRef {
181 bus_name,
182 path: path.to_string(),
183 })
184 .collect())
185 }
186
187 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
189 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
190 let reply = proxy
191 .call_method("GetState", &())
192 .map_err(|e| Error::Platform {
193 code: -1,
194 message: format!("GetState failed: {}", e),
195 })?;
196 reply
197 .body()
198 .deserialize::<Vec<u32>>()
199 .map_err(|e| Error::Platform {
200 code: -1,
201 message: format!("GetState deserialize failed: {}", e),
202 })
203 }
204
205 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
209 if !self.has_interface(aref, "Component") {
210 return None;
211 }
212 let proxy = self
213 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
214 .ok()?;
215 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
218 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
219 if w <= 0 && h <= 0 {
220 return None;
221 }
222 Some(Rect {
223 x,
224 y,
225 width: w.max(0) as u32,
226 height: h.max(0) as u32,
227 })
228 }
229
230 fn get_actions(&self, aref: &AccessibleRef) -> Vec<Action> {
234 let mut actions = Vec::new();
235
236 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
238 if let Ok(n_actions) = proxy.get_property::<i32>("NActions") {
239 for i in 0..n_actions {
240 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
241 if let Ok(name) = reply.body().deserialize::<String>() {
242 if let Some(action) = map_atspi_action(&name) {
243 if !actions.contains(&action) {
244 actions.push(action);
245 }
246 }
247 }
248 }
249 }
250 }
251 }
252
253 if !actions.contains(&Action::Focus) {
255 if let Ok(proxy) =
256 self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
257 {
258 if proxy.call_method("GetExtents", &(0u32,)).is_ok() {
260 actions.push(Action::Focus);
261 }
262 }
263 }
264
265 actions
266 }
267
268 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
271 let text_value = self.get_text_content(aref);
275 if text_value.is_some() {
276 return text_value;
277 }
278 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
280 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
281 return Some(val.to_string());
282 }
283 }
284 None
285 }
286
287 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
289 let proxy = self
290 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
291 .ok()?;
292 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
293 if char_count > 0 {
294 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
295 let text: String = reply.body().deserialize().ok()?;
296 if !text.is_empty() {
297 return Some(text);
298 }
299 }
300 None
301 }
302
303 #[allow(clippy::too_many_arguments)]
305 #[allow(clippy::only_used_in_recursion)]
306 fn traverse(
307 &self,
308 aref: &AccessibleRef,
309 opts: &QueryOptions,
310 nodes: &mut Vec<Node>,
311 refs: &mut Vec<AccessibleRef>,
312 parent_idx: Option<u32>,
313 depth: u32,
314 screen_size: (u32, u32),
315 ) {
316 if let Some(max_depth) = opts.max_depth {
317 if depth > max_depth {
318 return;
319 }
320 }
321 if let Some(max_elements) = opts.max_elements {
322 if nodes.len() >= max_elements as usize {
323 return;
324 }
325 }
326
327 let role_name = self.get_role_name(aref).unwrap_or_default();
328 let role_num = self.get_role_number(aref).unwrap_or(0);
329 let role = if !role_name.is_empty() {
330 map_atspi_role(&role_name)
331 } else {
332 map_atspi_role_number(role_num)
333 };
334
335 let is_root = depth == 0;
338
339 let skip_for_role = if !is_root {
342 if let Some(ref filter_roles) = opts.roles {
343 !filter_roles.contains(&role)
344 } else {
345 false
346 }
347 } else {
348 false
349 };
350
351 if skip_for_role {
354 let children = self.get_children(aref).unwrap_or_default();
355 for child_ref in &children {
356 if let Some(max_elements) = opts.max_elements {
357 if nodes.len() >= max_elements as usize {
358 break;
359 }
360 }
361 if child_ref.path == "/org/a11y/atspi/null"
362 || child_ref.bus_name.is_empty()
363 || child_ref.path.is_empty()
364 {
365 continue;
366 }
367 self.traverse(
368 child_ref,
369 opts,
370 nodes,
371 refs,
372 parent_idx,
373 depth + 1,
374 screen_size,
375 );
376 }
377 return;
378 }
379
380 let mut name = self.get_name(aref).ok().filter(|s| !s.is_empty());
381 let description = self.get_description(aref).ok().filter(|s| !s.is_empty());
382 let value = self.get_value(aref);
383
384 if name.is_none() && role == Role::StaticText {
387 if let Some(ref v) = value {
388 name = Some(v.clone());
389 }
390 }
391 let bounds = self.get_extents(aref);
392 let states = self.parse_states(aref, role);
393 let actions = self.get_actions(aref);
394
395 if !is_root && opts.visible_only && !states.visible {
396 return;
397 }
398
399 let raw = {
400 let raw_role = if role_name.is_empty() {
401 format!("role_num:{}", role_num)
402 } else {
403 role_name
404 };
405 xa11y_core::RawPlatformData::Linux {
406 atspi_role: raw_role,
407 bus_name: aref.bus_name.clone(),
408 object_path: aref.path.clone(),
409 }
410 };
411
412 let (numeric_value, min_value, max_value) = if matches!(
413 role,
414 Role::Slider | Role::ProgressBar | Role::ScrollBar | Role::SpinButton
415 ) {
416 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
417 (
418 proxy.get_property::<f64>("CurrentValue").ok(),
419 proxy.get_property::<f64>("MinimumValue").ok(),
420 proxy.get_property::<f64>("MaximumValue").ok(),
421 )
422 } else {
423 (None, None, None)
424 }
425 } else {
426 (None, None, None)
427 };
428
429 let node_idx = nodes.len() as u32;
430 nodes.push(Node {
431 role,
432 name,
433 value,
434 description,
435 bounds,
436 actions,
437 states,
438 numeric_value,
439 min_value,
440 max_value,
441 stable_id: Some(aref.path.clone()),
442 raw,
443 index: node_idx,
444 children_indices: vec![], parent_index: parent_idx,
446 });
447 refs.push(aref.clone());
448
449 let children = self.get_children(aref).unwrap_or_default();
451 let mut child_ids = Vec::new();
452
453 for child_ref in &children {
454 if let Some(max_elements) = opts.max_elements {
455 if nodes.len() >= max_elements as usize {
456 break;
457 }
458 }
459 if child_ref.path == "/org/a11y/atspi/null"
461 || child_ref.bus_name.is_empty()
462 || child_ref.path.is_empty()
463 {
464 continue;
465 }
466 let child_idx = nodes.len() as u32;
467 child_ids.push(child_idx);
468 self.traverse(
469 child_ref,
470 opts,
471 nodes,
472 refs,
473 Some(node_idx),
474 depth + 1,
475 screen_size,
476 );
477 }
478
479 nodes[node_idx as usize].children_indices = child_ids;
481 }
482
483 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
485 let state_bits = self.get_state(aref).unwrap_or_default();
486
487 let bits: u64 = if state_bits.len() >= 2 {
489 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
490 } else if state_bits.len() == 1 {
491 state_bits[0] as u64
492 } else {
493 0
494 };
495
496 const BUSY: u64 = 1 << 3;
498 const CHECKED: u64 = 1 << 4;
499 const EDITABLE: u64 = 1 << 7;
500 const ENABLED: u64 = 1 << 8;
501 const EXPANDABLE: u64 = 1 << 9;
502 const EXPANDED: u64 = 1 << 10;
503 const FOCUSABLE: u64 = 1 << 11;
504 const FOCUSED: u64 = 1 << 12;
505 const MODAL: u64 = 1 << 16;
506 const SELECTED: u64 = 1 << 23;
507 const SENSITIVE: u64 = 1 << 24;
508 const SHOWING: u64 = 1 << 25;
509 const VISIBLE: u64 = 1 << 30;
510 const INDETERMINATE: u64 = 1 << 32;
511 const REQUIRED: u64 = 1 << 33;
512
513 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
514 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
515
516 let checked = match role {
517 Role::CheckBox | Role::RadioButton | Role::MenuItem => {
518 if (bits & INDETERMINATE) != 0 {
519 Some(Toggled::Mixed)
520 } else if (bits & CHECKED) != 0 {
521 Some(Toggled::On)
522 } else {
523 Some(Toggled::Off)
524 }
525 }
526 _ => None,
527 };
528
529 let expanded = if (bits & EXPANDABLE) != 0 {
530 Some((bits & EXPANDED) != 0)
531 } else {
532 None
533 };
534
535 StateSet {
536 enabled,
537 visible,
538 focused: (bits & FOCUSED) != 0,
539 checked,
540 selected: (bits & SELECTED) != 0,
541 expanded,
542 editable: (bits & EDITABLE) != 0,
543 focusable: (bits & FOCUSABLE) != 0,
544 modal: (bits & MODAL) != 0,
545 required: (bits & REQUIRED) != 0,
546 busy: (bits & BUSY) != 0,
547 }
548 }
549
550 fn detect_screen_size() -> (u32, u32) {
552 if let Ok(output) = std::process::Command::new("xdpyinfo").output() {
553 let stdout = String::from_utf8_lossy(&output.stdout);
554 for line in stdout.lines() {
555 let trimmed = line.trim();
556 if trimmed.starts_with("dimensions:") {
557 if let Some(dims) = trimmed.split_whitespace().nth(1) {
558 let parts: Vec<&str> = dims.split('x').collect();
559 if parts.len() == 2 {
560 if let (Ok(w), Ok(h)) = (parts[0].parse(), parts[1].parse()) {
561 return (w, h);
562 }
563 }
564 }
565 }
566 }
567 }
568 (1920, 1080)
569 }
570
571 fn find_app_by_name(&self, name: &str) -> Result<AccessibleRef> {
573 let registry = AccessibleRef {
574 bus_name: "org.a11y.atspi.Registry".to_string(),
575 path: "/org/a11y/atspi/accessible/root".to_string(),
576 };
577 let children = self.get_children(®istry)?;
578 let name_lower = name.to_lowercase();
579
580 for child in &children {
581 if child.path == "/org/a11y/atspi/null" {
582 continue;
583 }
584 if let Ok(app_name) = self.get_name(child) {
585 if app_name.to_lowercase().contains(&name_lower) {
586 return Ok(child.clone());
587 }
588 }
589 }
590
591 Err(Error::AppNotFound {
592 target: name.to_string(),
593 })
594 }
595
596 fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
598 let registry = AccessibleRef {
599 bus_name: "org.a11y.atspi.Registry".to_string(),
600 path: "/org/a11y/atspi/accessible/root".to_string(),
601 };
602 let children = self.get_children(®istry)?;
603
604 for child in &children {
605 if child.path == "/org/a11y/atspi/null" {
606 continue;
607 }
608 if let Ok(proxy) =
610 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
611 {
612 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
613 if app_pid as u32 == pid {
614 return Ok(child.clone());
615 }
616 }
617 }
618 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
620 if app_pid == pid {
621 return Ok(child.clone());
622 }
623 }
624 }
625
626 Err(Error::AppNotFound {
627 target: format!("PID {}", pid),
628 })
629 }
630
631 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
633 let proxy = self
634 .make_proxy(
635 "org.freedesktop.DBus",
636 "/org/freedesktop/DBus",
637 "org.freedesktop.DBus",
638 )
639 .ok()?;
640 let reply = proxy
641 .call_method("GetConnectionUnixProcessID", &(bus_name,))
642 .ok()?;
643 let pid: u32 = reply.body().deserialize().ok()?;
644 if pid > 0 {
645 Some(pid)
646 } else {
647 None
648 }
649 }
650
651 fn do_atspi_action(&self, aref: &AccessibleRef, action_name: &str) -> Result<()> {
653 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
654 let n_actions: i32 = proxy.get_property("NActions").unwrap_or(0);
655
656 for i in 0..n_actions {
657 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
658 if let Ok(name) = reply.body().deserialize::<String>() {
659 if name == action_name {
660 let _ =
661 proxy
662 .call_method("DoAction", &(i,))
663 .map_err(|e| Error::Platform {
664 code: -1,
665 message: format!("DoAction failed: {}", e),
666 })?;
667 return Ok(());
668 }
669 }
670 }
671 }
672
673 Err(Error::Platform {
674 code: -1,
675 message: format!("Action '{}' not found", action_name),
676 })
677 }
678
679 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
681 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
683 {
684 if let Ok(pid) = proxy.get_property::<i32>("Id") {
685 if pid > 0 {
686 return Some(pid as u32);
687 }
688 }
689 }
690
691 if let Ok(proxy) = self.make_proxy(
693 "org.freedesktop.DBus",
694 "/org/freedesktop/DBus",
695 "org.freedesktop.DBus",
696 ) {
697 if let Ok(reply) =
698 proxy.call_method("GetConnectionUnixProcessID", &(aref.bus_name.as_str(),))
699 {
700 if let Ok(pid) = reply.body().deserialize::<u32>() {
701 if pid > 0 {
702 return Some(pid);
703 }
704 }
705 }
706 }
707
708 None
709 }
710}
711
712impl Provider for LinuxProvider {
713 fn get_app_tree(&self, target: &AppTarget, opts: &QueryOptions) -> Result<Tree> {
714 let app_ref = match target {
715 AppTarget::ByName(name) => self.find_app_by_name(name)?,
716 AppTarget::ByPid(pid) => self.find_app_by_pid(*pid)?,
717 AppTarget::ByWindow(_) => {
718 return Err(Error::Platform {
719 code: -1,
720 message: "ByWindow not supported on Linux AT-SPI2".to_string(),
721 });
722 }
723 };
724
725 let app_name = self.get_name(&app_ref).unwrap_or_default();
726 let screen_size = Self::detect_screen_size();
727 let mut nodes = Vec::new();
728 let mut refs = Vec::new();
729
730 self.traverse(&app_ref, opts, &mut nodes, &mut refs, None, 0, screen_size);
731
732 if nodes.is_empty() {
733 return Err(Error::AppNotFound {
734 target: format!("{:?}", target),
735 });
736 }
737
738 *self.cached_refs.lock().unwrap() = refs;
740
741 let pid = self.get_app_pid(&app_ref);
742
743 Ok(Tree::new(app_name, pid, screen_size, nodes))
744 }
745
746 fn get_all_apps(&self, opts: &QueryOptions) -> Result<Tree> {
747 let screen_size = Self::detect_screen_size();
748 let mut nodes = Vec::new();
749
750 nodes.push(Node {
751 role: Role::Application,
752 name: Some("Desktop".to_string()),
753 value: None,
754 description: None,
755 bounds: Some(Rect {
756 x: 0,
757 y: 0,
758 width: screen_size.0,
759 height: screen_size.1,
760 }),
761 actions: vec![],
762 states: StateSet::default(),
763 numeric_value: None,
764 min_value: None,
765 max_value: None,
766 stable_id: None,
767 raw: xa11y_core::RawPlatformData::Synthetic,
768 index: 0,
769 children_indices: vec![],
770 parent_index: None,
771 });
772
773 let mut refs = Vec::new();
774 refs.push(AccessibleRef {
775 bus_name: String::new(),
776 path: String::new(),
777 }); let registry = AccessibleRef {
780 bus_name: "org.a11y.atspi.Registry".to_string(),
781 path: "/org/a11y/atspi/accessible/root".to_string(),
782 };
783 let children = self.get_children(®istry).unwrap_or_default();
784 let mut root_children = Vec::new();
785
786 for child in &children {
787 if child.path == "/org/a11y/atspi/null" {
788 continue;
789 }
790 let app_name = self.get_name(child).unwrap_or_default();
791 if app_name.is_empty() {
792 continue;
793 }
794 let child_idx = nodes.len() as u32;
795 root_children.push(child_idx);
796 self.traverse(child, opts, &mut nodes, &mut refs, Some(0), 1, screen_size);
797 }
798
799 nodes[0].children_indices = root_children;
800
801 *self.cached_refs.lock().unwrap() = refs;
802
803 Ok(Tree::new("Desktop".to_string(), None, screen_size, nodes))
804 }
805
806 fn perform_action(
807 &self,
808 tree: &Tree,
809 node: &Node,
810 action: Action,
811 data: Option<ActionData>,
812 ) -> Result<()> {
813 let node_idx = tree.node_index(node);
814
815 let cache = self.cached_refs.lock().unwrap();
817 let target = cache
818 .get(node_idx as usize)
819 .ok_or(Error::ElementStale {
820 selector: format!("index:{}", node_idx),
821 })?
822 .clone();
823 drop(cache);
824
825 match action {
826 Action::Press => self
827 .do_atspi_action(&target, "click")
828 .or_else(|_| self.do_atspi_action(&target, "activate"))
829 .or_else(|_| self.do_atspi_action(&target, "press")),
830 Action::Focus => {
831 if let Ok(proxy) =
833 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
834 {
835 if proxy.call_method("GrabFocus", &()).is_ok() {
836 return Ok(());
837 }
838 }
839 self.do_atspi_action(&target, "focus")
840 .or_else(|_| self.do_atspi_action(&target, "setFocus"))
841 }
842 Action::SetValue => match data {
843 Some(ActionData::NumericValue(v)) => {
844 let proxy =
845 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
846 proxy
847 .set_property("CurrentValue", v)
848 .map_err(|e| Error::Platform {
849 code: -1,
850 message: format!("SetValue failed: {}", e),
851 })
852 }
853 Some(ActionData::Value(text)) => {
854 let proxy = self
855 .make_proxy(
856 &target.bus_name,
857 &target.path,
858 "org.a11y.atspi.EditableText",
859 )
860 .map_err(|_| Error::TextValueNotSupported)?;
861 let _ = proxy.call_method("DeleteText", &(0i32, i32::MAX));
862 proxy
863 .call_method("InsertText", &(0i32, &*text, text.len() as i32))
864 .map_err(|_| Error::TextValueNotSupported)?;
865 Ok(())
866 }
867 _ => Err(Error::Platform {
868 code: -1,
869 message: "SetValue requires ActionData".to_string(),
870 }),
871 },
872 Action::Toggle => self
873 .do_atspi_action(&target, "toggle")
874 .or_else(|_| self.do_atspi_action(&target, "click"))
875 .or_else(|_| self.do_atspi_action(&target, "activate")),
876 Action::Expand => self
877 .do_atspi_action(&target, "expand")
878 .or_else(|_| self.do_atspi_action(&target, "open")),
879 Action::Collapse => self
880 .do_atspi_action(&target, "collapse")
881 .or_else(|_| self.do_atspi_action(&target, "close")),
882 Action::Select => self.do_atspi_action(&target, "select"),
883 Action::ShowMenu => self
884 .do_atspi_action(&target, "menu")
885 .or_else(|_| self.do_atspi_action(&target, "showmenu")),
886 Action::ScrollIntoView => {
887 let proxy =
888 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
889 proxy
890 .call_method("ScrollTo", &(0u32,))
891 .map_err(|e| Error::Platform {
892 code: -1,
893 message: format!("ScrollTo failed: {}", e),
894 })?;
895 Ok(())
896 }
897 Action::Increment => self.do_atspi_action(&target, "increment").or_else(|_| {
898 let proxy =
900 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
901 let current: f64 =
902 proxy
903 .get_property("CurrentValue")
904 .map_err(|e| Error::Platform {
905 code: -1,
906 message: format!("Value.CurrentValue failed: {}", e),
907 })?;
908 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
909 let step = if step <= 0.0 { 1.0 } else { step };
910 proxy
911 .set_property("CurrentValue", current + step)
912 .map_err(|e| Error::Platform {
913 code: -1,
914 message: format!("Value.SetCurrentValue failed: {}", e),
915 })
916 }),
917 Action::Decrement => self.do_atspi_action(&target, "decrement").or_else(|_| {
918 let proxy =
919 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
920 let current: f64 =
921 proxy
922 .get_property("CurrentValue")
923 .map_err(|e| Error::Platform {
924 code: -1,
925 message: format!("Value.CurrentValue failed: {}", e),
926 })?;
927 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
928 let step = if step <= 0.0 { 1.0 } else { step };
929 proxy
930 .set_property("CurrentValue", current - step)
931 .map_err(|e| Error::Platform {
932 code: -1,
933 message: format!("Value.SetCurrentValue failed: {}", e),
934 })
935 }),
936 Action::Blur => {
937 let proxy =
939 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Accessible")?;
940 if let Ok(reply) = proxy.call_method("GetParent", &()) {
941 if let Ok((bus, path)) = reply
942 .body()
943 .deserialize::<(String, zbus::zvariant::OwnedObjectPath)>()
944 {
945 let path_str = path.as_str();
946 if path_str != "/org/a11y/atspi/null" {
947 if let Ok(p) =
948 self.make_proxy(&bus, path_str, "org.a11y.atspi.Component")
949 {
950 let _ = p.call_method("GrabFocus", &());
951 return Ok(());
952 }
953 }
954 }
955 }
956 Ok(())
957 }
958
959 Action::Scroll => {
960 let (direction, amount) = match data {
961 Some(ActionData::ScrollAmount { direction, amount }) => (direction, amount),
962 _ => {
963 return Err(Error::Platform {
964 code: -1,
965 message: "Scroll requires ActionData::ScrollAmount".to_string(),
966 })
967 }
968 };
969 let count = (amount.abs() as u32).max(1);
971 let action_name = match direction {
972 ScrollDirection::Up => "scroll up",
973 ScrollDirection::Down => "scroll down",
974 ScrollDirection::Left => "scroll left",
975 ScrollDirection::Right => "scroll right",
976 };
977 for _ in 0..count {
978 if self.do_atspi_action(&target, action_name).is_err() {
979 let proxy = self.make_proxy(
981 &target.bus_name,
982 &target.path,
983 "org.a11y.atspi.Component",
984 )?;
985 let scroll_type: u32 = match direction {
986 ScrollDirection::Up => 2, ScrollDirection::Down => 3, ScrollDirection::Left => 4, ScrollDirection::Right => 5, };
991 proxy
992 .call_method("ScrollTo", &(scroll_type,))
993 .map_err(|e| Error::Platform {
994 code: -1,
995 message: format!("ScrollTo failed: {}", e),
996 })?;
997 return Ok(());
998 }
999 }
1000 Ok(())
1001 }
1002
1003 Action::SetTextSelection => {
1004 let (start, end) = match data {
1005 Some(ActionData::TextSelection { start, end }) => (start, end),
1006 _ => {
1007 return Err(Error::Platform {
1008 code: -1,
1009 message: "SetTextSelection requires ActionData::TextSelection"
1010 .to_string(),
1011 })
1012 }
1013 };
1014 let proxy =
1015 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1016 if proxy
1018 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1019 .is_err()
1020 {
1021 proxy
1022 .call_method("AddSelection", &(start as i32, end as i32))
1023 .map_err(|e| Error::Platform {
1024 code: -1,
1025 message: format!("Text.AddSelection failed: {}", e),
1026 })?;
1027 }
1028 Ok(())
1029 }
1030
1031 Action::TypeText => {
1032 let text = match data {
1033 Some(ActionData::Value(text)) => text,
1034 _ => {
1035 return Err(Error::Platform {
1036 code: -1,
1037 message: "TypeText requires ActionData::Value".to_string(),
1038 })
1039 }
1040 };
1041 let text_proxy =
1044 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1045 let insert_pos = text_proxy
1046 .as_ref()
1047 .ok()
1048 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1049 .unwrap_or(-1); let proxy = self
1052 .make_proxy(
1053 &target.bus_name,
1054 &target.path,
1055 "org.a11y.atspi.EditableText",
1056 )
1057 .map_err(|_| Error::TextValueNotSupported)?;
1058 let pos = if insert_pos >= 0 {
1059 insert_pos
1060 } else {
1061 i32::MAX
1062 };
1063 proxy
1064 .call_method("InsertText", &(pos, &*text, text.len() as i32))
1065 .map_err(|e| Error::Platform {
1066 code: -1,
1067 message: format!("EditableText.InsertText failed: {}", e),
1068 })?;
1069 Ok(())
1070 }
1071 }
1072 }
1073
1074 fn check_permissions(&self) -> Result<PermissionStatus> {
1075 let registry = AccessibleRef {
1076 bus_name: "org.a11y.atspi.Registry".to_string(),
1077 path: "/org/a11y/atspi/accessible/root".to_string(),
1078 };
1079 match self.get_children(®istry) {
1080 Ok(_) => Ok(PermissionStatus::Granted),
1081 Err(_) => Ok(PermissionStatus::Denied {
1082 instructions:
1083 "Enable accessibility: gsettings set org.gnome.desktop.interface toolkit-accessibility true\nEnsure at-spi2-core is installed."
1084 .to_string(),
1085 }),
1086 }
1087 }
1088
1089 fn list_apps(&self) -> Result<Vec<AppInfo>> {
1090 let registry = AccessibleRef {
1091 bus_name: "org.a11y.atspi.Registry".to_string(),
1092 path: "/org/a11y/atspi/accessible/root".to_string(),
1093 };
1094 let children = self.get_children(®istry)?;
1095 let mut apps = Vec::new();
1096
1097 for child in &children {
1098 if child.path == "/org/a11y/atspi/null" {
1099 continue;
1100 }
1101 let name = self.get_name(child).unwrap_or_default();
1102 if name.is_empty() {
1103 continue;
1104 }
1105 let pid = self.get_app_pid(child);
1106 apps.push(AppInfo {
1107 name,
1108 pid: pid.unwrap_or(0),
1109 bundle_id: None,
1110 });
1111 }
1112
1113 Ok(apps)
1114 }
1115}
1116
1117impl EventProvider for LinuxProvider {
1120 fn subscribe(&self, target: &AppTarget, filter: EventFilter) -> Result<Subscription> {
1121 let (tx, rx) = std::sync::mpsc::channel();
1122
1123 let app_info = match target {
1124 AppTarget::ByName(name) => {
1125 let app_ref = self.find_app_by_name(name)?;
1126 let pid = self.get_app_pid(&app_ref).unwrap_or(0);
1127 AppInfo {
1128 name: self.get_name(&app_ref).unwrap_or_default(),
1129 pid,
1130 bundle_id: None,
1131 }
1132 }
1133 AppTarget::ByPid(pid) => {
1134 let app_ref = self.find_app_by_pid(*pid)?;
1135 AppInfo {
1136 name: self.get_name(&app_ref).unwrap_or_default(),
1137 pid: *pid,
1138 bundle_id: None,
1139 }
1140 }
1141 AppTarget::ByWindow(_) => {
1142 return Err(Error::Platform {
1143 code: -1,
1144 message: "ByWindow not supported for event subscription".to_string(),
1145 })
1146 }
1147 };
1148
1149 let poll_provider = LinuxProvider::new()?;
1151 let target_clone = target.clone();
1152 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1153 let stop_clone = stop.clone();
1154
1155 let handle = std::thread::spawn(move || {
1157 let mut prev_focused: Option<String> = None;
1158 let mut prev_node_count: usize = 0;
1159
1160 while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
1161 std::thread::sleep(Duration::from_millis(100));
1162
1163 let tree = match poll_provider.get_app_tree(&target_clone, &QueryOptions::default())
1164 {
1165 Ok(t) => t,
1166 Err(_) => continue,
1167 };
1168
1169 let focused_name = tree
1171 .iter()
1172 .find(|n| n.states.focused)
1173 .and_then(|n| n.name.clone());
1174 if focused_name != prev_focused {
1175 if prev_focused.is_some() {
1176 let kind = EventKind::FocusChanged;
1177 if filter.kinds.is_empty() || filter.kinds.contains(&kind) {
1178 let _ = tx.send(Event {
1179 kind,
1180 app: app_info.clone(),
1181 target: tree.iter().find(|n| n.states.focused).cloned(),
1182 state_flag: None,
1183 state_value: None,
1184 text_change: None,
1185 timestamp: std::time::Instant::now(),
1186 });
1187 }
1188 }
1189 prev_focused = focused_name;
1190 }
1191
1192 let node_count = tree.len();
1194 if node_count != prev_node_count && prev_node_count > 0 {
1195 let kind = EventKind::StructureChanged;
1196 if filter.kinds.is_empty() || filter.kinds.contains(&kind) {
1197 let _ = tx.send(Event {
1198 kind,
1199 app: app_info.clone(),
1200 target: None,
1201 state_flag: None,
1202 state_value: None,
1203 text_change: None,
1204 timestamp: std::time::Instant::now(),
1205 });
1206 }
1207 }
1208 prev_node_count = node_count;
1209 }
1210 });
1211
1212 let cancel = CancelHandle::new(move || {
1213 stop.store(true, std::sync::atomic::Ordering::Relaxed);
1214 let _ = handle.join();
1215 });
1216
1217 Ok(Subscription::new(EventReceiver::new(rx), cancel))
1218 }
1219
1220 fn wait_for_event(
1221 &self,
1222 target: &AppTarget,
1223 filter: EventFilter,
1224 timeout: Duration,
1225 ) -> Result<Event> {
1226 let sub = self.subscribe(target, filter)?;
1227 let start = std::time::Instant::now();
1228 loop {
1229 if let Some(event) = sub.try_recv() {
1230 return Ok(event);
1231 }
1232 let elapsed = start.elapsed();
1233 if elapsed >= timeout {
1234 return Err(Error::Timeout { elapsed });
1235 }
1236 std::thread::sleep(Duration::from_millis(10));
1237 }
1238 }
1239
1240 fn wait_for(
1241 &self,
1242 target: &AppTarget,
1243 selector: &str,
1244 state: ElementState,
1245 timeout: Duration,
1246 ) -> Result<Node> {
1247 let start = std::time::Instant::now();
1248 let poll_interval = Duration::from_millis(100);
1249
1250 loop {
1251 let elapsed = start.elapsed();
1252 if elapsed >= timeout {
1253 return Err(Error::Timeout { elapsed });
1254 }
1255
1256 let tree = self.get_app_tree(target, &QueryOptions::default())?;
1257 let matches = tree.query(selector).ok();
1258 let node = matches.as_ref().and_then(|m| m.first().copied());
1259
1260 if state.is_met(node) {
1261 return Ok(node.cloned().unwrap_or_else(Node::synthetic_empty));
1262 }
1263
1264 std::thread::sleep(poll_interval);
1265 }
1266 }
1267}
1268
1269fn map_atspi_role(role_name: &str) -> Role {
1271 match role_name.to_lowercase().as_str() {
1272 "application" => Role::Application,
1273 "window" | "frame" => Role::Window,
1274 "dialog" | "file chooser" => Role::Dialog,
1275 "alert" | "notification" => Role::Alert,
1276 "push button" | "push button menu" => Role::Button,
1277 "check box" | "check menu item" => Role::CheckBox,
1278 "radio button" | "radio menu item" => Role::RadioButton,
1279 "entry" | "password text" => Role::TextField,
1280 "spin button" => Role::SpinButton,
1281 "text" => Role::TextArea,
1282 "label" | "static" | "caption" => Role::StaticText,
1283 "combo box" => Role::ComboBox,
1284 "list" | "list box" => Role::List,
1285 "list item" => Role::ListItem,
1286 "menu" => Role::Menu,
1287 "menu item" | "tearoff menu item" => Role::MenuItem,
1288 "menu bar" => Role::MenuBar,
1289 "page tab" => Role::Tab,
1290 "page tab list" => Role::TabGroup,
1291 "table" | "tree table" => Role::Table,
1292 "table row" => Role::TableRow,
1293 "table cell" | "table column header" | "table row header" => Role::TableCell,
1294 "tool bar" => Role::Toolbar,
1295 "scroll bar" => Role::ScrollBar,
1296 "slider" => Role::Slider,
1297 "image" | "icon" | "desktop icon" => Role::Image,
1298 "link" => Role::Link,
1299 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1300 "progress bar" => Role::ProgressBar,
1301 "tree item" => Role::TreeItem,
1302 "document web" | "document frame" => Role::WebArea,
1303 "heading" => Role::Heading,
1304 "separator" => Role::Separator,
1305 "split pane" => Role::SplitGroup,
1306 "tooltip" | "tool tip" => Role::Tooltip,
1307 "status bar" | "statusbar" => Role::Status,
1308 "landmark" | "navigation" => Role::Navigation,
1309 _ => Role::Unknown,
1310 }
1311}
1312
1313fn map_atspi_role_number(role: u32) -> Role {
1316 match role {
1317 2 => Role::Alert, 7 => Role::CheckBox, 8 => Role::CheckBox, 11 => Role::ComboBox, 16 => Role::Dialog, 19 => Role::Dialog, 20 => Role::Group, 23 => Role::Window, 26 => Role::Image, 27 => Role::Image, 29 => Role::StaticText, 31 => Role::List, 32 => Role::ListItem, 33 => Role::Menu, 34 => Role::MenuBar, 35 => Role::MenuItem, 37 => Role::Tab, 38 => Role::TabGroup, 39 => Role::Group, 40 => Role::TextField, 42 => Role::ProgressBar, 43 => Role::Button, 44 => Role::RadioButton, 45 => Role::RadioButton, 48 => Role::ScrollBar, 49 => Role::Group, 50 => Role::Separator, 51 => Role::Slider, 52 => Role::SpinButton, 53 => Role::SplitGroup, 55 => Role::Table, 56 => Role::TableCell, 57 => Role::TableCell, 58 => Role::TableCell, 61 => Role::TextArea, 62 => Role::Button, 63 => Role::Toolbar, 65 => Role::Group, 66 => Role::Table, 67 => Role::Unknown, 68 => Role::Group, 69 => Role::Window, 75 => Role::Application, 79 => Role::TextField, 82 => Role::WebArea, 83 => Role::Heading, 85 => Role::Group, 86 => Role::Group, 87 => Role::Group, 88 => Role::Link, 90 => Role::TableRow, 91 => Role::TreeItem, 95 => Role::WebArea, 98 => Role::List, 93 => Role::Tooltip, 97 => Role::Status, 101 => Role::Alert, 116 => Role::StaticText, 129 => Role::Button, _ => Role::Unknown,
1377 }
1378}
1379
1380fn map_atspi_action(action_name: &str) -> Option<Action> {
1382 match action_name.to_lowercase().as_str() {
1383 "click" | "activate" | "press" | "invoke" => Some(Action::Press),
1384 "toggle" | "check" | "uncheck" => Some(Action::Toggle),
1385 "expand" | "open" => Some(Action::Expand),
1386 "collapse" | "close" => Some(Action::Collapse),
1387 "select" => Some(Action::Select),
1388 "menu" | "showmenu" | "popup" | "show menu" => Some(Action::ShowMenu),
1389 "increment" => Some(Action::Increment),
1390 "decrement" => Some(Action::Decrement),
1391 _ => None,
1392 }
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397 use super::*;
1398
1399 #[test]
1400 fn test_role_mapping() {
1401 assert_eq!(map_atspi_role("push button"), Role::Button);
1402 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
1403 assert_eq!(map_atspi_role("entry"), Role::TextField);
1404 assert_eq!(map_atspi_role("label"), Role::StaticText);
1405 assert_eq!(map_atspi_role("window"), Role::Window);
1406 assert_eq!(map_atspi_role("frame"), Role::Window);
1407 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
1408 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
1409 assert_eq!(map_atspi_role("slider"), Role::Slider);
1410 assert_eq!(map_atspi_role("panel"), Role::Group);
1411 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
1412 }
1413
1414 #[test]
1415 fn test_action_mapping() {
1416 assert_eq!(map_atspi_action("click"), Some(Action::Press));
1417 assert_eq!(map_atspi_action("activate"), Some(Action::Press));
1418 assert_eq!(map_atspi_action("toggle"), Some(Action::Toggle));
1419 assert_eq!(map_atspi_action("expand"), Some(Action::Expand));
1420 assert_eq!(map_atspi_action("collapse"), Some(Action::Collapse));
1421 assert_eq!(map_atspi_action("select"), Some(Action::Select));
1422 assert_eq!(map_atspi_action("increment"), Some(Action::Increment));
1423 assert_eq!(map_atspi_action("foobar"), None);
1424 }
1425}