#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "linux")]
use linux as imp;
#[cfg(target_os = "macos")]
use macos as imp;
#[cfg(target_os = "windows")]
use windows as imp;
use crate::extensions::computer::state::Rect;
use std::sync::{Mutex, OnceLock};
const SNAPSHOT_KEEP: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeId {
pub bus: String,
pub path: String,
pub name: String,
pub role: String,
}
#[derive(Debug, Clone)]
pub struct A11yOptions {
pub max_depth: usize,
pub max_nodes: usize,
pub max_visited: usize,
pub patterns: bool,
pub skip_empty: bool,
}
impl Default for A11yOptions {
fn default() -> Self {
Self {
max_depth: 5,
max_nodes: 250,
max_visited: 4000,
patterns: true,
skip_empty: true,
}
}
}
#[derive(Debug, Clone)]
pub struct A11yNode {
pub index: usize,
pub depth: usize,
pub role: String,
pub name: String,
pub description: String,
pub automation_id: String,
pub states: Vec<String>,
pub rect: Option<Rect>,
pub actions: Vec<String>,
pub interfaces: Vec<String>,
pub has_children: bool,
}
#[derive(Debug, Clone)]
pub struct A11ySnapshot {
pub seq: u64,
pub prefix: String,
pub nodes: Vec<NodeId>,
}
fn snapshots() -> &'static Mutex<Vec<A11ySnapshot>> {
static SNAPSHOTS: OnceLock<Mutex<Vec<A11ySnapshot>>> = OnceLock::new();
SNAPSHOTS.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn install_snapshot(nodes: Vec<NodeId>) -> String {
let mut list = snapshots().lock().unwrap_or_else(|e| e.into_inner());
let seq = list.last().map(|s| s.seq + 1).unwrap_or(1);
let prefix = format!("s{seq}.");
list.push(A11ySnapshot {
seq,
prefix: prefix.clone(),
nodes,
});
while list.len() > SNAPSHOT_KEEP {
list.remove(0);
}
prefix
}
pub fn resolve_ref(reference: &str) -> Result<NodeId, String> {
let (prefix, index) = reference.rsplit_once('.').ok_or_else(|| {
format!("{reference:?} is not a ref; refs look like s1.4 (from computer_ui)")
})?;
let index: usize = index.parse().map_err(|_| {
format!("{reference:?} is not a ref; refs look like s1.4 (from computer_ui)")
})?;
let list = snapshots().lock().unwrap_or_else(|e| e.into_inner());
let snapshot = list
.iter()
.find(|s| s.prefix == format!("{prefix}."))
.ok_or_else(|| {
format!("{reference} is from an older read; call computer_ui again and use a fresh ref")
})?;
snapshot
.nodes
.get(index)
.cloned()
.ok_or_else(|| format!("{reference} is not in that snapshot"))
}
pub fn available() -> bool {
imp::available()
}
pub async fn read_tree(
window: Option<u64>,
options: A11yOptions,
) -> Result<(Vec<A11yNode>, Vec<NodeId>), String> {
imp::read_tree(window, options).await
}
pub async fn do_action(target: &NodeId, index: u32) -> Result<(), String> {
imp::do_action(target, index).await
}
pub async fn action_by_name(target: &NodeId, wanted: &str) -> Result<(), String> {
imp::action_by_name(target, wanted).await
}
pub async fn read_text(target: &NodeId) -> Result<String, String> {
imp::read_text(target).await
}
pub async fn set_text(target: &NodeId, text: &str) -> Result<(), String> {
imp::set_text(target, text).await
}
pub async fn read_value(target: &NodeId) -> Result<String, String> {
imp::read_value(target).await
}
pub async fn grab_focus(target: &NodeId) -> Result<(), String> {
imp::grab_focus(target).await
}
pub async fn select_text(target: &NodeId, start: i32, end: i32) -> Result<(), String> {
imp::select_text(target, start, end).await
}
pub async fn select_child(target: &NodeId, index: i32) -> Result<(), String> {
imp::select_child(target, index).await
}
pub async fn caret_offset(target: &NodeId) -> Option<i32> {
imp::caret_offset(target).await
}
pub async fn node_rect(target: &NodeId) -> Option<Rect> {
imp::node_rect(target).await
}
pub fn render(nodes: &[A11yNode], prefix: &str, compact: bool) -> String {
let mut lines = Vec::new();
for node in nodes {
let indent = " ".repeat(node.depth);
let mut line = format!(
"{indent}[{prefix}{}] {}",
node.index,
if node.role.is_empty() {
"?"
} else {
&node.role
}
);
if !node.name.is_empty() {
line.push_str(&format!(" {:?}", node.name));
}
if !node.automation_id.is_empty() {
line.push_str(&format!(" #{}", node.automation_id));
}
if !node.states.is_empty() {
line.push_str(&format!(" [{}]", node.states.join(",")));
}
if !node.actions.is_empty() {
line.push_str(&format!(" {{{}}}", node.actions.join(",")));
}
if !compact && let Some(rect) = node.rect {
line.push_str(&format!(
" @{},{} {}x{}",
rect.x, rect.y, rect.width, rect.height
));
}
lines.push(line);
}
lines.join("\n")
}