1use std::fmt::Write;
4#[cfg(feature = "ui-snapshot-png")]
5use std::path::PathBuf;
6#[cfg(feature = "ui-snapshot-png")]
7use std::sync::Arc;
8
9use crate::style::ansi::write_cell_style_sgr;
10use crate::style::{Color, Rect, Style};
11
12#[cfg(feature = "ui-snapshot-png")]
13mod png;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct CapturedCell {
18 pub symbol: String,
20 pub fg: Color,
22 pub bg: Color,
24 pub underline_color: Color,
26 pub modifiers: CellModifiers,
28}
29
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct CellModifiers {
33 pub bold: bool,
35 pub dim: bool,
37 pub italic: bool,
39 pub underline: bool,
41 pub reverse: bool,
43 pub strikethrough: bool,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct CursorState {
50 pub x: u16,
52 pub y: u16,
54 pub visible: bool,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct CapturedFrame {
61 pub viewport: Rect,
63 pub width: u16,
65 pub height: u16,
67 pub cells: Vec<CapturedCell>,
69 pub cursor: Option<CursorState>,
71}
72
73#[cfg(feature = "ui-snapshot-png")]
75#[derive(Clone, Debug)]
76pub struct PngOptions {
77 pub cell_width: u16,
81 pub cell_height: u16,
85 pub scale: u16,
89 pub default_fg: Color,
91 pub default_bg: Color,
93 pub render_cursor: bool,
97 pub text_renderer: PngTextRenderer,
101 pub font_family: Option<Arc<str>>,
103 pub font_path: Option<PathBuf>,
105}
106
107#[cfg(feature = "ui-snapshot-png")]
109#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
110pub enum PngTextRenderer {
111 #[default]
113 Auto,
114 Font,
116 Bitmap,
118}
119
120#[cfg(feature = "ui-snapshot-png")]
121impl Default for PngOptions {
122 fn default() -> Self {
123 Self {
124 cell_width: 8,
125 cell_height: 16,
126 scale: 2,
127 default_fg: Color::White,
128 default_bg: Color::Black,
129 render_cursor: true,
130 text_renderer: PngTextRenderer::Auto,
131 font_family: None,
132 font_path: None,
133 }
134 }
135}
136
137impl CapturedCell {
138 fn style(&self) -> Style {
139 Style {
140 fg: Some(self.fg.into()),
141 bg: Some(self.bg.into()),
142 fg_transform: None,
143 bg_transform: None,
144 contrast_policy: None,
145 bold: Some(self.modifiers.bold),
146 dim: Some(self.modifiers.dim),
147 italic: Some(self.modifiers.italic),
148 underline: Some(self.modifiers.underline),
149 reverse: Some(self.modifiers.reverse),
150 strikethrough: Some(self.modifiers.strikethrough),
151 underline_color: Some(self.underline_color.into()),
152 dim_amount: None,
153 tint: None,
154 }
155 }
156
157 pub fn write_ansi_style(&self, out: &mut String) {
159 let m = &self.modifiers;
160 write_cell_style_sgr(
161 out,
162 crate::style::ansi::CellStyleSgr {
163 fg: self.fg,
164 bg: self.bg,
165 underline_color: self.underline_color,
166 bold: m.bold,
167 dim: m.dim,
168 italic: m.italic,
169 underline: m.underline,
170 reverse: m.reverse,
171 strikethrough: m.strikethrough,
172 },
173 );
174 }
175
176 pub fn ansi_style_matches(&self, other: &CapturedCell) -> bool {
178 self.fg == other.fg
179 && self.bg == other.bg
180 && self.underline_color == other.underline_color
181 && self.modifiers == other.modifiers
182 }
183}
184
185impl CapturedFrame {
186 #[cfg(feature = "ui-snapshot-png")]
194 pub fn to_png(&self, options: &PngOptions) -> Vec<u8> {
195 png::encode_frame(self, options)
196 }
197
198 #[cfg(feature = "ui-snapshot-png")]
200 pub fn try_to_png(&self, options: &PngOptions) -> crate::Result<Vec<u8>> {
201 png::try_encode_frame(self, options)
202 .map_err(|err| std::io::Error::other(err.to_string()).into())
203 }
204
205 pub fn plain_text(&self) -> String {
207 self.to_lines().join("\n")
208 }
209
210 pub fn to_fixed_grid(&self) -> String {
212 self.to_fixed_grid_lines().join("\n")
213 }
214
215 pub fn to_fixed_grid_lines(&self) -> Vec<String> {
217 (0..self.height)
218 .map(|y| {
219 let mut line = String::new();
220 for cell in self.row(y) {
221 line.push_str(&cell.symbol);
222 }
223 line
224 })
225 .collect()
226 }
227
228 pub fn to_ansi(&self) -> String {
230 self.to_ansi_diff(None)
231 }
232
233 pub fn to_ansi_diff(&self, prev: Option<&CapturedFrame>) -> String {
238 let full_repaint = prev
239 .map(|p| p.width != self.width || p.height != self.height)
240 .unwrap_or(true);
241 let mut out = String::with_capacity(if full_repaint {
242 usize::from(self.width) * usize::from(self.height) * 12 + 64
243 } else {
244 usize::from(self.width) * usize::from(self.height) * 3 + 512
245 });
246
247 if full_repaint {
248 out.push_str("\x1b[2J\x1b[3J\x1b[H\x1b[?25l");
249 }
250
251 let w = usize::from(self.width);
252 let mut last_written: Option<(u16, u16)> = None;
253 let mut last_styled_cell: Option<&CapturedCell> = None;
254 for y in 0..self.height {
255 for x in 0..self.width {
256 let idx = usize::from(y) * w + usize::from(x);
257 let cell = &self.cells[idx];
258 if !full_repaint
259 && let Some(prev_frame) = prev
260 && prev_frame.cells[idx] == *cell
261 {
262 continue;
263 }
264 let contiguous = matches!(last_written, Some((lx, ly)) if ly == y && lx + 1 == x);
265 if !contiguous {
266 let _ = write!(out, "\x1b[{};{}H", y + 1, x + 1);
267 }
268 if !(contiguous
269 && last_styled_cell.is_some_and(|prev_cell| prev_cell.ansi_style_matches(cell)))
270 {
271 cell.write_ansi_style(&mut out);
272 last_styled_cell = Some(cell);
273 }
274 out.push_str(&cell.symbol);
275 last_written = Some((x, y));
276 }
277 }
278 if let Some(c) = self.cursor.as_ref().filter(|c| c.visible) {
279 let _ = write!(out, "\x1b[{};{}H\x1b[?25h", c.y + 1, c.x + 1);
280 } else {
281 out.push_str("\x1b[?25l");
282 }
283 out
284 }
285
286 pub fn to_lines(&self) -> Vec<String> {
288 (0..self.height)
289 .map(|y| {
290 let mut line = String::new();
291 for cell in self.row(y) {
292 line.push_str(&cell.symbol);
293 }
294 line.trim_end_matches(' ').to_owned()
295 })
296 .collect()
297 }
298
299 pub fn row(&self, y: u16) -> &[CapturedCell] {
303 assert!(y < self.height, "row y out of bounds");
304 let start = usize::from(y) * usize::from(self.width);
305 let end = start + usize::from(self.width);
306 &self.cells[start..end]
307 }
308
309 pub fn cell(&self, x: u16, y: u16) -> &CapturedCell {
313 assert!(x < self.width, "cell x out of bounds");
314 &self.row(y)[usize::from(x)]
315 }
316
317 pub fn styled_lines(&self) -> Vec<Vec<(String, Style)>> {
319 (0..self.height)
320 .map(|y| {
321 let mut runs: Vec<(String, Style)> = Vec::new();
322
323 for cell in self.row(y) {
324 let style = cell.style();
325 if let Some((text, run_style)) = runs.last_mut()
326 && *run_style == style
327 {
328 text.push_str(&cell.symbol);
329 } else {
330 runs.push((cell.symbol.clone(), style));
331 }
332 }
333
334 runs
335 })
336 .collect()
337 }
338}