Skip to main content

agent_tui/
wait.rs

1//! Wait conditions for agent-tui
2//!
3//! Provides various wait conditions for polling until a condition is met
4//! or timeout occurs.
5
6use crate::session::Session;
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9
10/// Wait condition types
11#[derive(Debug, Clone)]
12pub enum WaitCondition {
13    /// Wait for text to appear on screen
14    Text(String),
15    /// Wait for element ref to exist
16    Element(String),
17    /// Wait for element to be focused
18    Focused(String),
19    /// Wait for element to NOT be visible
20    NotVisible(String),
21    /// Wait for screen to be stable (3 consecutive identical hashes)
22    Stable,
23    /// Wait for text to disappear from screen
24    TextGone(String),
25    /// Wait for element value to match (format: ref=expected_value)
26    Value { element: String, expected: String },
27}
28
29impl WaitCondition {
30    /// Parse a wait condition from string parameters
31    pub fn parse(
32        condition: Option<&str>,
33        target: Option<&str>,
34        text: Option<&str>,
35    ) -> Option<Self> {
36        match condition {
37            Some("text") => text.map(|t| WaitCondition::Text(t.to_string())),
38            Some("element") => target.map(|t| WaitCondition::Element(t.to_string())),
39            Some("focused") => target.map(|t| WaitCondition::Focused(t.to_string())),
40            Some("not_visible") => target.map(|t| WaitCondition::NotVisible(t.to_string())),
41            Some("stable") => Some(WaitCondition::Stable),
42            Some("text_gone") => target.map(|t| WaitCondition::TextGone(t.to_string())),
43            Some("value") => {
44                // Parse target as "ref=value" OR use target for element and text for value
45                target.and_then(|t| {
46                    let parts: Vec<&str> = t.splitn(2, '=').collect();
47                    if parts.len() == 2 {
48                        // Format: --target @ref=expected_value
49                        Some(WaitCondition::Value {
50                            element: parts[0].to_string(),
51                            expected: parts[1].to_string(),
52                        })
53                    } else {
54                        // Format: --target @ref --text "expected_value"
55                        text.map(|expected_value| WaitCondition::Value {
56                            element: t.to_string(),
57                            expected: expected_value.to_string(),
58                        })
59                    }
60                })
61            }
62            None => text.map(|t| WaitCondition::Text(t.to_string())),
63            _ => None,
64        }
65    }
66
67    /// Get a description of the condition for error messages
68    pub fn description(&self) -> String {
69        match self {
70            WaitCondition::Text(t) => format!("text \"{}\"", t),
71            WaitCondition::Element(e) => format!("element {}", e),
72            WaitCondition::Focused(e) => format!("{} to be focused", e),
73            WaitCondition::NotVisible(e) => format!("{} to disappear", e),
74            WaitCondition::Stable => "screen to stabilize".to_string(),
75            WaitCondition::TextGone(t) => format!("text \"{}\" to disappear", t),
76            WaitCondition::Value { element, expected } => {
77                format!("{} to have value \"{}\"", element, expected)
78            }
79        }
80    }
81}
82
83/// State tracker for stable wait condition
84#[derive(Default)]
85pub struct StableTracker {
86    last_hashes: Vec<u64>,
87    required_consecutive: usize,
88}
89
90impl StableTracker {
91    pub fn new(required_consecutive: usize) -> Self {
92        Self {
93            last_hashes: Vec::new(),
94            required_consecutive,
95        }
96    }
97
98    /// Add a screen hash and return true if screen is stable
99    pub fn add_hash(&mut self, screen: &str) -> bool {
100        let mut hasher = DefaultHasher::new();
101        screen.hash(&mut hasher);
102        let hash = hasher.finish();
103
104        self.last_hashes.push(hash);
105
106        if self.last_hashes.len() > self.required_consecutive {
107            self.last_hashes.remove(0);
108        }
109
110        if self.last_hashes.len() >= self.required_consecutive {
111            let first = self.last_hashes[0];
112            self.last_hashes.iter().all(|&h| h == first)
113        } else {
114            false
115        }
116    }
117}
118
119/// Check if a wait condition is satisfied
120pub fn check_condition(
121    session: &mut Session,
122    condition: &WaitCondition,
123    stable_tracker: &mut StableTracker,
124) -> bool {
125    let _ = session.update();
126
127    match condition {
128        WaitCondition::Text(text) => {
129            let screen = session.screen_text();
130            screen.contains(text)
131        }
132
133        WaitCondition::Element(element_ref) => {
134            session.detect_elements();
135            session.find_element(element_ref).is_some()
136        }
137
138        WaitCondition::Focused(element_ref) => {
139            session.detect_elements();
140            session
141                .find_element(element_ref)
142                .map(|el| el.focused)
143                .unwrap_or(false)
144        }
145
146        WaitCondition::NotVisible(element_ref) => {
147            session.detect_elements();
148            session.find_element(element_ref).is_none()
149        }
150
151        WaitCondition::Stable => {
152            let screen = session.screen_text();
153            stable_tracker.add_hash(&screen)
154        }
155
156        WaitCondition::TextGone(text) => {
157            let screen = session.screen_text();
158            !screen.contains(text)
159        }
160
161        WaitCondition::Value { element, expected } => {
162            session.detect_elements();
163            session
164                .find_element(element)
165                .and_then(|el| el.value.as_ref())
166                .map(|v| v == expected)
167                .unwrap_or(false)
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_parse_text_condition() {
178        let cond = WaitCondition::parse(Some("text"), None, Some("hello"));
179        assert!(matches!(cond, Some(WaitCondition::Text(t)) if t == "hello"));
180    }
181
182    #[test]
183    fn test_parse_element_condition() {
184        let cond = WaitCondition::parse(Some("element"), Some("@btn1"), None);
185        assert!(matches!(cond, Some(WaitCondition::Element(e)) if e == "@btn1"));
186    }
187
188    #[test]
189    fn test_parse_stable_condition() {
190        let cond = WaitCondition::parse(Some("stable"), None, None);
191        assert!(matches!(cond, Some(WaitCondition::Stable)));
192    }
193
194    #[test]
195    fn test_parse_value_condition() {
196        let cond = WaitCondition::parse(Some("value"), Some("@inp1=hello"), None);
197        assert!(
198            matches!(cond, Some(WaitCondition::Value { element, expected }) if element == "@inp1" && expected == "hello")
199        );
200    }
201
202    #[test]
203    fn test_parse_value_condition_with_separate_text() {
204        // Spec format: --condition value --target @e2 --text "expected value"
205        let cond = WaitCondition::parse(Some("value"), Some("@e2"), Some("expected value"));
206        assert!(
207            matches!(cond, Some(WaitCondition::Value { element, expected }) if element == "@e2" && expected == "expected value")
208        );
209    }
210
211    #[test]
212    fn test_stable_tracker() {
213        let mut tracker = StableTracker::new(3);
214
215        // Different screens
216        assert!(!tracker.add_hash("screen1"));
217        assert!(!tracker.add_hash("screen2"));
218        assert!(!tracker.add_hash("screen3"));
219
220        // Same screen 3 times
221        assert!(!tracker.add_hash("stable"));
222        assert!(!tracker.add_hash("stable"));
223        assert!(tracker.add_hash("stable"));
224    }
225
226    #[test]
227    fn test_default_to_text_condition() {
228        let cond = WaitCondition::parse(None, None, Some("hello"));
229        assert!(matches!(cond, Some(WaitCondition::Text(t)) if t == "hello"));
230    }
231}