Skip to main content

rmux_core/screen/
capture.rs

1use crate::grid::{Grid, GridCapture, GridRenderOptions, GridStringState};
2use crate::hyperlinks::Hyperlinks;
3use crate::style::Style;
4use crate::transcript::{resolve_screen_capture_range, ScreenCaptureRange};
5
6use super::Screen;
7
8impl Screen {
9    #[cfg_attr(not(test), allow(dead_code))]
10    #[must_use]
11    pub(crate) fn capture_grid(&self, join_wrapped: bool) -> GridCapture {
12        self.grid.capture(join_wrapped)
13    }
14
15    /// Captures a tmux-style line range over the current grid contents.
16    #[must_use]
17    pub fn capture_transcript(
18        &self,
19        range: ScreenCaptureRange,
20        options: GridRenderOptions,
21    ) -> Vec<u8> {
22        capture_grid_bytes(&self.grid, &self.hyperlinks, range, options)
23    }
24
25    /// Captures physical lines with each line rendered from a fresh ANSI state.
26    ///
27    /// This is intended for renderers that repaint individual terminal rows:
28    /// a row must carry its own SGR state instead of depending on a previous
29    /// captured row having been emitted first.
30    #[must_use]
31    pub fn capture_transcript_lines_independent(
32        &self,
33        range: ScreenCaptureRange,
34        options: GridRenderOptions,
35    ) -> Vec<Vec<u8>> {
36        capture_grid_lines_independent(&self.grid, &self.hyperlinks, range, options)
37    }
38
39    #[must_use]
40    /// Returns the monotonic mutation revision for one visible row.
41    pub fn visible_line_revision(&self, row: usize) -> Option<u64> {
42        self.grid
43            .visible_line(u32::try_from(row).ok()?)
44            .map(|line| line.revision())
45    }
46
47    #[must_use]
48    /// Renders one visible row from a fresh ANSI state.
49    pub fn render_visible_line_independent(
50        &self,
51        row: usize,
52        options: GridRenderOptions,
53    ) -> Option<Vec<u8>> {
54        let absolute_y = self.grid.hsize().checked_add(row)?;
55        let mut state = GridStringState::default();
56        self.grid
57            .render_absolute_line(absolute_y, options, &mut state, Some(&self.hyperlinks))
58            .map(String::into_bytes)
59    }
60
61    #[must_use]
62    /// Renders one visible row from a fresh ANSI state after applying pane
63    /// default-style to default cells only.
64    pub fn render_visible_line_independent_with_default_style(
65        &self,
66        row: usize,
67        options: GridRenderOptions,
68        style: &Style,
69    ) -> Option<Vec<u8>> {
70        let mut state = GridStringState::default();
71        self.grid
72            .render_visible_line_with_default_style(
73                row,
74                options,
75                &mut state,
76                Some(&self.hyperlinks),
77                style,
78            )
79            .map(String::into_bytes)
80    }
81
82    /// Captures the saved pre-alternate-screen copy when alternate mode is active.
83    #[must_use]
84    pub fn capture_saved_transcript(
85        &self,
86        range: ScreenCaptureRange,
87        options: GridRenderOptions,
88    ) -> Option<Vec<u8>> {
89        self.saved_grid
90            .as_ref()
91            .map(|saved| capture_grid_bytes(&saved.grid, &self.hyperlinks, range, options))
92    }
93}
94
95fn capture_grid_lines_independent(
96    grid: &Grid,
97    hyperlinks: &Hyperlinks,
98    range: ScreenCaptureRange,
99    options: GridRenderOptions,
100) -> Vec<Vec<u8>> {
101    let total_lines = grid.hsize() + usize::try_from(grid.sy()).unwrap_or(usize::MAX);
102    let Some(range) = resolve_screen_capture_range(range, grid.hsize(), total_lines) else {
103        return Vec::new();
104    };
105
106    let mut output = Vec::new();
107    for absolute_y in range {
108        let mut state = GridStringState::default();
109        let Some(line) =
110            grid.render_absolute_line(absolute_y, options, &mut state, Some(hyperlinks))
111        else {
112            continue;
113        };
114        output.push(line.into_bytes());
115    }
116    output
117}
118
119fn capture_grid_bytes(
120    grid: &Grid,
121    hyperlinks: &Hyperlinks,
122    range: ScreenCaptureRange,
123    options: GridRenderOptions,
124) -> Vec<u8> {
125    let total_lines = grid.hsize() + usize::try_from(grid.sy()).unwrap_or(usize::MAX);
126    let Some(range) = resolve_screen_capture_range(range, grid.hsize(), total_lines) else {
127        return Vec::new();
128    };
129
130    let line_count = range.end().saturating_sub(*range.start()).saturating_add(1);
131    let mut output = Vec::with_capacity(capture_capacity_hint(
132        line_count,
133        usize::try_from(grid.sx()).unwrap_or(usize::MAX),
134    ));
135    let mut state = GridStringState::default();
136    for absolute_y in range {
137        if grid
138            .append_rendered_absolute_line(
139                absolute_y,
140                options,
141                &mut state,
142                Some(hyperlinks),
143                &mut output,
144            )
145            .is_none()
146        {
147            continue;
148        };
149        let wrapped = grid.absolute_line_wrapped(absolute_y).unwrap_or(false);
150        if !options.join_wrapped || !wrapped {
151            state.reset_to_default_line_style(options, Some(hyperlinks), &mut output);
152            output.push(b'\n');
153        }
154    }
155    output
156}
157
158fn capture_capacity_hint(line_count: usize, line_width: usize) -> usize {
159    line_count
160        .saturating_mul(line_width.saturating_add(1))
161        .min(64 * 1024 * 1024)
162}