Skip to main content

termgrid_core/
ansi.rs

1use crate::{Cell, Grid, Style};
2
3/// Render a grid to an ANSI string.
4///
5/// This is intentionally conservative:
6/// - Continuation cells are skipped (the preceding wide glyph consumes them).
7/// - Lines are terminated with a reset to avoid style bleed.
8pub fn grid_to_ansi(grid: &Grid) -> String {
9    let mut out = String::new();
10    let mut current = Style::plain();
11
12    for row in grid.rows() {
13        emit_row(&mut out, row, &mut current);
14        // Reset at end of line.
15        out.push_str("\x1b[0m");
16        current = Style::plain();
17        out.push('\n');
18    }
19
20    out
21}
22
23fn emit_row(out: &mut String, row: &[Cell], current: &mut Style) {
24    let mut cur = *current;
25    for cell in row {
26        match cell {
27            Cell::Empty => {
28                // Empty cells are rendered as plain spaces. If we are currently in a
29                // non-plain SGR state, reset so the space doesn't inherit styling.
30                if cur != Style::plain() {
31                    out.push_str("\x1b[0m");
32                    cur = Style::plain();
33                }
34                out.push(' ');
35            }
36            Cell::Continuation => {
37                // Skip. The previous glyph consumed this cell.
38            }
39            Cell::Glyph { grapheme, style } => {
40                if *style != cur {
41                    emit_style(out, *style);
42                    cur = *style;
43                }
44                out.push_str(grapheme);
45            }
46        }
47    }
48    *current = cur;
49}
50
51fn emit_style(out: &mut String, style: Style) {
52    // Reset, then re-apply.
53    out.push_str("\x1b[0m");
54
55    let mut parts: Vec<String> = Vec::new();
56    if style.dim {
57        parts.push("2".to_string());
58    }
59    if style.bold {
60        parts.push("1".to_string());
61    }
62    if style.italic {
63        parts.push("3".to_string());
64    }
65    if style.underline {
66        parts.push("4".to_string());
67    }
68    if style.blink {
69        parts.push("5".to_string());
70    }
71    if style.inverse {
72        parts.push("7".to_string());
73    }
74    if style.strike {
75        parts.push("9".to_string());
76    }
77    if let Some(fg) = style.fg {
78        parts.push(format!("38;5;{}", fg));
79    }
80    if let Some(bg) = style.bg {
81        parts.push(format!("48;5;{}", bg));
82    }
83
84    if !parts.is_empty() {
85        out.push_str("\x1b[");
86        out.push_str(&parts.join(";"));
87        out.push('m');
88    }
89}