Skip to main content

vta_cli_common/
render.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Color, Modifier},
5    widgets::Widget,
6};
7
8// ── ANSI constants ──────────────────────────────────────────────────
9
10pub const BOLD: &str = "\x1b[1m";
11pub const DIM: &str = "\x1b[2m";
12pub const GREEN: &str = "\x1b[32m";
13pub const RED: &str = "\x1b[31m";
14pub const CYAN: &str = "\x1b[36m";
15pub const YELLOW: &str = "\x1b[33m";
16pub const RESET: &str = "\x1b[0m";
17
18// ── Ratatui rendering helpers ───────────────────────────────────────
19
20pub fn print_widget(widget: impl Widget, height: u16) {
21    let width = ratatui::crossterm::terminal::size().map_or(120, |(w, _)| w);
22    let area = Rect::new(0, 0, width, height);
23    let mut buf = Buffer::empty(area);
24    widget.render(area, &mut buf);
25
26    let mut out = String::new();
27    for y in 0..height {
28        let mut cur_fg = Color::Reset;
29        let mut cur_bg = Color::Reset;
30        let mut cur_mod = Modifier::empty();
31
32        for x in 0..width {
33            let cell = &buf[(x, y)];
34            if cell.skip {
35                continue;
36            }
37
38            if cell.fg != cur_fg || cell.bg != cur_bg || cell.modifier != cur_mod {
39                out.push_str("\x1b[0m");
40                push_ansi_fg(&mut out, cell.fg);
41                push_ansi_bg(&mut out, cell.bg);
42                push_ansi_mod(&mut out, cell.modifier);
43                cur_fg = cell.fg;
44                cur_bg = cell.bg;
45                cur_mod = cell.modifier;
46            }
47
48            out.push_str(cell.symbol());
49        }
50        out.push_str("\x1b[0m\n");
51    }
52
53    print!("{out}");
54}
55
56pub fn push_ansi_fg(out: &mut String, color: Color) {
57    use std::fmt::Write as _;
58    match color {
59        Color::Reset => {}
60        Color::Black => out.push_str("\x1b[30m"),
61        Color::Red => out.push_str("\x1b[31m"),
62        Color::Green => out.push_str("\x1b[32m"),
63        Color::Yellow => out.push_str("\x1b[33m"),
64        Color::Blue => out.push_str("\x1b[34m"),
65        Color::Magenta => out.push_str("\x1b[35m"),
66        Color::Cyan => out.push_str("\x1b[36m"),
67        Color::Gray => out.push_str("\x1b[37m"),
68        Color::DarkGray => out.push_str("\x1b[90m"),
69        Color::LightRed => out.push_str("\x1b[91m"),
70        Color::LightGreen => out.push_str("\x1b[92m"),
71        Color::LightYellow => out.push_str("\x1b[93m"),
72        Color::LightBlue => out.push_str("\x1b[94m"),
73        Color::LightMagenta => out.push_str("\x1b[95m"),
74        Color::LightCyan => out.push_str("\x1b[96m"),
75        Color::White => out.push_str("\x1b[97m"),
76        Color::Rgb(r, g, b) => {
77            let _ = write!(out, "\x1b[38;2;{r};{g};{b}m");
78        }
79        Color::Indexed(i) => {
80            let _ = write!(out, "\x1b[38;5;{i}m");
81        }
82    }
83}
84
85pub fn push_ansi_bg(out: &mut String, color: Color) {
86    use std::fmt::Write as _;
87    match color {
88        Color::Reset => {}
89        Color::Black => out.push_str("\x1b[40m"),
90        Color::Red => out.push_str("\x1b[41m"),
91        Color::Green => out.push_str("\x1b[42m"),
92        Color::Yellow => out.push_str("\x1b[43m"),
93        Color::Blue => out.push_str("\x1b[44m"),
94        Color::Magenta => out.push_str("\x1b[45m"),
95        Color::Cyan => out.push_str("\x1b[46m"),
96        Color::Gray => out.push_str("\x1b[47m"),
97        Color::DarkGray => out.push_str("\x1b[100m"),
98        Color::LightRed => out.push_str("\x1b[101m"),
99        Color::LightGreen => out.push_str("\x1b[102m"),
100        Color::LightYellow => out.push_str("\x1b[103m"),
101        Color::LightBlue => out.push_str("\x1b[104m"),
102        Color::LightMagenta => out.push_str("\x1b[105m"),
103        Color::LightCyan => out.push_str("\x1b[106m"),
104        Color::White => out.push_str("\x1b[107m"),
105        Color::Rgb(r, g, b) => {
106            let _ = write!(out, "\x1b[48;2;{r};{g};{b}m");
107        }
108        Color::Indexed(i) => {
109            let _ = write!(out, "\x1b[48;5;{i}m");
110        }
111    }
112}
113
114pub fn push_ansi_mod(out: &mut String, modifier: Modifier) {
115    if modifier.contains(Modifier::BOLD) {
116        out.push_str("\x1b[1m");
117    }
118    if modifier.contains(Modifier::DIM) {
119        out.push_str("\x1b[2m");
120    }
121    if modifier.contains(Modifier::ITALIC) {
122        out.push_str("\x1b[3m");
123    }
124    if modifier.contains(Modifier::UNDERLINED) {
125        out.push_str("\x1b[4m");
126    }
127    if modifier.contains(Modifier::REVERSED) {
128        out.push_str("\x1b[7m");
129    }
130    if modifier.contains(Modifier::CROSSED_OUT) {
131        out.push_str("\x1b[9m");
132    }
133}
134
135pub fn print_section(title: &str) {
136    let pad = 46usize.saturating_sub(title.len());
137    println!(
138        "\n{DIM}──{RESET} {BOLD}{title}{RESET} {DIM}{}{RESET}",
139        "─".repeat(pad)
140    );
141}