use std::time::Duration;
use xa11y::{App, AppExt};
use super::snapshot::DEFAULT_TIMEOUT_MS;
use super::types::{ActAction, ComputerAct};
pub fn validate(step: &ComputerAct, tool: &str) -> Result<(), String> {
if step
.selector
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.is_none()
{
return Err(format!(
"{tool}: a semantic action requires `selector` (the element to act on); \
use `key`/`text` for typing, or `wait` to pause"
));
}
super::surface::validate_act(step)?;
if step.nth == Some(0) {
return Err(format!(
"{tool}: `nth` is 1-based; use 1 for the first match"
));
}
if step.held.is_some() {
return Err(format!(
"{tool}: `held` applies to keyboard and pointer steps — a semantic action \
carries no chord; chain a keyboard step via `then` if you need one"
));
}
match step.action.unwrap_or_default() {
ActAction::SetValue | ActAction::TypeText | ActAction::PerformAction
if step.value.is_none() =>
{
return Err(format!(
"{tool}: action `{}` requires `value`",
action_name(step.action.unwrap_or_default())
));
}
ActAction::SetNumericValue if step.numeric_value.is_none() => {
return Err(format!(
"{tool}: action `set_numeric_value` requires `numeric_value`"
));
}
ActAction::SelectText => {
let Some(range) = &step.range else {
return Err(format!(
"{tool}: action `select_text` requires `range` [start, end]"
));
};
if range.len() != 2 {
return Err(format!(
"{tool}: `range` takes exactly 2 numbers [start, end], got {}",
range.len()
));
}
if range[0] > range[1] {
return Err(format!(
"{tool}: `range` start ({}) must be <= end ({})",
range[0], range[1]
));
}
}
_ => {}
}
Ok(())
}
pub fn run_step(step: &ComputerAct, tool: &str) -> Result<String, String> {
let timeout = Duration::from_millis(step.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS));
let selector = step
.selector
.as_deref()
.expect("validated: semantic step carries a selector");
let nth = step.nth.unwrap_or(1);
let action = step.action.unwrap_or_default();
if let Some(surface_kind) = step.surface {
let surface =
super::surface::resolve(surface_kind, timeout).map_err(|e| format!("{tool}: {e}"))?;
return run_step_on_surface(step, surface, timeout, tool);
}
let name = step
.name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let app = match (name, step.pid) {
(Some(name), None) => App::by_name(name, timeout),
(None, Some(pid)) => App::by_pid(pid, timeout),
_ => unreachable!("validated: semantic step carries exactly one app scope"),
}
.map_err(|e| super::errors::render_app_miss(tool, &e))?;
let locator = app.locator(selector.trim()).nth(nth).with_timeout(timeout);
dispatch_action(&locator, action, step, app.name.clone(), tool)
}
pub(crate) fn run_step_on_surface(
step: &ComputerAct,
surface: xa11y::ShellSurface,
timeout: Duration,
tool: &str,
) -> Result<String, String> {
let selector = step
.selector
.as_deref()
.expect("validated: semantic step carries a selector");
let nth = step.nth.unwrap_or(1);
let action = step.action.unwrap_or_default();
let locator = surface
.locator(selector.trim())
.nth(nth)
.with_timeout(timeout);
dispatch_action(&locator, action, step, surface.name.clone(), tool)
}
fn dispatch_action(
locator: &xa11y::Locator,
action: ActAction,
step: &ComputerAct,
target_name: String,
tool: &str,
) -> Result<String, String> {
let selector = step
.selector
.as_deref()
.expect("validated: semantic step carries a selector");
let result = match action {
ActAction::Press => locator.press(),
ActAction::Focus => locator.focus(),
ActAction::Blur => locator.blur(),
ActAction::Toggle => locator.toggle(),
ActAction::Select => locator.select(),
ActAction::Expand => locator.expand(),
ActAction::Collapse => locator.collapse(),
ActAction::ShowMenu => locator.show_menu(),
ActAction::Increment => locator.increment(),
ActAction::Decrement => locator.decrement(),
ActAction::ScrollIntoView => locator.scroll_into_view(),
ActAction::SetValue => {
let value = step.value.as_deref().unwrap_or_default();
match locator.set_value(value) {
Err(xa11y::Error::TextValueNotSupported) => match value.trim().parse::<f64>() {
Ok(numeric) => locator.set_numeric_value(numeric),
Err(_) => Err(xa11y::Error::TextValueNotSupported),
},
result => result,
}
}
ActAction::SetNumericValue => {
locator.set_numeric_value(step.numeric_value.unwrap_or_default())
}
ActAction::SelectText => {
let range = step.range.as_deref().unwrap_or_default();
locator.select_text(
range.first().copied().unwrap_or(0),
range.get(1).copied().unwrap_or(0),
)
}
ActAction::TypeText => locator.type_text(step.value.as_deref().unwrap_or_default()),
ActAction::PerformAction => {
locator.perform_action(step.value.as_deref().unwrap_or_default())
}
};
result.map_err(|e| super::errors::render(tool, &action_name(action).to_lowercase(), &e))?;
Ok(format!(
"{} `{}` ({})",
action_name(action),
selector,
target_name
))
}
pub fn action_name(action: ActAction) -> &'static str {
match action {
ActAction::Press => "press",
ActAction::Focus => "focus",
ActAction::Blur => "blur",
ActAction::Toggle => "toggle",
ActAction::Select => "select",
ActAction::Expand => "expand",
ActAction::Collapse => "collapse",
ActAction::ShowMenu => "show_menu",
ActAction::Increment => "increment",
ActAction::Decrement => "decrement",
ActAction::ScrollIntoView => "scroll_into_view",
ActAction::SetValue => "set_value",
ActAction::SetNumericValue => "set_numeric_value",
ActAction::SelectText => "select_text",
ActAction::TypeText => "type_text",
ActAction::PerformAction => "perform_action",
}
}