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}