cosh_tools/computer/
wait.rs1use std::time::{Duration, Instant};
16
17use xa11y::{App, AppExt, ElementState, Locator};
18
19use super::types::{ComputerWait, WaitObservation, WaitOutput, WaitState};
20
21pub const DEFAULT_WAIT_MS: u64 = 10_000;
25
26pub const MAX_WAIT_MS: u64 = 60_000;
29
30pub fn validate(input: &ComputerWait) -> Result<(), String> {
39 if input
40 .selector
41 .as_deref()
42 .map(str::trim)
43 .filter(|s| !s.is_empty())
44 .is_none()
45 {
46 return Err(
47 "computer_wait: `selector` is required — the element to watch, \
48 e.g. progress_bar[name='Exporting…']"
49 .to_string(),
50 );
51 }
52 let has_name = input
53 .name
54 .as_deref()
55 .map(str::trim)
56 .filter(|s| !s.is_empty())
57 .is_some();
58 if has_name && input.pid.is_some() {
59 return Err("computer_wait: provide `name` or `pid`, not both".to_string());
60 }
61 if !has_name && input.pid.is_none() {
62 return Err("computer_wait: provide `name` or `pid`".to_string());
63 }
64 if input.nth == Some(0) {
65 return Err("computer_wait: `nth` is 1-based; use 1 for the first match".to_string());
66 }
67 if input.timeout_ms.is_some_and(|ms| ms > MAX_WAIT_MS) {
68 return Err(format!(
69 "computer_wait: `timeout_ms` is capped at {MAX_WAIT_MS} ms per call — \
70 chain calls for longer waits"
71 ));
72 }
73 Ok(())
74}
75
76pub async fn wait(input: &ComputerWait) -> Result<WaitOutput, String> {
86 let input = input.clone();
87 tokio::task::spawn_blocking(move || wait_blocking(&input))
88 .await
89 .map_err(|e| format!("computer_wait: blocking task failed: {e}"))?
90}
91
92fn wait_blocking(input: &ComputerWait) -> Result<WaitOutput, String> {
93 validate(input)?;
94 let timeout = Duration::from_millis(input.timeout_ms.unwrap_or(DEFAULT_WAIT_MS));
95 let started = Instant::now();
99
100 let name = input
101 .name
102 .as_deref()
103 .map(str::trim)
104 .filter(|s| !s.is_empty());
105 let app_timeout = timeout.min(Duration::from_millis(super::snapshot::DEFAULT_TIMEOUT_MS));
111 let app = match (name, input.pid) {
112 (Some(name), None) => App::by_name(name, app_timeout),
113 (None, Some(pid)) => App::by_pid(pid, app_timeout),
114 _ => unreachable!("validated: computer_wait carries exactly one app scope"),
115 }
116 .map_err(|e| super::errors::render_app_miss("computer_wait", &e))?;
117
118 let selector = input
119 .selector
120 .as_deref()
121 .expect("validated: computer_wait carries a selector");
122 let locator = app.locator(selector.trim()).nth(input.nth.unwrap_or(1));
123 let state = wait_state(input.state.unwrap_or_default());
124 let remaining = timeout.saturating_sub(started.elapsed());
129 wait_on_locator(&locator, state, remaining, started)
130}
131
132pub(crate) fn wait_state(state: WaitState) -> ElementState {
134 match state {
135 WaitState::Attached => ElementState::Attached,
136 WaitState::Detached => ElementState::Detached,
137 WaitState::Visible => ElementState::Visible,
138 WaitState::Hidden => ElementState::Hidden,
139 WaitState::Enabled => ElementState::Enabled,
140 WaitState::Disabled => ElementState::Disabled,
141 WaitState::Focused => ElementState::Focused,
142 WaitState::Unfocused => ElementState::Unfocused,
143 }
144}
145
146pub(crate) fn wait_on_locator(
158 locator: &Locator,
159 state: ElementState,
160 timeout: Duration,
161 started: Instant,
162) -> Result<WaitOutput, String> {
163 let elapsed_ms = || u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
164 match locator.wait_for_state(state, timeout) {
165 Ok(Some(element)) => Ok(WaitOutput {
166 met: true,
167 elapsed_ms: elapsed_ms(),
168 observed: observation(&element.data().states, true),
169 }),
170 Ok(None) => Ok(WaitOutput {
173 met: true,
174 elapsed_ms: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
175 observed: WaitObservation {
176 attached: false,
177 visible: None,
178 enabled: None,
179 focused: None,
180 },
181 }),
182 Err(e) => Err(super::errors::render(
183 "computer_wait",
184 "wait for element state",
185 &e,
186 )),
187 }
188}
189
190fn observation(states: &xa11y::StateSet, attached: bool) -> WaitObservation {
191 if attached {
192 WaitObservation {
193 attached: true,
194 visible: Some(states.visible),
195 enabled: Some(states.enabled),
196 focused: Some(states.focused),
197 }
198 } else {
199 WaitObservation {
200 attached: false,
201 visible: None,
202 enabled: None,
203 focused: None,
204 }
205 }
206}