Skip to main content

prettyping_rs/render/
mod.rs

1pub mod palette;
2pub mod plain;
3pub mod terminal;
4
5use crate::config::Config;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct RenderConfig {
9    pub color: bool,
10    pub multicolor: bool,
11    pub unicode: bool,
12    pub legend: bool,
13    pub globalstats: bool,
14    pub recentstats: bool,
15    pub last: u32,
16    pub columns: Option<u16>,
17    pub lines: Option<u16>,
18    pub rttmin: Option<u32>,
19    pub rttmax: Option<u32>,
20}
21
22impl From<&Config> for RenderConfig {
23    fn from(value: &Config) -> Self {
24        Self {
25            color: value.color,
26            multicolor: value.multicolor,
27            unicode: value.unicode,
28            legend: value.legend,
29            globalstats: value.globalstats,
30            recentstats: value.recentstats,
31            last: value.last,
32            columns: value.columns,
33            lines: value.lines,
34            rttmin: value.rttmin,
35            rttmax: value.rttmax,
36        }
37    }
38}
39
40pub(crate) fn format_global_stats_line_terminal(
41    snapshot: &crate::stats::GlobalStatsSnapshot,
42) -> String {
43    format!(
44        "{:>2}/{:>3} ({:>2}%) lost; {:>4}/{:>4}/{:>4}ms; last: {:>4}ms",
45        snapshot.loss.lost,
46        snapshot.loss.total,
47        snapshot.loss.percent,
48        snapshot.rtt.min_ms,
49        snapshot.rtt.avg_ms,
50        snapshot.rtt.max_ms,
51        snapshot.last_rtt_ms
52    )
53}
54
55pub(crate) fn format_global_stats_line_plain(
56    snapshot: &crate::stats::GlobalStatsSnapshot,
57) -> String {
58    format!(
59        "{:>2}/{:>3} ({:>2}%) lost; {:>4}/{:>4}/{:>4}ms",
60        snapshot.loss.lost,
61        snapshot.loss.total,
62        snapshot.loss.percent,
63        snapshot.rtt.min_ms,
64        snapshot.rtt.avg_ms,
65        snapshot.rtt.max_ms
66    )
67}
68
69pub(crate) fn format_recent_stats_line(snapshot: &crate::stats::RecentStatsSnapshot) -> String {
70    format!(
71        "{:>2}/{:>3} ({:>2}%) lost; {:>4}/{:>4}/{:>4}/{:>4}ms stddev (last {})",
72        snapshot.loss.lost,
73        snapshot.loss.total,
74        snapshot.loss.percent,
75        snapshot.rtt.min_ms,
76        snapshot.rtt.avg_ms,
77        snapshot.rtt.max_ms,
78        snapshot.rtt.stddev_ms,
79        snapshot.rtt.count
80    )
81}
82
83pub(crate) fn trim_to_width(line: &str, width: usize) -> String {
84    if width == 0 {
85        return String::new();
86    }
87
88    line.chars().take(width).collect()
89}
90
91/// Trims a string to a visible width, keeping ANSI escape sequences intact.
92///
93/// This is primarily needed for rendering the startup legend, which may contain
94/// color escape codes (SGR). Escape sequences do not count towards the visible
95/// width.
96pub(crate) fn trim_ansi_to_width(input: &str, width: usize) -> String {
97    if width == 0 || input.is_empty() {
98        return String::new();
99    }
100
101    let bytes = input.as_bytes();
102    let mut out = String::new();
103    let mut idx = 0usize;
104    let mut visible = 0usize;
105    let mut saw_escape = false;
106
107    while idx < bytes.len() {
108        if bytes[idx] == b'\x1b' {
109            saw_escape = true;
110
111            // CSI: ESC [ ... <final byte>
112            if idx + 1 < bytes.len() && bytes[idx + 1] == b'[' {
113                let mut j = idx + 2;
114                while j < bytes.len() {
115                    let b = bytes[j];
116                    // Final byte is in the range 0x40..=0x7E.
117                    if (0x40..=0x7E).contains(&b) {
118                        j += 1;
119                        break;
120                    }
121                    j += 1;
122                }
123
124                out.push_str(&input[idx..j.min(bytes.len())]);
125                idx = j.min(bytes.len());
126                continue;
127            }
128
129            // Non-CSI 2-byte sequences like ESC 7 / ESC 8.
130            if idx + 1 < bytes.len() {
131                out.push_str(&input[idx..idx + 2]);
132                idx += 2;
133                continue;
134            }
135
136            // Trailing ESC without payload.
137            out.push('\x1b');
138            break;
139        }
140
141        let ch = input[idx..].chars().next().unwrap();
142        if visible >= width {
143            break;
144        }
145
146        out.push(ch);
147        visible += 1;
148        idx += ch.len_utf8();
149    }
150
151    // If we truncated after emitting escape sequences, reset to avoid color bleed.
152    if saw_escape && idx < bytes.len() {
153        out.push_str("\x1b[0m");
154    }
155
156    out
157}