Skip to main content

automata_windows/
lib.rs

1pub type Result<T> = anyhow::Result<T>;
2
3/// Window state action for [`set_window_state`].
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum WindowAction {
6    Minimize,
7    Maximize,
8    Restore,
9    Close,
10}
11
12#[cfg(target_os = "windows")]
13mod browser;
14mod clipboard;
15mod desktop;
16mod dialog;
17#[cfg(target_os = "windows")]
18mod element;
19#[cfg(not(target_os = "windows"))]
20#[path = "element_stub.rs"]
21mod element;
22mod element_info;
23mod error;
24mod input;
25mod locator;
26mod mouse;
27mod mouse_hook;
28mod overlay;
29mod process;
30mod selector;
31mod task_view;
32mod taskbar;
33mod uia_probe;
34mod util;
35mod window;
36
37#[cfg(target_os = "windows")]
38mod element_tree;
39#[cfg(target_os = "windows")]
40mod windows_info;
41
42pub use clipboard::*;
43pub use desktop::*;
44pub use dialog::*;
45pub use element::*;
46pub use element_info::*;
47pub use error::*;
48pub use input::*;
49pub use locator::*;
50pub use mouse::*;
51pub use mouse_hook::*;
52pub use overlay::*;
53pub use process::*;
54pub use selector::*;
55pub use task_view::*;
56pub use taskbar::*;
57pub use uia_probe::*;
58pub use window::*;
59
60#[cfg(target_os = "windows")]
61pub use element_tree::*;
62#[cfg(target_os = "windows")]
63pub use windows_info::*;
64
65/// Run a UI-Automata YAML workflow from an in-memory string.
66///
67/// `params` is the CLI override map (snake_case keys).
68#[cfg(target_os = "windows")]
69pub fn run_workflow_str(
70    yaml: &str,
71    params: &std::collections::HashMap<String, String>,
72) -> Result<()> {
73    let workflow =
74        ui_automata::yaml::WorkflowFile::load_from_str(yaml, params).map_err(anyhow::Error::msg)?;
75    let desktop = Desktop::new();
76    let mut executor = ui_automata::Executor::new(desktop);
77    log::info!("running workflow '{}' ...", workflow.name);
78    workflow
79        .run(&mut executor, None, None)
80        .map(|_| ())
81        .map_err(anyhow::Error::msg)
82}
83
84/// Workspace crates that get DEBUG level in the log file; everything else is INFO.
85const OWN_CRATES: &[&str] = &["automata_browser", "automata_windows", "ui_automata"];
86
87/// Build a file dispatch: INFO for third-party crates, DEBUG for workspace crates.
88/// Returns `None` if the file can't be opened.
89fn make_file_dispatch(log_path: &std::path::Path) -> Option<fern::Dispatch> {
90    match fern::log_file(log_path) {
91        Ok(file) => {
92            let mut dispatch = fern::Dispatch::new()
93                .level(log::LevelFilter::Info)
94                .format(|out, message, record| {
95                    out.finish(format_args!(
96                        "[{}] [{}] {}",
97                        chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
98                        record.level(),
99                        message
100                    ))
101                })
102                .chain(file);
103            for krate in OWN_CRATES {
104                dispatch = dispatch.level_for(*krate, log::LevelFilter::Debug);
105            }
106            Some(dispatch)
107        }
108        Err(e) => {
109            eprintln!("could not open log file {}: {e}", log_path.display());
110            None
111        }
112    }
113}
114
115/// File-only logger — no stdout output. Used in pipe mode (spawned by automata-client).
116///
117/// Call once at startup before any `log::` calls.
118pub fn init_logging_file_only(log_path: &std::path::Path) {
119    let mut dispatch = fern::Dispatch::new();
120    if let Some(file_dispatch) = make_file_dispatch(log_path) {
121        dispatch = dispatch.chain(file_dispatch);
122    }
123    if let Err(e) = dispatch.apply() {
124        eprintln!("failed to initialise logging: {e}");
125    }
126}
127
128/// Send `CTRL_C_EVENT` to a process by PID. Returns `true` on success.
129///
130/// The target process must be in the same console process group (the default
131/// when spawned via `std::process::Command` without `CREATE_NEW_PROCESS_GROUP`).
132#[cfg(target_os = "windows")]
133pub fn send_ctrl_c(pid: u32) -> bool {
134    use windows::Win32::System::Console::{CTRL_C_EVENT, GenerateConsoleCtrlEvent};
135    unsafe { GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid).is_ok() }
136}
137
138#[cfg(not(target_os = "windows"))]
139pub fn send_ctrl_c(_pid: u32) -> bool {
140    false
141}
142
143/// Initialise logging: coloured stdout (Info for everything, override with `RUST_LOG`) and optional
144/// file (Info for third-party crates, Debug for workspace crates).
145///
146/// Call once at startup before any `log::` calls.
147pub fn init_logging(log_path: Option<&std::path::Path>) {
148    let stdout_level = std::env::var("RUST_LOG")
149        .ok()
150        .and_then(|v| v.parse().ok())
151        .unwrap_or(log::LevelFilter::Info);
152
153    let colors = fern::colors::ColoredLevelConfig::new()
154        .error(fern::colors::Color::Red)
155        .warn(fern::colors::Color::Yellow)
156        .info(fern::colors::Color::Green)
157        .debug(fern::colors::Color::Cyan)
158        .trace(fern::colors::Color::White);
159
160    let stdout_dispatch = fern::Dispatch::new()
161        .level(stdout_level)
162        .format(move |out, message, record| {
163            out.finish(format_args!(
164                "[{} {:<5} {}] {}",
165                chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"),
166                colors.color(record.level()),
167                record.target(),
168                message
169            ))
170        })
171        .chain(std::io::stdout());
172
173    let mut dispatch = fern::Dispatch::new().chain(stdout_dispatch);
174
175    if let Some(path) = log_path {
176        if let Some(file_dispatch) = make_file_dispatch(path) {
177            dispatch = dispatch.chain(file_dispatch);
178        }
179    }
180
181    if let Err(e) = dispatch.apply() {
182        eprintln!("failed to initialise logging: {e}");
183    }
184}
185
186/// Initialise COM (MTA) on the calling thread. Call once before using any UIA functions.
187pub fn init_com() {
188    #[cfg(target_os = "windows")]
189    unsafe {
190        use windows::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx};
191        let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
192    }
193}