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 tmux-style per-line format flags for the selected physical rows.
26    #[must_use]
27    pub fn capture_line_format_flags(&self, range: ScreenCaptureRange) -> Vec<u8> {
28        capture_grid_line_format_flags(&self.grid, range)
29    }
30
31    /// Captures physical lines with each line rendered from a fresh ANSI state.
32    ///
33    /// This is intended for renderers that repaint individual terminal rows:
34    /// a row must carry its own SGR state instead of depending on a previous
35    /// captured row having been emitted first.
36    #[must_use]
37    pub fn capture_transcript_lines_independent(
38        &self,
39        range: ScreenCaptureRange,
40        options: GridRenderOptions,
41    ) -> Vec<Vec<u8>> {
42        capture_grid_lines_independent(&self.grid, &self.hyperlinks, range, options)
43    }
44
45    #[must_use]
46    /// Returns the monotonic mutation revision for one visible row.
47    pub fn visible_line_revision(&self, row: usize) -> Option<u64> {
48        self.grid
49            .visible_line(u32::try_from(row).ok()?)
50            .map(|line| line.revision())
51    }
52
53    #[must_use]
54    /// Renders one visible row from a fresh ANSI state.
55    pub fn render_visible_line_independent(
56        &self,
57        row: usize,
58        options: GridRenderOptions,
59    ) -> Option<Vec<u8>> {
60        let absolute_y = self.grid.hsize().checked_add(row)?;
61        let mut state = GridStringState::default();
62        self.grid
63            .render_absolute_line(absolute_y, options, &mut state, Some(&self.hyperlinks))
64            .map(String::into_bytes)
65    }
66
67    #[must_use]
68    /// Renders one visible row from a fresh ANSI state after applying pane
69    /// default-style to default cells only.
70    pub fn render_visible_line_independent_with_default_style(
71        &self,
72        row: usize,
73        options: GridRenderOptions,
74        style: &Style,
75    ) -> Option<Vec<u8>> {
76        let mut state = GridStringState::default();
77        self.grid
78            .render_visible_line_with_default_style(
79                row,
80                options,
81                &mut state,
82                Some(&self.hyperlinks),
83                style,
84            )
85            .map(String::into_bytes)
86    }
87
88    /// Captures the saved pre-alternate-screen copy when alternate mode is active.
89    #[must_use]
90    pub fn capture_saved_transcript(
91        &self,
92        range: ScreenCaptureRange,
93        options: GridRenderOptions,
94    ) -> Option<Vec<u8>> {
95        self.saved_grid
96            .as_ref()
97            .map(|saved| capture_grid_bytes(&saved.grid, &self.hyperlinks, range, options))
98    }
99
100    /// Captures tmux-style per-line format flags from the saved alternate screen.
101    #[must_use]
102    pub fn capture_saved_line_format_flags(&self, range: ScreenCaptureRange) -> Option<Vec<u8>> {
103        self.saved_grid
104            .as_ref()
105            .map(|saved| capture_grid_line_format_flags(&saved.grid, range))
106    }
107}
108
109fn capture_grid_lines_independent(
110    grid: &Grid,
111    hyperlinks: &Hyperlinks,
112    range: ScreenCaptureRange,
113    options: GridRenderOptions,
114) -> Vec<Vec<u8>> {
115    let total_lines = grid.hsize() + usize::try_from(grid.sy()).unwrap_or(usize::MAX);
116    let Some(range) = resolve_screen_capture_range(range, grid.hsize(), total_lines) else {
117        return Vec::new();
118    };
119
120    let mut output = Vec::new();
121    for absolute_y in range {
122        let mut state = GridStringState::default();
123        let Some(line) =
124            grid.render_absolute_line(absolute_y, options, &mut state, Some(hyperlinks))
125        else {
126            continue;
127        };
128        output.push(line.into_bytes());
129    }
130    output
131}
132
133fn capture_grid_bytes(
134    grid: &Grid,
135    hyperlinks: &Hyperlinks,
136    range: ScreenCaptureRange,
137    options: GridRenderOptions,
138) -> Vec<u8> {
139    let total_lines = grid.hsize() + usize::try_from(grid.sy()).unwrap_or(usize::MAX);
140    let Some(range) = resolve_screen_capture_range(range, grid.hsize(), total_lines) else {
141        return Vec::new();
142    };
143
144    let line_count = range.end().saturating_sub(*range.start()).saturating_add(1);
145    let mut output = Vec::with_capacity(capture_capacity_hint(
146        line_count,
147        usize::try_from(grid.sx()).unwrap_or(usize::MAX),
148    ));
149    let mut state = GridStringState::default();
150    for absolute_y in range {
151        if grid
152            .append_rendered_absolute_line(
153                absolute_y,
154                options,
155                &mut state,
156                Some(hyperlinks),
157                &mut output,
158            )
159            .is_none()
160        {
161            continue;
162        };
163        let wrapped = grid.absolute_line_wrapped(absolute_y).unwrap_or(false);
164        if !options.join_wrapped || !wrapped {
165            state.reset_to_default_line_style(options, Some(hyperlinks), &mut output);
166            output.push(b'\n');
167        }
168    }
169    output
170}
171
172fn capture_grid_line_format_flags(grid: &Grid, range: ScreenCaptureRange) -> Vec<u8> {
173    let total_lines = grid.hsize() + usize::try_from(grid.sy()).unwrap_or(usize::MAX);
174    let Some(range) = resolve_screen_capture_range(range, grid.hsize(), total_lines) else {
175        return Vec::new();
176    };
177
178    let mut flags = Vec::new();
179    for absolute_y in range {
180        if grid.absolute_line(absolute_y).is_none() {
181            continue;
182        }
183        flags.push(if grid.absolute_line_wrapped(absolute_y).unwrap_or(false) {
184            b'W'
185        } else {
186            b'-'
187        });
188    }
189    flags
190}
191
192fn capture_capacity_hint(line_count: usize, line_width: usize) -> usize {
193    line_count
194        .saturating_mul(line_width.saturating_add(1))
195        .min(64 * 1024 * 1024)
196}