Skip to main content
Glama
wait.rs11.7 kB
//! Wait conditions and mechanisms for terminal state changes. use regex::Regex; use std::time::{Duration, Instant}; use terminal_mcp_core::{Error, Result, TerminalStateTree}; use terminal_mcp_detector::DetectionPipeline; use crate::session::Session; use crate::snapshot::SnapshotConfig; /// Condition to wait for in terminal state. #[derive(Debug, Clone)] pub struct WaitCondition { /// Text pattern to match (regex) pub text: Option<String>, /// Element type to wait for pub element_type: Option<String>, /// Wait for condition to disappear instead of appear pub gone: bool, /// Wait for terminal to be idle pub idle: bool, /// Maximum time to wait pub timeout: Duration, /// Polling interval between checks pub poll_interval: Duration, } impl Default for WaitCondition { fn default() -> Self { Self { text: None, element_type: None, gone: false, idle: false, timeout: Duration::from_secs(30), poll_interval: Duration::from_millis(100), } } } impl WaitCondition { /// Create a new wait condition with default values. pub fn new() -> Self { Self::default() } /// Wait for text to appear. pub fn for_text(text: impl Into<String>) -> Self { Self { text: Some(text.into()), ..Self::default() } } /// Wait for text to disappear. pub fn for_text_gone(text: impl Into<String>) -> Self { Self { text: Some(text.into()), gone: true, ..Self::default() } } /// Wait for element type to appear. pub fn for_element(element_type: impl Into<String>) -> Self { Self { element_type: Some(element_type.into()), ..Self::default() } } /// Wait for element type to disappear. pub fn for_element_gone(element_type: impl Into<String>) -> Self { Self { element_type: Some(element_type.into()), gone: true, ..Self::default() } } /// Wait for terminal to become idle. pub fn for_idle() -> Self { Self { idle: true, ..Self::default() } } /// Set timeout duration. pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } /// Set polling interval. pub fn with_poll_interval(mut self, interval: Duration) -> Self { self.poll_interval = interval; self } } /// Result of a wait operation. #[derive(Debug, Clone)] pub struct WaitResult { /// Whether the condition was met pub condition_met: bool, /// Time waited in milliseconds pub waited_ms: u64, /// Terminal state tree when condition was met (or at timeout) pub snapshot: TerminalStateTree, } impl Session { /// Wait for a condition to be met. /// /// Repeatedly takes snapshots and checks if the condition is satisfied. /// Returns when the condition is met or timeout is reached. /// /// # Arguments /// * `condition` - The condition to wait for /// * `pipeline` - Detection pipeline for taking snapshots /// * `snapshot_config` - Configuration for snapshot operations /// /// # Returns /// `WaitResult` containing the snapshot and whether condition was met /// /// # Example /// ```no_run /// # use terminal_mcp_session::{Session, WaitCondition}; /// # use terminal_mcp_detector::DetectionPipeline; /// # use terminal_mcp_session::SnapshotConfig; /// # use terminal_mcp_core::Dimensions; /// # use std::time::Duration; /// # let session = Session::create("bash".to_string(), vec![], Dimensions::new(24, 80)).unwrap(); /// let pipeline = DetectionPipeline::new(); /// let config = SnapshotConfig::default(); /// /// // Wait for "Success" to appear /// let condition = WaitCondition::for_text("Success") /// .with_timeout(Duration::from_secs(10)); /// /// let result = session.wait_for(&condition, &pipeline, &config).unwrap(); /// if result.condition_met { /// println!("Condition met after {}ms", result.waited_ms); /// } /// ``` pub fn wait_for( &self, condition: &WaitCondition, pipeline: &DetectionPipeline, snapshot_config: &SnapshotConfig, ) -> Result<WaitResult> { let start = Instant::now(); loop { // Check timeout let elapsed = start.elapsed(); if elapsed >= condition.timeout { // Timeout reached - take final snapshot let snapshot = self.snapshot(pipeline, snapshot_config)?; return Ok(WaitResult { condition_met: false, waited_ms: elapsed.as_millis() as u64, snapshot, }); } // Take snapshot let snapshot = self.snapshot(pipeline, snapshot_config)?; // Check if condition is met if Self::check_condition(&snapshot, condition)? { return Ok(WaitResult { condition_met: true, waited_ms: elapsed.as_millis() as u64, snapshot, }); } // Wait before next poll std::thread::sleep(condition.poll_interval); } } /// Check if a condition is satisfied by the given snapshot. fn check_condition(snapshot: &TerminalStateTree, condition: &WaitCondition) -> Result<bool> { // Idle condition is always met (snapshot already waits for idle) if condition.idle { return Ok(true); } // Check text condition if let Some(pattern) = &condition.text { let regex = Regex::new(pattern) .map_err(|e| Error::InvalidInput(format!("Invalid regex: {e}")))?; let found = regex.is_match(&snapshot.raw_text); return Ok(if condition.gone { !found } else { found }); } // Check element type condition if let Some(elem_type) = &condition.element_type { let found = snapshot.elements.iter().any(|e| e.type_name() == elem_type); return Ok(if condition.gone { !found } else { found }); } // No specific condition - just wait for idle Ok(true) } } #[cfg(test)] mod tests { use super::*; use terminal_mcp_core::Dimensions; #[test] fn test_wait_condition_default() { let condition = WaitCondition::default(); assert_eq!(condition.timeout, Duration::from_secs(30)); assert_eq!(condition.poll_interval, Duration::from_millis(100)); assert!(!condition.gone); assert!(!condition.idle); } #[test] fn test_wait_condition_for_text() { let condition = WaitCondition::for_text("hello"); assert_eq!(condition.text, Some("hello".to_string())); assert!(!condition.gone); } #[test] fn test_wait_condition_for_text_gone() { let condition = WaitCondition::for_text_gone("loading"); assert_eq!(condition.text, Some("loading".to_string())); assert!(condition.gone); } #[test] fn test_wait_condition_for_element() { let condition = WaitCondition::for_element("menu"); assert_eq!(condition.element_type, Some("menu".to_string())); assert!(!condition.gone); } #[test] fn test_wait_condition_for_idle() { let condition = WaitCondition::for_idle(); assert!(condition.idle); } #[test] fn test_wait_condition_with_timeout() { let condition = WaitCondition::for_text("test").with_timeout(Duration::from_secs(5)); assert_eq!(condition.timeout, Duration::from_secs(5)); } #[test] fn test_wait_for_idle_always_succeeds() { let session = Session::create( "echo".to_string(), vec!["test".to_string()], Dimensions::new(24, 80), ) .unwrap(); let pipeline = DetectionPipeline::new(); let snapshot_config = SnapshotConfig::default(); // Wait for idle with short timeout let condition = WaitCondition::for_idle().with_timeout(Duration::from_secs(2)); std::thread::sleep(Duration::from_millis(200)); let result = session.wait_for(&condition, &pipeline, &snapshot_config); assert!(result.is_ok()); let wait_result = result.unwrap(); assert!(wait_result.condition_met); } #[test] fn test_wait_for_text_appears() { let session = Session::create( "echo".to_string(), vec!["hello world".to_string()], Dimensions::new(24, 80), ) .unwrap(); let pipeline = DetectionPipeline::new(); let snapshot_config = SnapshotConfig { idle_threshold: Duration::from_millis(50), ..Default::default() }; // Wait for "hello" to appear let condition = WaitCondition::for_text("hello") .with_timeout(Duration::from_secs(2)) .with_poll_interval(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(200)); let result = session.wait_for(&condition, &pipeline, &snapshot_config); assert!(result.is_ok()); let wait_result = result.unwrap(); assert!(wait_result.condition_met); assert!(wait_result.snapshot.raw_text.contains("hello")); } #[test] fn test_wait_for_text_timeout() { let session = Session::create( "echo".to_string(), vec!["test".to_string()], Dimensions::new(24, 80), ) .unwrap(); let pipeline = DetectionPipeline::new(); let snapshot_config = SnapshotConfig { idle_threshold: Duration::from_millis(50), idle_timeout: Duration::from_millis(500), ..Default::default() }; // Wait for text that won't appear let condition = WaitCondition::for_text("nonexistent") .with_timeout(Duration::from_millis(300)) .with_poll_interval(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(200)); let result = session.wait_for(&condition, &pipeline, &snapshot_config); assert!(result.is_ok()); let wait_result = result.unwrap(); assert!(!wait_result.condition_met); // Timeout reached assert!(wait_result.waited_ms >= 300); } #[test] fn test_check_condition_text_regex() { use terminal_mcp_core::Position; let snapshot = TerminalStateTree { session_id: "test".to_string(), dimensions: Dimensions::new(24, 80), cursor: Position::new(0, 0), timestamp: "2025-11-30T00:00:00Z".to_string(), elements: vec![], raw_text: "Server started successfully on port 8080".to_string(), ansi_buffer: None, }; // Test regex pattern matching let condition = WaitCondition::for_text("port \\d+"); assert!(Session::check_condition(&snapshot, &condition).unwrap()); // Test non-matching pattern let condition = WaitCondition::for_text("failed"); assert!(!Session::check_condition(&snapshot, &condition).unwrap()); // Test gone condition let condition = WaitCondition::for_text_gone("failed"); assert!(Session::check_condition(&snapshot, &condition).unwrap()); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server