cosh_tools/computer/
touch.rs1use std::time::Duration;
16
17use xa11y::{App, AppExt};
18
19use super::snapshot::DEFAULT_TIMEOUT_MS;
20use super::types::{ActAction, ComputerAct};
21
22pub fn validate(step: &ComputerAct, tool: &str) -> Result<(), String> {
36 if step
37 .selector
38 .as_deref()
39 .map(str::trim)
40 .filter(|s| !s.is_empty())
41 .is_none()
42 {
43 return Err(format!(
44 "{tool}: a semantic action requires `selector` (the element to act on); \
45 use `key`/`text` for typing, or `wait` to pause"
46 ));
47 }
48 super::surface::validate_act(step)?;
49 if step.nth == Some(0) {
50 return Err(format!(
51 "{tool}: `nth` is 1-based; use 1 for the first match"
52 ));
53 }
54 if step.held.is_some() {
55 return Err(format!(
56 "{tool}: `held` applies to keyboard and pointer steps — a semantic action \
57 carries no chord; chain a keyboard step via `then` if you need one"
58 ));
59 }
60
61 match step.action.unwrap_or_default() {
64 ActAction::SetValue | ActAction::TypeText | ActAction::PerformAction
65 if step.value.is_none() =>
66 {
67 return Err(format!(
68 "{tool}: action `{}` requires `value`",
69 action_name(step.action.unwrap_or_default())
70 ));
71 }
72 ActAction::SetNumericValue if step.numeric_value.is_none() => {
73 return Err(format!(
74 "{tool}: action `set_numeric_value` requires `numeric_value`"
75 ));
76 }
77 ActAction::SelectText => {
78 let Some(range) = &step.range else {
79 return Err(format!(
80 "{tool}: action `select_text` requires `range` [start, end]"
81 ));
82 };
83 if range.len() != 2 {
84 return Err(format!(
85 "{tool}: `range` takes exactly 2 numbers [start, end], got {}",
86 range.len()
87 ));
88 }
89 if range[0] > range[1] {
90 return Err(format!(
91 "{tool}: `range` start ({}) must be <= end ({})",
92 range[0], range[1]
93 ));
94 }
95 }
96 _ => {}
97 }
98 Ok(())
99}
100
101pub fn run_step(step: &ComputerAct, tool: &str) -> Result<String, String> {
105 let timeout = Duration::from_millis(step.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS));
106 let selector = step
107 .selector
108 .as_deref()
109 .expect("validated: semantic step carries a selector");
110 let nth = step.nth.unwrap_or(1);
111 let action = step.action.unwrap_or_default();
112
113 if let Some(surface_kind) = step.surface {
116 let surface =
117 super::surface::resolve(surface_kind, timeout).map_err(|e| format!("{tool}: {e}"))?;
118 return run_step_on_surface(step, surface, timeout, tool);
119 }
120 let name = step
121 .name
122 .as_deref()
123 .map(str::trim)
124 .filter(|s| !s.is_empty());
125 let app = match (name, step.pid) {
126 (Some(name), None) => App::by_name(name, timeout),
127 (None, Some(pid)) => App::by_pid(pid, timeout),
128 _ => unreachable!("validated: semantic step carries exactly one app scope"),
129 }
130 .map_err(|e| super::errors::render_app_miss(tool, &e))?;
131 let locator = app.locator(selector.trim()).nth(nth).with_timeout(timeout);
132 dispatch_action(&locator, action, step, app.name.clone(), tool)
133}
134
135pub(crate) fn run_step_on_surface(
139 step: &ComputerAct,
140 surface: xa11y::ShellSurface,
141 timeout: Duration,
142 tool: &str,
143) -> Result<String, String> {
144 let selector = step
145 .selector
146 .as_deref()
147 .expect("validated: semantic step carries a selector");
148 let nth = step.nth.unwrap_or(1);
149 let action = step.action.unwrap_or_default();
150 let locator = surface
151 .locator(selector.trim())
152 .nth(nth)
153 .with_timeout(timeout);
154 dispatch_action(&locator, action, step, surface.name.clone(), tool)
155}
156
157fn dispatch_action(
160 locator: &xa11y::Locator,
161 action: ActAction,
162 step: &ComputerAct,
163 target_name: String,
164 tool: &str,
165) -> Result<String, String> {
166 let selector = step
167 .selector
168 .as_deref()
169 .expect("validated: semantic step carries a selector");
170 let result = match action {
171 ActAction::Press => locator.press(),
172 ActAction::Focus => locator.focus(),
173 ActAction::Blur => locator.blur(),
174 ActAction::Toggle => locator.toggle(),
175 ActAction::Select => locator.select(),
176 ActAction::Expand => locator.expand(),
177 ActAction::Collapse => locator.collapse(),
178 ActAction::ShowMenu => locator.show_menu(),
179 ActAction::Increment => locator.increment(),
180 ActAction::Decrement => locator.decrement(),
181 ActAction::ScrollIntoView => locator.scroll_into_view(),
182 ActAction::SetValue => {
183 let value = step.value.as_deref().unwrap_or_default();
184 match locator.set_value(value) {
188 Err(xa11y::Error::TextValueNotSupported) => match value.trim().parse::<f64>() {
189 Ok(numeric) => locator.set_numeric_value(numeric),
190 Err(_) => Err(xa11y::Error::TextValueNotSupported),
192 },
193 result => result,
194 }
195 }
196 ActAction::SetNumericValue => {
197 locator.set_numeric_value(step.numeric_value.unwrap_or_default())
198 }
199 ActAction::SelectText => {
200 let range = step.range.as_deref().unwrap_or_default();
201 locator.select_text(
202 range.first().copied().unwrap_or(0),
203 range.get(1).copied().unwrap_or(0),
204 )
205 }
206 ActAction::TypeText => locator.type_text(step.value.as_deref().unwrap_or_default()),
207 ActAction::PerformAction => {
208 locator.perform_action(step.value.as_deref().unwrap_or_default())
209 }
210 };
211 result.map_err(|e| super::errors::render(tool, &action_name(action).to_lowercase(), &e))?;
212
213 Ok(format!(
214 "{} `{}` ({})",
215 action_name(action),
216 selector,
217 target_name
218 ))
219}
220
221pub fn action_name(action: ActAction) -> &'static str {
223 match action {
224 ActAction::Press => "press",
225 ActAction::Focus => "focus",
226 ActAction::Blur => "blur",
227 ActAction::Toggle => "toggle",
228 ActAction::Select => "select",
229 ActAction::Expand => "expand",
230 ActAction::Collapse => "collapse",
231 ActAction::ShowMenu => "show_menu",
232 ActAction::Increment => "increment",
233 ActAction::Decrement => "decrement",
234 ActAction::ScrollIntoView => "scroll_into_view",
235 ActAction::SetValue => "set_value",
236 ActAction::SetNumericValue => "set_numeric_value",
237 ActAction::SelectText => "select_text",
238 ActAction::TypeText => "type_text",
239 ActAction::PerformAction => "perform_action",
240 }
241}