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