pub mod appactivate;
pub mod backend;
pub mod inputbox;
pub mod memory;
pub mod msgbox;
pub mod native;
pub mod sendkeys;
pub mod shell;
use std::sync::{Mutex, OnceLock};
pub use appactivate::{AppActivateRecord, AppActivateRequest};
pub use backend::InteractionBackend;
pub use inputbox::{InputBoxRecord, InputBoxRequest};
pub use msgbox::{
MsgBoxButton, MsgBoxButtonSet, MsgBoxIcon, MsgBoxModality, MsgBoxRecord, MsgBoxRequest,
};
pub use sendkeys::{Keystroke, SendKey, SendKeysRecord, SendKeysRequest};
pub use shell::{ShellRecord, ShellRequest, WindowStyle};
static BACKEND: OnceLock<Mutex<Box<dyn InteractionBackend>>> = OnceLock::new();
fn backend() -> &'static Mutex<Box<dyn InteractionBackend>> {
BACKEND.get_or_init(|| Mutex::new(default_backend()))
}
fn default_backend() -> Box<dyn InteractionBackend> {
Box::new(native::NativeBackend::new())
}
pub fn set_backend(new_backend: Box<dyn InteractionBackend>) {
*backend().lock().unwrap_or_else(|e| e.into_inner()) = new_backend;
}
pub fn reset_backend() {
set_backend(default_backend());
}
pub fn command_args() -> Vec<String> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.command_args()
}
pub fn command_string() -> String {
command_args().join(" ")
}
pub fn do_events() -> i16 {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.do_events()
}
pub fn beep() {
backend().lock().unwrap_or_else(|e| e.into_inner()).beep()
}
pub fn stop() {
backend().lock().unwrap_or_else(|e| e.into_inner()).stop()
}
pub fn msg_box(request: &MsgBoxRequest) -> crate::error::VBResult<MsgBoxButton> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.msg_box(request)
}
pub fn input_box(request: &InputBoxRequest) -> crate::error::VBResult<String> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.input_box(request)
}
pub fn app_activate(request: &AppActivateRequest) -> crate::error::VBResult<()> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.app_activate(request)
}
pub fn send_keys(request: &SendKeysRequest) -> crate::error::VBResult<()> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.send_keys(request)
}
pub fn shell(request: &ShellRequest) -> crate::error::VBResult<f64> {
backend()
.lock()
.unwrap_or_else(|e| e.into_inner())
.shell(request)
}
pub fn with_memory_backend<R>(f: impl FnOnce(&memory::MemoryBackend) -> R) -> Option<R> {
let guard = backend().lock().unwrap_or_else(|e| e.into_inner());
guard
.as_any()
.downcast_ref::<memory::MemoryBackend>()
.map(f)
}
pub fn reset() {
reset_backend();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::test_support::lock_test;
#[test]
fn default_backend_returns_args() {
let _guard = lock_test();
let _args = command_args();
}
#[test]
fn command_string_joins_with_spaces() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_args(vec![
"foo".into(),
"bar baz".into(),
])));
assert_eq!(command_string(), "foo bar baz");
reset_backend();
}
#[test]
fn command_string_empty_when_no_args() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
assert_eq!(command_string(), "");
reset_backend();
}
#[test]
fn do_events_returns_zero() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
assert_eq!(do_events(), 0i16);
reset_backend();
}
#[test]
fn beep_does_not_panic() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
beep();
reset_backend();
}
#[test]
fn stop_does_not_panic() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
stop();
reset_backend();
}
#[test]
fn msg_box_routes_to_the_active_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_msgbox_responses([
msgbox::MsgBoxButton::Retry,
])));
let request = MsgBoxRequest::parse("continue?", 5).unwrap();
assert_eq!(msg_box(&request).unwrap(), msgbox::MsgBoxButton::Retry);
reset_backend();
}
#[test]
fn msg_box_defaults_without_scripted_responses() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
let request = MsgBoxRequest::parse("proceed?", 1).unwrap();
assert_eq!(msg_box(&request).unwrap(), msgbox::MsgBoxButton::Ok);
reset_backend();
}
#[test]
fn input_box_routes_to_the_active_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_input_responses([
"scripted",
])));
let request = InputBoxRequest::new("value?").with_default("default");
assert_eq!(input_box(&request).unwrap(), "scripted");
reset_backend();
}
#[test]
fn input_box_defaults_without_scripted_responses() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
let request = InputBoxRequest::new("value?").with_default("fallback");
assert_eq!(input_box(&request).unwrap(), "fallback");
reset_backend();
}
#[test]
fn app_activate_routes_to_the_active_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_activate_responses([
false,
])));
let request = AppActivateRequest::new("Calculator");
assert!(app_activate(&request).is_err());
reset_backend();
}
#[test]
fn app_activate_defaults_without_scripted_responses() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
assert!(app_activate(&AppActivateRequest::new("Calculator")).is_ok());
reset_backend();
}
#[test]
fn send_keys_routes_to_the_active_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_sendkeys_responses([
false,
])));
let request = SendKeysRequest::parse("^c", true).unwrap();
assert!(send_keys(&request).is_err());
reset_backend();
}
#[test]
fn send_keys_defaults_without_scripted_responses() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
let request = SendKeysRequest::parse("Hello{ENTER}", false).unwrap();
assert!(send_keys(&request).is_ok());
assert_eq!(
with_memory_backend(|backend| backend.sendkeys_requests().len()),
Some(1)
);
reset_backend();
}
#[test]
fn send_keys_rejects_malformed_strings_before_reaching_the_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
assert_eq!(
SendKeysRequest::parse("{NOT-A-KEY}", false)
.unwrap_err()
.number,
5
);
assert_eq!(
with_memory_backend(|backend| backend.sendkeys_requests().len()),
Some(0)
);
reset_backend();
}
#[test]
fn shell_routes_to_the_active_backend() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::with_shell_responses([
31337.0,
])));
assert_eq!(shell(&ShellRequest::new("calc.exe")).unwrap(), 31337.0);
reset_backend();
}
#[test]
fn shell_synthesizes_task_ids_without_scripted_responses() {
let _guard = lock_test();
set_backend(Box::new(memory::MemoryBackend::new()));
let first = shell(&ShellRequest::new("notepad.exe")).unwrap();
let second = shell(&ShellRequest::new("calc.exe")).unwrap();
assert!(first > 0.0);
assert_ne!(first, second);
reset_backend();
}
}