Skip to main content

rmux_sdk/events/
render.rs

1//! Minimal snapshot render stream built from raw pane output.
2
3use std::time::Duration;
4
5use crate::{Pane, PaneLagNotice, PaneOutputChunk, PaneOutputStream, PaneSnapshot, Result};
6
7const DEFAULT_RENDER_DEBOUNCE: Duration = Duration::from_millis(16);
8
9/// Snapshot update emitted by [`PaneRenderStream`].
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RenderUpdate {
12    snapshot: PaneSnapshot,
13    lag: Option<PaneLagNotice>,
14}
15
16impl RenderUpdate {
17    /// Returns the snapshot captured for this render update.
18    #[must_use]
19    pub const fn snapshot(&self) -> &PaneSnapshot {
20        &self.snapshot
21    }
22
23    /// Returns the lag notice that preceded this snapshot, when output lag was
24    /// observed.
25    #[must_use]
26    pub const fn lag(&self) -> Option<&PaneLagNotice> {
27        self.lag.as_ref()
28    }
29
30    /// Consumes the update and returns its snapshot.
31    #[must_use]
32    pub fn into_snapshot(self) -> PaneSnapshot {
33        self.snapshot
34    }
35}
36
37/// Minimal event-driven render stream for one pane.
38///
39/// This render stream is intentionally built from [`Pane::output_stream`]:
40/// output wakes the stream, a short debounce coalesces bursts, then the SDK
41/// captures a fresh snapshot and emits it only when the snapshot revision
42/// changed. It avoids blind fixed-rate refresh loops without claiming a
43/// daemon-native revision stream.
44pub struct PaneRenderStream {
45    pane: Pane,
46    output: PaneOutputStream,
47    debounce: Duration,
48    last_revision: Option<u64>,
49    pending_lag: Option<PaneLagNotice>,
50}
51
52impl PaneRenderStream {
53    pub(crate) async fn open(pane: Pane) -> Result<Self> {
54        let output = pane.output_stream().await?;
55        let baseline = pane.snapshot().await?;
56        Ok(Self {
57            pane,
58            output,
59            debounce: DEFAULT_RENDER_DEBOUNCE,
60            last_revision: Some(baseline.revision),
61            pending_lag: None,
62        })
63    }
64
65    /// Overrides the debounce used before capturing snapshots after output.
66    #[must_use]
67    pub const fn with_debounce(mut self, debounce: Duration) -> Self {
68        self.debounce = debounce;
69        self
70    }
71
72    /// Returns the next render update, or `None` once the underlying output
73    /// subscription closes.
74    pub async fn next(&mut self) -> Result<Option<RenderUpdate>> {
75        loop {
76            let Some(chunk) = self.output.next().await? else {
77                return Ok(None);
78            };
79            if let PaneOutputChunk::Lag(lag) = chunk {
80                self.pending_lag = Some(lag);
81            }
82
83            if !self.debounce.is_zero() {
84                tokio::time::sleep(self.debounce).await;
85            }
86
87            let snapshot = self.pane.snapshot().await?;
88            if self.last_revision == Some(snapshot.revision) {
89                continue;
90            }
91            self.last_revision = Some(snapshot.revision);
92            return Ok(Some(RenderUpdate {
93                snapshot,
94                lag: self.pending_lag.take(),
95            }));
96        }
97    }
98}
99
100impl std::fmt::Debug for PaneRenderStream {
101    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        formatter
103            .debug_struct("PaneRenderStream")
104            .field("pane", &self.pane)
105            .field("debounce", &self.debounce)
106            .field("last_revision", &self.last_revision)
107            .field("pending_lag", &self.pending_lag)
108            .finish_non_exhaustive()
109    }
110}