use super::keyboard::{self, MAX_WAIT_MS};
use super::mouse;
use super::types::{ComputerControl, ControlOutput};
pub const CONTROL_CHAIN_MAX_DEPTH: usize = 8;
pub async fn control(input: &ComputerControl) -> Result<ControlOutput, String> {
validate_chain(input, CONTROL_CHAIN_MAX_DEPTH)?;
let owned = input.clone();
tokio::task::spawn_blocking(move || control_blocking(&owned))
.await
.map_err(|e| format!("computer_control: blocking task failed: {e}"))?
}
pub(crate) fn validate_chain(step: &ComputerControl, remaining: usize) -> Result<(), String> {
if remaining == 0 {
return Err(format!(
"computer_control: `then` chain exceeds {CONTROL_CHAIN_MAX_DEPTH} steps"
));
}
validate_step(step)?;
if let Some(next) = &step.then {
validate_chain(next, remaining - 1)?;
}
Ok(())
}
fn validate_step(step: &ComputerControl) -> Result<(), String> {
if let Some(ms) = step.wait {
let extra: Vec<&str> = [
(step.action.is_some(), "`action`"),
(step.x.is_some(), "`x`"),
(step.y.is_some(), "`y`"),
(step.button.is_some(), "`button`"),
(step.dx.is_some(), "`dx`"),
(step.dy.is_some(), "`dy`"),
(step.app.is_some(), "`app`"),
(step.pid.is_some(), "`pid`"),
(step.surface.is_some(), "`surface`"),
(step.selector.is_some(), "`selector`"),
(step.nth.is_some(), "`nth`"),
(step.anchor.is_some(), "`anchor`"),
(step.count.is_some(), "`count`"),
(step.held.is_some(), "`held`"),
(step.x2.is_some(), "`x2`"),
(step.y2.is_some(), "`y2`"),
(step.to_selector.is_some(), "`to_selector`"),
(step.duration_ms.is_some(), "`duration_ms`"),
(step.key.is_some(), "`key`"),
(step.text.is_some(), "`text`"),
]
.iter()
.filter(|(present, _)| *present)
.map(|(_, name)| *name)
.collect();
if !extra.is_empty() {
return Err(format!(
"computer_control: `wait` is a standalone step — it takes no {}",
extra.join(", ")
));
}
if ms == 0 {
return Err("computer_control: `wait` must be >= 1 ms".into());
}
if ms > MAX_WAIT_MS {
return Err(format!(
"computer_control: `wait` is capped at {MAX_WAIT_MS} ms per step — \
chain several wait steps for longer pauses"
));
}
return Ok(());
}
let key = step.key.as_deref().map(str::trim).filter(|s| !s.is_empty());
let text = step.text.as_deref().filter(|t| !t.is_empty());
if key.is_some() && text.is_some() {
return Err("computer_control: provide `key` or `text`, not both".into());
}
if key.is_some() || text.is_some() {
for (present, name) in [
(step.action.is_some(), "`action`"),
(step.x.is_some(), "`x`"),
(step.y.is_some(), "`y`"),
(step.button.is_some(), "`button`"),
(step.dx.is_some(), "`dx`"),
(step.dy.is_some(), "`dy`"),
(step.anchor.is_some(), "`anchor`"),
(step.count.is_some(), "`count`"),
(step.x2.is_some(), "`x2`"),
(step.y2.is_some(), "`y2`"),
(step.to_selector.is_some(), "`to_selector`"),
(step.duration_ms.is_some(), "`duration_ms`"),
(step.app.is_some(), "`app`"),
(step.pid.is_some(), "`pid`"),
(step.surface.is_some(), "`surface`"),
(step.selector.is_some(), "`selector`"),
(step.nth.is_some(), "`nth`"),
] {
if present {
return Err(format!(
"computer_control: {name} belongs to a pointer step — a keyboard step \
types into the focused element; click the target element in an \
earlier step (or coordinates), then chain the typing via `then`"
));
}
}
if let Some(text) = text {
if step.held.is_some() {
return Err(format!(
"computer_control: `held` is rejected with `text` ({text:?}) — backends \
synthesize case/shift; use `key` with `held` for chords"
));
}
} else {
keyboard::parse_key(key.unwrap_or_default(), "computer_control")?;
}
keyboard::parse_keys(step.held.as_deref().unwrap_or_default(), "computer_control")?;
return Ok(());
}
mouse::validate(step, "computer_control")
}
fn control_blocking(input: &ComputerControl) -> Result<ControlOutput, String> {
let sim =
keyboard::shared_sim().map_err(|e| format!("computer_control: input backend: {e}"))?;
let mut report = String::new();
let mut step = input;
let mut index = 1usize;
loop {
let sent = run_step(&sim, step).map_err(|e| {
if report.is_empty() {
format!("computer_control: step {index}: {e}")
} else {
format!("computer_control: step {index}: {e} — completed: {report}")
}
})?;
if !report.is_empty() {
report.push_str(" → ");
}
report.push_str(&sent);
match &step.then {
Some(next) => {
step = next;
index += 1;
}
None => return Ok(ControlOutput { sent: report }),
}
}
}
fn run_step(sim: &xa11y::InputSim, step: &ComputerControl) -> Result<String, String> {
if let Some(ms) = step.wait {
return Ok(keyboard::run_wait_step(ms));
}
let key = step.key.as_deref();
let text = step.text.as_deref();
if keyboard::is_keyboard_step(key, text) {
keyboard::run_keyboard_step(
sim,
&keyboard::KeyboardStep {
key,
text,
held: step.held.as_deref(),
},
"computer_control",
)
} else {
mouse::run_step(sim, step, "computer_control")
}
}