Skip to main content

rmux_sdk/
load_state.rs

1//! Terminal load-state waits.
2
3use std::future::{Future, IntoFuture};
4use std::pin::Pin;
5use std::time::{Duration, Instant};
6
7use crate::{Pane, PaneSnapshot, Result, RmuxError, WaitTimeoutError};
8
9const DEFAULT_QUIET_FOR: Duration = Duration::from_millis(300);
10
11/// Terminal load states supported by the SDK.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum TerminalLoadState {
15    /// No visible snapshot change has been observed for the configured quiet window.
16    Quiet,
17}
18
19/// Awaitable terminal load-state wait.
20#[derive(Debug)]
21#[must_use = "terminal load-state waits do nothing unless awaited"]
22pub struct TerminalLoadStateWait {
23    pane: Pane,
24    state: TerminalLoadState,
25    quiet_for: Duration,
26    timeout: Option<Duration>,
27    poll_interval: Duration,
28}
29
30impl TerminalLoadStateWait {
31    pub(crate) fn new(pane: Pane, state: TerminalLoadState) -> Self {
32        Self {
33            pane,
34            state,
35            quiet_for: DEFAULT_QUIET_FOR,
36            timeout: None,
37            poll_interval: crate::wait::TEXT_POLL_INTERVAL,
38        }
39    }
40
41    pub(crate) fn quiet_for(pane: Pane, quiet_for: Duration) -> Self {
42        Self {
43            quiet_for,
44            ..Self::new(pane, TerminalLoadState::Quiet)
45        }
46    }
47
48    /// Overrides the overall timeout for this wait.
49    pub const fn timeout(mut self, timeout: Duration) -> Self {
50        self.timeout = Some(timeout);
51        self
52    }
53
54    /// Overrides the snapshot polling interval for this wait.
55    pub const fn poll_interval(mut self, interval: Duration) -> Self {
56        self.poll_interval = interval;
57        self
58    }
59
60    /// Overrides the quiet window used by [`TerminalLoadState::Quiet`].
61    pub const fn stable_for(mut self, quiet_for: Duration) -> Self {
62        self.quiet_for = quiet_for;
63        self
64    }
65
66    async fn run(self) -> Result<PaneSnapshot> {
67        match self.state {
68            TerminalLoadState::Quiet => wait_until_quiet(self).await,
69        }
70    }
71}
72
73impl IntoFuture for TerminalLoadStateWait {
74    type Output = Result<PaneSnapshot>;
75    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
76
77    fn into_future(self) -> Self::IntoFuture {
78        Box::pin(self.run())
79    }
80}
81
82impl Pane {
83    /// Waits for a terminal load state.
84    ///
85    /// `Quiet` means the rendered snapshot stays unchanged for the wait's
86    /// configured quiet window; no prompt-specific heuristics are inferred.
87    pub fn wait_for_load_state(&self, state: TerminalLoadState) -> TerminalLoadStateWait {
88        TerminalLoadStateWait::new(self.clone(), state)
89    }
90
91    /// Waits until the rendered terminal snapshot is stable for `duration`.
92    pub fn wait_until_stable_for(&self, duration: Duration) -> TerminalLoadStateWait {
93        TerminalLoadStateWait::quiet_for(self.clone(), duration)
94    }
95
96    /// Waits for the default terminal quiet window.
97    pub fn expect_stable(&self) -> TerminalLoadStateWait {
98        self.wait_for_load_state(TerminalLoadState::Quiet)
99    }
100}
101
102async fn wait_until_quiet(wait: TerminalLoadStateWait) -> Result<PaneSnapshot> {
103    let timeout = wait
104        .timeout
105        .or_else(|| crate::wait::resolved_wait_timeout(wait.pane.configured_default_timeout()));
106    let deadline = timeout.map(|timeout| Instant::now() + timeout);
107    let mut last = wait.pane.snapshot().await?;
108    let mut stable_since = Instant::now();
109
110    loop {
111        if stable_since.elapsed() >= wait.quiet_for {
112            return Ok(last);
113        }
114        if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
115            return Err(RmuxError::wait_timeout(WaitTimeoutError::new(
116                format!(
117                    "terminal load state {:?} for {:?}",
118                    wait.state, wait.quiet_for
119                ),
120                timeout.expect("deadline implies timeout"),
121                last,
122            )));
123        }
124        sleep_until_next_poll(deadline, wait.poll_interval).await;
125        let snapshot = wait.pane.snapshot().await?;
126        if snapshot.revision == last.revision && snapshot.visible_text() == last.visible_text() {
127            continue;
128        }
129        last = snapshot;
130        stable_since = Instant::now();
131    }
132}
133
134async fn sleep_until_next_poll(deadline: Option<Instant>, poll_interval: Duration) {
135    let Some(deadline) = deadline else {
136        tokio::time::sleep(poll_interval).await;
137        return;
138    };
139    let now = Instant::now();
140    if now < deadline {
141        tokio::time::sleep(poll_interval.min(deadline - now)).await;
142    }
143}