1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum TerminalLoadState {
15 Quiet,
17}
18
19#[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 pub const fn timeout(mut self, timeout: Duration) -> Self {
50 self.timeout = Some(timeout);
51 self
52 }
53
54 pub const fn poll_interval(mut self, interval: Duration) -> Self {
56 self.poll_interval = interval;
57 self
58 }
59
60 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 pub fn wait_for_load_state(&self, state: TerminalLoadState) -> TerminalLoadStateWait {
88 TerminalLoadStateWait::new(self.clone(), state)
89 }
90
91 pub fn wait_until_stable_for(&self, duration: Duration) -> TerminalLoadStateWait {
93 TerminalLoadStateWait::quiet_for(self.clone(), duration)
94 }
95
96 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}