use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(i32)]
pub enum CoordMode {
ActiveWindow = 0,
#[default]
Screen = 1,
Client = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(i32)]
pub enum TitleMatchMode {
#[default]
StartsWith = 1,
Substring = 2,
Exact = 3,
Advanced = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(i32)]
pub enum TextMatchMode {
#[default]
Complete = 1,
Quick = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
#[non_exhaustive]
pub enum ShowState {
Hide = 0,
ShowNormal = 1,
ShowMinimized = 2,
Maximize = 3,
ShowNoActivate = 4,
Show = 5,
Minimize = 6,
ShowMinNoActive = 7,
ShowNa = 8,
Restore = 9,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WinState: i32 {
const EXISTS = 1;
const VISIBLE = 2;
const ENABLED = 4;
const ACTIVE = 8;
const MINIMIZED = 16;
const MAXIMIZED = 32;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum KeyMap {
#[default]
AsWritten,
PortableShortcuts,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Speed(u8);
impl Speed {
pub const INSTANT: Self = Self(0);
pub const DEFAULT: Self = Self(10);
pub const SLOWEST: Self = Self(100);
#[must_use]
pub const fn new(v: u8) -> Self {
Self(if v > 100 { 100 } else { v })
}
#[must_use]
pub const fn get(self) -> i32 {
self.0 as i32
}
}
impl Default for Speed {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Options {
pub mouse_coord_mode: CoordMode,
pub pixel_coord_mode: CoordMode,
pub caret_coord_mode: CoordMode,
pub win_title_match_mode: TitleMatchMode,
pub win_title_match_case_insensitive: bool,
pub win_text_match_mode: TextMatchMode,
pub win_detect_hidden_text: bool,
pub win_search_children: bool,
pub send_key_delay: Duration,
pub send_key_down_delay: Duration,
pub send_capslock_mode: bool,
pub mouse_click_delay: Duration,
pub mouse_click_down_delay: Duration,
pub mouse_click_drag_delay: Duration,
pub win_wait_delay: Duration,
pub key_map: KeyMap,
}
impl Options {
#[must_use]
pub const fn with_key_map(mut self, key_map: KeyMap) -> Self {
self.key_map = key_map;
self
}
}
impl Default for Options {
fn default() -> Self {
Self {
mouse_coord_mode: CoordMode::Screen,
pixel_coord_mode: CoordMode::Screen,
caret_coord_mode: CoordMode::Screen,
win_title_match_mode: TitleMatchMode::StartsWith,
win_title_match_case_insensitive: false,
win_text_match_mode: TextMatchMode::Complete,
win_detect_hidden_text: false,
win_search_children: false,
send_key_delay: Duration::from_millis(5),
send_key_down_delay: Duration::from_millis(5),
send_capslock_mode: true,
mouse_click_delay: Duration::from_millis(10),
mouse_click_down_delay: Duration::from_millis(10),
mouse_click_drag_delay: Duration::from_millis(250),
win_wait_delay: Duration::from_millis(250),
key_map: KeyMap::AsWritten,
}
}
}
#[cfg(all(target_os = "macos", not(feature = "mock-loader")))]
pub(crate) fn apply_named(
options: &mut Options,
name: &str,
value: i32,
read_only: bool,
) -> crate::Result<i32> {
fn millis(field: &mut Duration, value: i32, read_only: bool) -> i32 {
let previous = field.as_millis() as i32;
if !read_only && value >= 0 {
*field = Duration::from_millis(value as u64);
}
previous
}
fn flag(field: &mut bool, value: i32, read_only: bool) -> i32 {
let previous = i32::from(*field);
if !read_only {
*field = value != 0;
}
previous
}
fn coords(field: &mut CoordMode, value: i32, read_only: bool) -> i32 {
let previous = *field as i32;
if !read_only {
*field = match value {
0 => CoordMode::ActiveWindow,
2 => CoordMode::Client,
_ => CoordMode::Screen,
};
}
previous
}
let previous = match name.to_ascii_lowercase().as_str() {
"mousecoordmode" => coords(&mut options.mouse_coord_mode, value, read_only),
"pixelcoordmode" => coords(&mut options.pixel_coord_mode, value, read_only),
"caretcoordmode" => coords(&mut options.caret_coord_mode, value, read_only),
"wintitlematchmode" => {
let sign = if options.win_title_match_case_insensitive {
-1
} else {
1
};
let previous = options.win_title_match_mode as i32 * sign;
if !read_only {
options.win_title_match_case_insensitive = value < 0;
options.win_title_match_mode = match value.abs() {
2 => TitleMatchMode::Substring,
3 => TitleMatchMode::Exact,
4 => TitleMatchMode::Advanced,
_ => TitleMatchMode::StartsWith,
};
}
previous
}
"wintextmatchmode" => {
let previous = options.win_text_match_mode as i32;
if !read_only {
options.win_text_match_mode = if value == 2 {
TextMatchMode::Quick
} else {
TextMatchMode::Complete
};
}
previous
}
"windetecthiddentext" => flag(&mut options.win_detect_hidden_text, value, read_only),
"winsearchchildren" => flag(&mut options.win_search_children, value, read_only),
"sendcapslockmode" => flag(&mut options.send_capslock_mode, value, read_only),
"sendkeydelay" => millis(&mut options.send_key_delay, value, read_only),
"sendkeydowndelay" => millis(&mut options.send_key_down_delay, value, read_only),
"mouseclickdelay" => millis(&mut options.mouse_click_delay, value, read_only),
"mouseclickdowndelay" => millis(&mut options.mouse_click_down_delay, value, read_only),
"mouseclickdragdelay" => millis(&mut options.mouse_click_drag_delay, value, read_only),
"winwaitdelay" => millis(&mut options.win_wait_delay, value, read_only),
_ => {
return Err(crate::Error::Platform {
operation: "set an option it does not model (see `Options` for the ones it does)",
platform: "the macOS backend",
});
}
};
Ok(previous)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(target_os = "macos", not(feature = "mock-loader")))]
#[test]
fn the_read_only_sentinel_reports_every_option_without_changing_any() {
let mut o = Options::default();
let before = o;
for name in [
"MouseCoordMode",
"PixelCoordMode",
"CaretCoordMode",
"WinTitleMatchMode",
"WinTextMatchMode",
"WinDetectHiddenText",
"WinSearchChildren",
"SendCapslockMode",
"SendKeyDelay",
"SendKeyDownDelay",
"MouseClickDelay",
"MouseClickDownDelay",
"MouseClickDragDelay",
"WinWaitDelay",
] {
apply_named(&mut o, name, autoitx_sys::AU3_INTDEFAULT, true)
.unwrap_or_else(|e| panic!("{name}: {e}"));
}
assert_eq!(o, before, "a read-only call changed something");
}
#[cfg(all(target_os = "macos", not(feature = "mock-loader")))]
#[test]
fn a_negative_title_match_mode_means_case_insensitive() {
let mut o = Options::default();
apply_named(&mut o, "WinTitleMatchMode", -2, false).expect("known option");
assert_eq!(o.win_title_match_mode, TitleMatchMode::Substring);
assert!(o.win_title_match_case_insensitive);
let read = apply_named(
&mut o,
"WinTitleMatchMode",
autoitx_sys::AU3_INTDEFAULT,
true,
)
.expect("known option");
assert_eq!(read, -2);
}
#[cfg(all(target_os = "macos", not(feature = "mock-loader")))]
#[test]
fn names_are_matched_case_insensitively_like_autoit() {
let mut o = Options::default();
assert_eq!(
apply_named(&mut o, "sendkeydelay", 20, false).expect("known option"),
5
);
assert_eq!(o.send_key_delay, Duration::from_millis(20));
assert_eq!(
apply_named(&mut o, "SENDKEYDELAY", 30, false).expect("known option"),
20
);
}
#[cfg(all(target_os = "macos", not(feature = "mock-loader")))]
#[test]
fn an_unmodelled_option_is_an_error_rather_than_a_silent_no_op() {
let mut o = Options::default();
assert!(apply_named(&mut o, "TrayIconDebug", 1, false).is_err());
assert!(apply_named(&mut o, "SendKeyDelays", 1, false).is_err());
}
#[test]
fn defaults_match_the_autoit_table() {
let o = Options::default();
assert_eq!(o.mouse_coord_mode, CoordMode::Screen);
assert_eq!(o.win_title_match_mode, TitleMatchMode::StartsWith);
assert_eq!(o.send_key_delay, Duration::from_millis(5));
assert_eq!(o.win_wait_delay, Duration::from_millis(250));
assert_eq!(o.mouse_click_drag_delay, Duration::from_millis(250));
assert!(!o.win_detect_hidden_text);
assert!(o.send_capslock_mode);
}
#[test]
fn enum_discriminants_match_autoits_numbering() {
assert_eq!(CoordMode::ActiveWindow as i32, 0);
assert_eq!(CoordMode::Screen as i32, 1);
assert_eq!(TitleMatchMode::StartsWith as i32, 1);
assert_eq!(TitleMatchMode::Advanced as i32, 4);
assert_eq!(ShowState::Maximize as i32, 3);
assert_eq!(ShowState::Restore as i32, 9);
}
#[test]
fn speed_clamps_instead_of_wrapping() {
assert_eq!(Speed::new(200).get(), 100);
assert_eq!(Speed::new(0), Speed::INSTANT);
assert_eq!(Speed::default(), Speed::DEFAULT);
assert_eq!(Speed::DEFAULT.get(), 10);
}
#[test]
fn win_state_flags_compose() {
let s = WinState::EXISTS | WinState::VISIBLE | WinState::ACTIVE;
assert!(s.contains(WinState::ACTIVE));
assert!(!s.contains(WinState::MINIMIZED));
assert_eq!(s.bits(), 1 + 2 + 8);
}
}