accessibility 0.2.0

Bindings for macOS Accessibility services
Documentation
pub mod action;
pub mod attribute;
pub mod ui_element;
mod util;

use accessibility_sys::{error_string, AXError};
use core_foundation::{
    array::CFArray,
    base::CFTypeID,
    base::{CFCopyTypeIDDescription, TCFType},
    string::CFString,
};
use std::{
    cell::{Cell, RefCell},
    thread,
    time::{Duration, Instant},
};
use thiserror::Error as TError;

pub use action::*;
pub use attribute::*;
pub use ui_element::*;

#[non_exhaustive]
#[derive(Debug, TError)]
pub enum Error {
    #[error("element not found")]
    NotFound,
    #[error(
        "expected attribute type {} but got {}",
        type_name(*expected),
        type_name(*received),
    )]
    UnexpectedType {
        expected: CFTypeID,
        received: CFTypeID,
    },
    #[error("accessibility error {}", error_string(*.0))]
    Ax(AXError),
}

fn type_name(type_id: CFTypeID) -> CFString {
    unsafe { CFString::wrap_under_create_rule(CFCopyTypeIDDescription(type_id)) }
}

pub trait TreeVisitor {
    fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow;
    fn exit_element(&self, element: &AXUIElement);
}

pub struct TreeWalker {
    attr_children: AXAttribute<CFArray<AXUIElement>>,
}

#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TreeWalkerFlow {
    Continue,
    SkipSubtree,
    Exit,
}

impl Default for TreeWalker {
    fn default() -> Self {
        Self {
            attr_children: AXAttribute::children(),
        }
    }
}

impl TreeWalker {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn walk(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) {
        let _ = self.walk_one(root, visitor);
    }

    fn walk_one(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) -> TreeWalkerFlow {
        let mut flow = visitor.enter_element(root);

        if flow == TreeWalkerFlow::Continue {
            if let Ok(children) = root.attribute(&self.attr_children) {
                for child in children.into_iter() {
                    let child_flow = self.walk_one(&child, visitor);

                    if child_flow == TreeWalkerFlow::Exit {
                        flow = child_flow;
                        break;
                    }
                }
            }
        }

        visitor.exit_element(root);
        flow
    }
}

pub struct ElementFinder {
    root: AXUIElement,
    implicit_wait: Option<Duration>,
    predicate: Box<dyn Fn(&AXUIElement) -> bool>,
    depth: Cell<usize>,
    cached: RefCell<Option<AXUIElement>>,
}

impl ElementFinder {
    pub fn new<F>(root: &AXUIElement, predicate: F, implicit_wait: Option<Duration>) -> Self
    where
        F: 'static + Fn(&AXUIElement) -> bool,
    {
        Self {
            root: root.clone(),
            predicate: Box::new(predicate),
            implicit_wait,
            depth: Cell::new(0),
            cached: RefCell::new(None),
        }
    }

    pub fn find(&self) -> Result<AXUIElement, Error> {
        if let Some(result) = &*self.cached.borrow() {
            return Ok(result.clone());
        }

        let mut deadline = Instant::now();
        let walker = TreeWalker::new();

        if let Some(implicit_wait) = &self.implicit_wait {
            deadline += *implicit_wait;
        }

        loop {
            if let Some(result) = &*self.cached.borrow() {
                return Ok(result.clone());
            }

            walker.walk(&self.root, self);
            let now = Instant::now();

            if now >= deadline {
                return Err(Error::NotFound);
            } else {
                let time_left = deadline.saturating_duration_since(now);
                thread::sleep(std::cmp::min(time_left, Duration::from_millis(250)));
            }
        }
    }

    pub fn reset(&self) {
        self.cached.replace(None);
    }

    pub fn attribute<T: TCFType>(&self, attribute: &AXAttribute<T>) -> Result<T, Error> {
        self.find()?.attribute(attribute)
    }

    pub fn set_attribute<T: TCFType>(
        &self,
        attribute: &AXAttribute<T>,
        value: impl Into<T>,
    ) -> Result<(), Error> {
        self.find()?.set_attribute(attribute, value)
    }

    pub fn perform_action(&self, name: &CFString) -> Result<(), Error> {
        self.find()?.perform_action(name)
    }
}

const MAX_DEPTH: usize = 100;

impl TreeVisitor for ElementFinder {
    fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow {
        self.depth.set(self.depth.get() + 1);

        if (self.predicate)(element) {
            self.cached.replace(Some(element.clone()));
            return TreeWalkerFlow::Exit;
        }

        if self.depth.get() > MAX_DEPTH {
            TreeWalkerFlow::SkipSubtree
        } else {
            TreeWalkerFlow::Continue
        }
    }

    fn exit_element(&self, _element: &AXUIElement) {
        self.depth.set(self.depth.get() - 1)
    }
}