1use alacritty_terminal::event::EventListener;
13use alacritty_terminal::grid::Dimensions;
14use alacritty_terminal::index::{Column, Line};
15use alacritty_terminal::term::cell::Flags;
16use alacritty_terminal::term::{Term, TermMode};
17use alacritty_terminal::vte::ansi::{Color, NamedColor};
18
19pub fn repaint<T: EventListener>(term: &Term<T>) -> Vec<u8> {
26 let grid = term.grid();
27 let rows = grid.screen_lines();
28 let cols = grid.columns();
29 let mut out = Vec::with_capacity(rows * cols * 2);
30 let mode = term.mode();
31 out.extend_from_slice(b"\x1b[0m");
32 toggle(&mut out, mode.contains(TermMode::ALT_SCREEN), "?1049");
34 toggle(&mut out, mode.contains(TermMode::BRACKETED_PASTE), "?2004");
35 toggle(&mut out, mode.contains(TermMode::APP_CURSOR), "?1");
36 toggle(&mut out, mode.contains(TermMode::MOUSE_REPORT_CLICK), "?1000");
37 toggle(&mut out, mode.contains(TermMode::MOUSE_MOTION), "?1002");
38 toggle(&mut out, mode.contains(TermMode::SGR_MOUSE), "?1006");
39 toggle(&mut out, mode.contains(TermMode::ALTERNATE_SCROLL), "?1007");
40 toggle(&mut out, mode.contains(TermMode::INSERT), "4");
41 toggle(&mut out, mode.contains(TermMode::LINE_FEED_NEW_LINE), "20");
42 out.extend_from_slice(if mode.contains(TermMode::APP_KEYPAD) { b"\x1b=" } else { b"\x1b>" });
44 out.extend_from_slice(b"\x1b[H\x1b[2J");
45 toggle(&mut out, false, "?6");
49 let mut current: Option<Style> = None;
50 for row in 0..rows {
51 out.extend_from_slice(format!("\x1b[{};1H", row + 1).as_bytes());
54 let line = Line(i32::try_from(row).unwrap_or(i32::MAX));
55 for col in 0..cols {
56 let cell = &grid[line][Column(col)];
57 if cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) {
58 continue;
59 }
60 let style = Style { fg: cell.fg, bg: cell.bg, flags: cell.flags };
61 if current != Some(style) {
62 out.extend_from_slice(b"\x1b[0m");
63 out.extend_from_slice(style.sgr().as_bytes());
64 current = Some(style);
65 }
66 let mut buf = [0u8; 4];
67 out.extend_from_slice(cell.c.encode_utf8(&mut buf).as_bytes());
68 }
69 }
70 out.extend_from_slice(b"\x1b[0m");
71 toggle(&mut out, mode.contains(TermMode::ORIGIN), "?6");
76 let point = grid.cursor.point;
77 let last_row = i32::try_from(rows.saturating_sub(1)).unwrap_or(i32::MAX);
78 let row = point.line.0.clamp(0, last_row) + 1;
79 let col = point.column.0 + 1;
80 out.extend_from_slice(format!("\x1b[{row};{col}H").as_bytes());
81 toggle(&mut out, mode.contains(TermMode::SHOW_CURSOR), "?25");
82 out
83}
84
85fn toggle(out: &mut Vec<u8>, on: bool, mode: &str) {
87 out.extend_from_slice(format!("\x1b[{mode}{}", if on { 'h' } else { 'l' }).as_bytes());
88}
89
90#[derive(Clone, Copy, PartialEq, Eq)]
92struct Style {
93 fg: Color,
94 bg: Color,
95 flags: Flags,
96}
97
98impl Style {
99 fn sgr(self) -> String {
100 let mut parts = Vec::new();
101 if self.flags.contains(Flags::BOLD) {
102 parts.push("1".to_owned());
103 }
104 if self.flags.contains(Flags::DIM) {
105 parts.push("2".to_owned());
106 }
107 if self.flags.contains(Flags::ITALIC) {
108 parts.push("3".to_owned());
109 }
110 if self.flags.intersects(Flags::ALL_UNDERLINES) {
111 parts.push("4".to_owned());
112 }
113 if self.flags.contains(Flags::INVERSE) {
114 parts.push("7".to_owned());
115 }
116 if self.flags.contains(Flags::HIDDEN) {
117 parts.push("8".to_owned());
118 }
119 if self.flags.contains(Flags::STRIKEOUT) {
120 parts.push("9".to_owned());
121 }
122 parts.push(fg_sgr(self.fg));
123 parts.push(bg_sgr(self.bg));
124 format!("\x1b[{}m", parts.join(";"))
125 }
126}
127
128fn fg_sgr(color: Color) -> String {
129 match color {
130 Color::Named(named) => match named {
131 NamedColor::Foreground
132 | NamedColor::Background
133 | NamedColor::Cursor
134 | NamedColor::BrightForeground
135 | NamedColor::DimForeground => "39".to_owned(),
136 other => {
137 let index = named_index(other);
138 if index < 8 { format!("{}", 30 + index) } else { format!("{}", 90 + index - 8) }
139 }
140 },
141 Color::Indexed(index) => format!("38;5;{index}"),
142 Color::Spec(rgb) => format!("38;2;{};{};{}", rgb.r, rgb.g, rgb.b),
143 }
144}
145
146fn bg_sgr(color: Color) -> String {
147 match color {
148 Color::Named(named) => match named {
149 NamedColor::Background
150 | NamedColor::Foreground
151 | NamedColor::Cursor
152 | NamedColor::BrightForeground
153 | NamedColor::DimForeground => "49".to_owned(),
154 other => {
155 let index = named_index(other);
156 if index < 8 { format!("{}", 40 + index) } else { format!("{}", 100 + index - 8) }
157 }
158 },
159 Color::Indexed(index) => format!("48;5;{index}"),
160 Color::Spec(rgb) => format!("48;2;{};{};{}", rgb.r, rgb.g, rgb.b),
161 }
162}
163
164fn named_index(named: NamedColor) -> u8 {
167 match named {
168 NamedColor::Black | NamedColor::DimBlack => 0,
169 NamedColor::Red | NamedColor::DimRed => 1,
170 NamedColor::Green | NamedColor::DimGreen => 2,
171 NamedColor::Yellow | NamedColor::DimYellow => 3,
172 NamedColor::Blue | NamedColor::DimBlue => 4,
173 NamedColor::Magenta | NamedColor::DimMagenta => 5,
174 NamedColor::Cyan | NamedColor::DimCyan => 6,
175 NamedColor::White
176 | NamedColor::DimWhite
177 | NamedColor::Foreground
178 | NamedColor::Background
179 | NamedColor::Cursor
180 | NamedColor::BrightForeground
181 | NamedColor::DimForeground => 7,
182 NamedColor::BrightBlack => 8,
183 NamedColor::BrightRed => 9,
184 NamedColor::BrightGreen => 10,
185 NamedColor::BrightYellow => 11,
186 NamedColor::BrightBlue => 12,
187 NamedColor::BrightMagenta => 13,
188 NamedColor::BrightCyan => 14,
189 NamedColor::BrightWhite => 15,
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use alacritty_terminal::event::VoidListener;
197 use alacritty_terminal::grid::Dimensions;
198 use alacritty_terminal::term::Config;
199 use alacritty_terminal::vte::ansi::Processor;
200
201 struct Size {
202 cols: usize,
203 rows: usize,
204 }
205
206 impl Dimensions for Size {
207 fn total_lines(&self) -> usize {
208 self.rows
209 }
210 fn screen_lines(&self) -> usize {
211 self.rows
212 }
213 fn columns(&self) -> usize {
214 self.cols
215 }
216 }
217
218 fn term_with(bytes: &[u8], cols: usize, rows: usize) -> Term<VoidListener> {
219 let mut term = Term::new(
220 Config { scrolling_history: 100, ..Default::default() },
221 &Size { cols, rows },
222 VoidListener,
223 );
224 let mut parser: Processor = Processor::new();
225 parser.advance(&mut term, bytes);
226 term
227 }
228
229 #[test]
233 fn the_repaint_reproduces_the_screen() {
234 let source = term_with(b"hello\r\n\x1b[31mred\x1b[0m world", 20, 5);
235 let bytes = repaint(&source);
236 let repainted = term_with(&bytes, 20, 5);
237 let before = crate::screen::extract(&source);
238 let after = crate::screen::extract(&repainted);
239 assert_eq!(after.text, before.text, "the text survives the repaint");
240 assert_eq!(after.cursor, before.cursor, "and so does the cursor");
241 }
242
243 #[test]
246 fn a_coloured_cell_carries_its_colour() {
247 let term = term_with(b"\x1b[31mred", 20, 5);
248 let bytes = repaint(&term);
249 let text = String::from_utf8_lossy(&bytes);
250 assert!(text.contains("\x1b[0m\x1b[31;49m"), "the red foreground is emitted: {text:?}");
251 }
252
253 #[test]
255 fn a_truecolour_cell_carries_its_rgb() {
256 let term = term_with(b"\x1b[38;2;1;2;3mX", 20, 5);
257 let bytes = repaint(&term);
258 let text = String::from_utf8_lossy(&bytes);
259 assert!(text.contains("38;2;1;2;3"), "{text:?}");
260 }
261
262 #[test]
266 fn the_repaint_carries_the_terminal_modes() {
267 let source =
268 term_with(b"\x1b[?1049h\x1b[?25l\x1b[?2004h\x1b[?1000h\x1b[?1h\x1b[4hX", 20, 5);
269 let repainted = term_with(&repaint(&source), 20, 5);
270 let mode = *repainted.mode();
271 assert!(mode.contains(TermMode::ALT_SCREEN), "alternate screen: {mode:?}");
272 assert!(!mode.contains(TermMode::SHOW_CURSOR), "hidden cursor: {mode:?}");
273 assert!(mode.contains(TermMode::BRACKETED_PASTE), "bracketed paste: {mode:?}");
274 assert!(mode.contains(TermMode::MOUSE_REPORT_CLICK), "mouse clicks: {mode:?}");
275 assert!(mode.contains(TermMode::APP_CURSOR), "application cursor: {mode:?}");
276 assert!(mode.contains(TermMode::INSERT), "insert mode: {mode:?}");
277 }
278
279 #[test]
285 fn the_repaint_sets_origin_mode_around_the_draw() {
286 let term = term_with(b"\x1b[?6hREADY", 20, 5);
287 let bytes = repaint(&term);
288 let text = String::from_utf8_lossy(&bytes);
289 let off = text.find("\x1b[?6l").expect("origin mode is cleared for the draw");
290 let first_row = text.find("\x1b[1;1H").expect("the first row is positioned");
291 assert!(off < first_row, "the rows are positioned with the mode off: {text:?}");
292 let on = text.rfind("\x1b[?6h").expect("the app's origin mode is restored");
293 let cursor = text.rfind("\x1b[1;6H").expect("the cursor is placed after the text");
294 assert!(on < cursor, "the cursor placement follows the origin toggle: {text:?}");
295 }
296
297 #[test]
300 fn the_repaint_ends_at_the_cursor() {
301 let term = term_with(b"abc", 20, 5);
302 let bytes = repaint(&term);
303 let text = String::from_utf8_lossy(&bytes);
304 let at = text.find("\x1b[1;4H");
305 assert!(
306 at.is_some_and(|at| text[..at].contains("abc")),
307 "the cursor is placed after 'abc': {text:?}",
308 );
309 }
310}