use super::{clipboard, input, permissions, pixel, process, window};
use crate::error::{Error, Result};
use crate::keys::Keys;
use crate::options::{Options, ShowState, Speed, WinState};
use crate::selector::Criterion;
use crate::{Point, Rect, Selector, Size};
use parking_lot::{Mutex, ReentrantMutex, ReentrantMutexGuard, RwLock};
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub(crate) struct Inner {
lock: ReentrantMutex<()>,
options: RwLock<Options>,
handles: Mutex<HashMap<u64, Pinned>>,
}
#[derive(Clone)]
struct Pinned {
pid: i32,
title: String,
}
impl Inner {
pub(crate) fn load(options: Options) -> Result<Self> {
permissions::require(permissions::Permission::Accessibility)?;
Ok(Self {
lock: ReentrantMutex::new(()),
options: RwLock::new(options),
handles: Mutex::new(HashMap::new()),
})
}
pub(crate) fn lock(&self) -> ReentrantMutexGuard<'_, ()> {
self.lock.lock()
}
pub(crate) fn options(&self) -> Options {
*self.options.read()
}
fn resolve(&self, s: &Selector) -> Result<window::Window> {
self.find(s)?.ok_or_else(|| Error::window_not_found(s))
}
fn find(&self, s: &Selector) -> Result<Option<window::Window>> {
if let Some(pinned) = self.pinned(s) {
return self.find_pinned(&pinned);
}
let o = self.options();
window::find(
s,
o.win_title_match_mode,
o.win_title_match_case_insensitive,
)
}
fn pinned(&self, s: &Selector) -> Option<Pinned> {
let criteria = s.criteria()?;
let handle = criteria.iter().find_map(|c| match c {
Criterion::Handle(h) => Some(*h),
_ => None,
})?;
self.handles.lock().get(&handle).cloned()
}
fn find_pinned(&self, pinned: &Pinned) -> Result<Option<window::Window>> {
Ok(window::all_windows()?
.into_iter()
.find(|w| w.pid == pinned.pid && w.title == pinned.title))
}
fn handle_for(&self, w: &window::Window) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in (w.pid as u32)
.to_le_bytes()
.iter()
.chain(w.title.as_bytes())
{
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
let handle = if hash == 0 { 1 } else { hash };
self.handles.lock().insert(
handle,
Pinned {
pid: w.pid,
title: w.title.clone(),
},
);
handle
}
fn wait_for(
&self,
timeout: Option<Duration>,
mut f: impl FnMut() -> Result<bool>,
) -> Result<bool> {
let delay = self.options().win_wait_delay;
let deadline = timeout.map(|t| Instant::now() + t);
loop {
if f()? {
return Ok(true);
}
if deadline.is_some_and(|d| Instant::now() >= d) {
return Ok(false);
}
std::thread::sleep(delay);
}
}
pub(crate) fn send(&self, keys: &Keys) -> Result<()> {
let _guard = self.lock();
input::send(keys, &self.options())
}
pub(crate) fn clip_get(&self) -> Result<String> {
let _guard = self.lock();
clipboard::get()
}
pub(crate) fn clip_sequence(&self) -> Option<u32> {
clipboard::sequence()
}
pub(crate) fn clip_put(&self, s: &str) -> Result<()> {
let _guard = self.lock();
clipboard::put(s)
}
pub(crate) fn is_idle(&self) -> Result<bool> {
let Some(app) = objc2_app_kit::NSWorkspace::sharedWorkspace().frontmostApplication() else {
return Ok(false);
};
Ok(window::is_app_responsive(
app.processIdentifier(),
Duration::from_millis(250),
))
}
pub(crate) fn mouse_click(
&self,
button: &str,
at: Point,
clicks: u32,
speed: Option<Speed>,
) -> Result<()> {
let _guard = self.lock();
input::mouse_click(button, at, clicks, speed, &self.options())
}
pub(crate) fn mouse_get_pos(&self) -> Result<Point> {
let _guard = self.lock();
input::mouse_get_pos()
}
pub(crate) fn mouse_move(&self, p: Point, speed: Option<Speed>) -> Result<()> {
let _guard = self.lock();
input::mouse_move(p, speed)
}
pub(crate) fn mouse_down(&self, button: &str) -> Result<()> {
let _guard = self.lock();
input::mouse_down(button)
}
pub(crate) fn mouse_up(&self, button: &str) -> Result<()> {
let _guard = self.lock();
input::mouse_up(button)
}
pub(crate) fn mouse_wheel(&self, direction: &str, clicks: u32) -> Result<()> {
let _guard = self.lock();
input::mouse_wheel(direction, clicks)
}
pub(crate) fn mouse_click_drag(
&self,
button: &str,
from: Point,
to: Point,
speed: Option<Speed>,
) -> Result<()> {
let _guard = self.lock();
input::mouse_click_drag(button, from, to, speed, &self.options())
}
pub(crate) fn win_exists(&self, s: &Selector) -> Result<bool> {
let _guard = self.lock();
Ok(self.find(s)?.is_some())
}
pub(crate) fn win_active(&self, s: &Selector) -> Result<bool> {
let _guard = self.lock();
Ok(self.find(s)?.is_some_and(|w| window::is_active(&w)))
}
pub(crate) fn win_activate(&self, s: &Selector) -> Result<bool> {
let _guard = self.lock();
let Some(w) = self.find(s)? else {
return Ok(false);
};
Ok(window::activate(&w))
}
pub(crate) fn win_wait(&self, s: &Selector, t: Option<Duration>) -> Result<bool> {
let _guard = self.lock();
self.wait_for(t, || Ok(self.find(s)?.is_some()))
}
pub(crate) fn win_wait_active(&self, s: &Selector, t: Option<Duration>) -> Result<bool> {
let _guard = self.lock();
self.wait_for(t, || {
Ok(self.find(s)?.is_some_and(|w| window::is_active(&w)))
})
}
pub(crate) fn win_wait_not_active(&self, s: &Selector, t: Option<Duration>) -> Result<bool> {
let _guard = self.lock();
self.wait_for(t, || {
Ok(!self.find(s)?.is_some_and(|w| window::is_active(&w)))
})
}
pub(crate) fn win_wait_close(&self, s: &Selector, t: Option<Duration>) -> Result<bool> {
let _guard = self.lock();
self.wait_for(t, || Ok(self.find(s)?.is_none()))
}
pub(crate) fn win_close(&self, s: &Selector) -> Result<()> {
let _guard = self.lock();
let w = self.resolve(s)?;
window::close(&w);
Ok(())
}
pub(crate) fn win_kill(&self, s: &Selector) -> Result<bool> {
let _guard = self.lock();
let Some(w) = self.find(s)? else {
return Ok(false);
};
Ok(process::close(w.pid))
}
pub(crate) fn win_get_process(&self, s: &Selector) -> Result<u32> {
let _guard = self.lock();
Ok(self.resolve(s)?.pid as u32)
}
pub(crate) fn win_get_handle(&self, s: &Selector) -> Result<u64> {
let _guard = self.lock();
let w = self.resolve(s)?;
Ok(self.handle_for(&w))
}
pub(crate) fn win_get_title(&self, s: &Selector) -> Result<String> {
let _guard = self.lock();
Ok(self.resolve(s)?.title)
}
pub(crate) fn win_get_pos(&self, s: &Selector) -> Result<Rect> {
let _guard = self.lock();
let w = self.resolve(s)?;
w.rect().ok_or(Error::Platform {
operation: "read the window's position",
platform: "macOS",
})
}
pub(crate) fn win_get_client_size(&self, s: &Selector) -> Result<Size> {
let _guard = self.lock();
let w = self.resolve(s)?;
w.size().ok_or(Error::Platform {
operation: "read the window's size",
platform: "macOS",
})
}
pub(crate) fn win_get_state(&self, s: &Selector) -> Result<WinState> {
let _guard = self.lock();
match self.find(s)? {
Some(w) => Ok(window::state(&w)),
None => Ok(WinState::empty()),
}
}
pub(crate) fn win_set_state(&self, s: &Selector, state: ShowState) -> Result<bool> {
let _guard = self.lock();
let Some(w) = self.find(s)? else {
return Ok(false);
};
Ok(window::set_show_state(&w, state))
}
pub(crate) fn win_move(&self, s: &Selector, r: Rect) -> Result<bool> {
let _guard = self.lock();
let Some(w) = self.find(s)? else {
return Ok(false);
};
Ok(window::set_rect(&w, r))
}
pub(crate) fn win_get_text(&self, s: &Selector) -> Result<String> {
let _guard = self.lock();
let w = self.resolve(s)?;
Ok(window::text_of(&w))
}
pub(crate) fn win_get_class_list(&self, s: &Selector) -> Result<Vec<String>> {
let _guard = self.lock();
let w = self.resolve(s)?;
Ok(window::roles_of(&w))
}
pub(crate) fn process_id(&self, name: &str) -> Result<Option<u32>> {
Ok(process::find(name).map(|pid| pid as u32))
}
pub(crate) fn process_close(&self, name_or_pid: &str) -> Result<()> {
if let Some(pid) = process::find(name_or_pid) {
process::close(pid);
}
Ok(())
}
pub(crate) fn process_wait(&self, name: &str, t: Option<Duration>) -> Result<bool> {
self.wait_for(t, || Ok(process::find(name).is_some()))
}
pub(crate) fn process_wait_close(&self, name: &str, t: Option<Duration>) -> Result<bool> {
self.wait_for(t, || Ok(process::find(name).is_none()))
}
pub(crate) fn process_set_priority(&self, name: &str, priority: i32) -> Result<bool> {
Ok(process::find(name).is_some_and(|pid| process::set_priority(pid, priority)))
}
pub(crate) fn run(&self, command: &str, working_dir: Option<&str>) -> Result<u32> {
Ok(spawn(command, working_dir)?.id())
}
pub(crate) fn run_wait(&self, command: &str, working_dir: Option<&str>) -> Result<i32> {
let status = spawn(command, working_dir)?.wait()?;
Ok(status.code().unwrap_or(-1))
}
pub(crate) fn is_admin(&self) -> bool {
process::is_root()
}
pub(crate) fn pixel_get_color(&self, p: Point) -> Result<u32> {
let _guard = self.lock();
pixel::color_at(p)
}
pub(crate) fn pixel_search(
&self,
area: Rect,
colour: u32,
variation: u32,
step: u32,
) -> Result<Option<Point>> {
let _guard = self.lock();
Ok(pixel::Capture::of(area)?.search(colour, variation, step))
}
pub(crate) fn pixel_checksum(&self, area: Rect, step: u32) -> Result<u32> {
let _guard = self.lock();
Ok(pixel::Capture::of(area)?.checksum(step))
}
pub(crate) fn set_option(&self, option: &str, value: i32) -> Result<i32> {
let mut options = self.options.write();
let read_only = value == autoitx_sys::AU3_INTDEFAULT;
crate::options::apply_named(&mut options, option, value, read_only)
}
pub(crate) fn sleep(&self, d: Duration) {
std::thread::sleep(d);
}
}
fn spawn(command: &str, working_dir: Option<&str>) -> std::io::Result<std::process::Child> {
let mut cmd = std::process::Command::new("/bin/sh");
cmd.arg("-c").arg(command);
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
cmd.spawn()
}
unsafe impl Send for Inner {}
unsafe impl Sync for Inner {}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::TitleMatchMode;
fn offline() -> Inner {
Inner {
lock: ReentrantMutex::new(()),
options: RwLock::new(Options::default()),
handles: Mutex::new(HashMap::new()),
}
}
#[test]
fn a_handle_is_stable_for_the_same_window() {
let inner = offline();
let w = window::Window::for_test(501, "Untitled - TextEdit");
let first = inner.handle_for(&w);
let second = inner.handle_for(&w);
assert_eq!(first, second);
assert_ne!(first, 0);
}
#[test]
fn different_windows_get_different_handles() {
let inner = offline();
let a = inner.handle_for(&window::Window::for_test(501, "Report"));
let b = inner.handle_for(&window::Window::for_test(501, "Invoice"));
let c = inner.handle_for(&window::Window::for_test(502, "Report"));
assert_ne!(a, b, "same process, different titles");
assert_ne!(a, c, "same title, different processes");
}
#[test]
fn a_handle_selector_resolves_back_to_what_was_pinned() {
let inner = offline();
let handle = inner.handle_for(&window::Window::for_test(4242, "Save changes?"));
let pinned = inner
.pinned(&Selector::handle(handle))
.expect("the handle was just minted");
assert_eq!(pinned.pid, 4242);
assert_eq!(pinned.title, "Save changes?");
}
#[test]
fn an_unknown_handle_is_not_mistaken_for_an_unpinned_selector() {
let inner = offline();
assert!(inner.pinned(&Selector::handle(0xDEAD_BEEF)).is_none());
}
#[test]
fn options_can_be_read_and_set_by_autoit_name() {
let inner = offline();
assert_eq!(
inner.options().win_title_match_mode,
TitleMatchMode::StartsWith
);
let previous = inner
.set_option("WinTitleMatchMode", 2)
.expect("known option");
assert_eq!(previous, 1);
assert_eq!(
inner.options().win_title_match_mode,
TitleMatchMode::Substring
);
}
#[test]
fn the_read_only_sentinel_reports_without_changing() {
let inner = offline();
let value = inner
.set_option("SendKeyDelay", autoitx_sys::AU3_INTDEFAULT)
.expect("known option");
assert_eq!(value, 5);
assert_eq!(inner.options().send_key_delay, Duration::from_millis(5));
}
}