Skip to main content

pito_tui/
dump.rs

1use std::fmt::Write;
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use ratatui::buffer::{Buffer, Cell};
5use ratatui::style::{Color, Modifier};
6use unicode_width::UnicodeWidthStr;
7
8pub fn size(text: &str) -> Option<(u16, u16)> {
9    let (width, height) = text.split_once(['x', 'X'])?;
10    let size = (width.parse().ok()?, height.parse().ok()?);
11    (size.0 > 0 && size.1 > 0).then_some(size)
12}
13
14pub fn keys(text: &str) -> Result<Vec<KeyEvent>, String> {
15    let mut out = Vec::new();
16    let plain = |code| KeyEvent::new(code, KeyModifiers::NONE);
17    for word in text.split_whitespace() {
18        let named = match word.to_lowercase().as_str() {
19            "enter" => Some(plain(KeyCode::Enter)),
20            "esc" => Some(plain(KeyCode::Esc)),
21            "up" => Some(plain(KeyCode::Up)),
22            "down" => Some(plain(KeyCode::Down)),
23            "left" => Some(plain(KeyCode::Left)),
24            "right" => Some(plain(KeyCode::Right)),
25            "pgup" => Some(plain(KeyCode::PageUp)),
26            "pgdn" => Some(plain(KeyCode::PageDown)),
27            "home" => Some(plain(KeyCode::Home)),
28            "end" => Some(plain(KeyCode::End)),
29            "backspace" => Some(plain(KeyCode::Backspace)),
30            "delete" => Some(plain(KeyCode::Delete)),
31            "space" => Some(plain(KeyCode::Char(' '))),
32            "tab" => Some(plain(KeyCode::Tab)),
33            "backtab" => Some(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT)),
34            _ => None,
35        };
36        if let Some(key) = named {
37            out.push(key);
38            continue;
39        }
40        let modified = [
41            ("ctrl+", KeyModifiers::CONTROL),
42            ("alt+", KeyModifiers::ALT),
43        ]
44        .into_iter()
45        .find_map(|(prefix, modifier)| Some((word.strip_prefix(prefix)?, modifier)));
46        if let Some((rest, modifier)) = modified {
47            let mut chars = rest.chars();
48            match (chars.next(), chars.next()) {
49                (Some(c), None) => {
50                    out.push(KeyEvent::new(KeyCode::Char(c), modifier));
51                    continue;
52                }
53                _ => return Err(word.to_string()),
54            }
55        }
56        out.extend(word.chars().map(|c| plain(KeyCode::Char(c))));
57    }
58    Ok(out)
59}
60
61fn rows(buffer: &Buffer) -> impl Iterator<Item = &[Cell]> {
62    buffer.content.chunks(usize::from(buffer.area.width).max(1))
63}
64
65fn shown(row: &[Cell]) -> impl Iterator<Item = &Cell> {
66    let mut skip = 0;
67    row.iter().filter(move |cell| {
68        if skip > 0 {
69            skip -= 1;
70            return false;
71        }
72        skip = cell.symbol().width().saturating_sub(1);
73        true
74    })
75}
76
77pub fn text(buffer: &Buffer) -> String {
78    let mut out = String::new();
79    for (y, row) in rows(buffer).enumerate() {
80        if y > 0 {
81            out.push('\n');
82        }
83        let start = out.len();
84        for cell in shown(row) {
85            out.push_str(cell.symbol());
86        }
87        let end = out[start..].trim_end().len() + start;
88        out.truncate(end);
89    }
90    out
91}
92
93fn colour(out: &mut String, color: Color, base: u8) {
94    let bright = base + 60;
95    let _ = match color {
96        Color::Reset => return,
97        Color::Black => write!(out, ";{base}"),
98        Color::Red => write!(out, ";{}", base + 1),
99        Color::Green => write!(out, ";{}", base + 2),
100        Color::Yellow => write!(out, ";{}", base + 3),
101        Color::Blue => write!(out, ";{}", base + 4),
102        Color::Magenta => write!(out, ";{}", base + 5),
103        Color::Cyan => write!(out, ";{}", base + 6),
104        Color::Gray => write!(out, ";{}", base + 7),
105        Color::DarkGray => write!(out, ";{bright}"),
106        Color::LightRed => write!(out, ";{}", bright + 1),
107        Color::LightGreen => write!(out, ";{}", bright + 2),
108        Color::LightYellow => write!(out, ";{}", bright + 3),
109        Color::LightBlue => write!(out, ";{}", bright + 4),
110        Color::LightMagenta => write!(out, ";{}", bright + 5),
111        Color::LightCyan => write!(out, ";{}", bright + 6),
112        Color::White => write!(out, ";{}", bright + 7),
113        Color::Rgb(r, g, b) => write!(out, ";{};2;{r};{g};{b}", base + 8),
114        Color::Indexed(n) => write!(out, ";{};5;{n}", base + 8),
115    };
116}
117
118fn sgr(cell: &Cell) -> String {
119    let mut out = String::from("\x1b[0");
120    for (flag, code) in [
121        (Modifier::BOLD, ";1"),
122        (Modifier::DIM, ";2"),
123        (Modifier::ITALIC, ";3"),
124        (Modifier::UNDERLINED, ";4"),
125        (Modifier::REVERSED, ";7"),
126        (Modifier::CROSSED_OUT, ";9"),
127    ] {
128        if cell.modifier.contains(flag) {
129            out.push_str(code);
130        }
131    }
132    colour(&mut out, cell.fg, 30);
133    colour(&mut out, cell.bg, 40);
134    out.push('m');
135    out
136}
137
138pub fn ansi(buffer: &Buffer) -> String {
139    let mut out = String::from("\x1b[?25l\x1b[2J");
140    for (y, row) in rows(buffer).enumerate() {
141        let _ = write!(out, "\x1b[{};1H", y + 1);
142        let mut last = String::new();
143        for cell in shown(row) {
144            let style = sgr(cell);
145            if style != last {
146                out.push_str(&style);
147                last = style;
148            }
149            out.push_str(cell.symbol());
150        }
151        out.push_str("\x1b[0m");
152    }
153    out
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use ratatui::layout::Rect;
160    use ratatui::style::Style;
161
162    #[test]
163    fn sizes_and_keys_read_the_way_every_app_wires_them() {
164        assert_eq!(size("120x34"), Some((120, 34)));
165        assert_eq!(size("0x9"), None);
166        assert_eq!(size("wide"), None);
167        let keys = keys("n tab 25 enter ctrl+c alt+x").unwrap();
168        assert_eq!(keys.len(), 7);
169        assert_eq!(keys[1].code, KeyCode::Tab);
170        assert_eq!(keys[5].modifiers, KeyModifiers::CONTROL);
171        assert_eq!(keys[6].modifiers, KeyModifiers::ALT);
172        assert_eq!(super::keys("ctrl+xy"), Err("ctrl+xy".to_string()));
173    }
174
175    #[test]
176    fn text_and_ansi_keep_wide_glyphs_whole_and_carry_the_colours() {
177        let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
178        buffer.set_string(0, 0, "日本 ok", Style::new().fg(Color::Rgb(1, 2, 3)));
179        assert_eq!(text(&buffer), "日本 ok\n");
180        let ansi = ansi(&buffer);
181        assert!(ansi.starts_with("\x1b[?25l\x1b[2J\x1b[1;1H"));
182        assert!(ansi.contains("\x1b[2;1H"));
183        assert!(ansi.contains("38;2;1;2;3"));
184    }
185}