1#![forbid(unsafe_code)]
27use std::fmt::{Display, Write};
28
29use alacritty_terminal::{
30 term::{
31 cell::{Cell as AlacrittyCell, Flags},
32 test::TermSize,
33 Config, Term as AlacrittyTerm,
34 },
35 vte::{self, ansi::Processor},
36};
37
38mod ansi;
39mod colors;
40
41pub use ansi::AnsiSignal;
42use colors::Colors;
43
44const FONT_SIZE_PX: f32 = 12.;
46
47#[derive(Clone, Copy, Debug)]
49pub struct FontMetrics {
50 pub units_per_em: u16,
56 pub advance: f32,
58 pub line_height: f32,
60 pub descent: f32,
63}
64
65impl FontMetrics {
66 pub const DEFAULT: FontMetrics = FontMetrics {
79 units_per_em: 1000,
80 advance: 600.,
81 line_height: 1200.,
82 descent: 300.,
83
84 };
115}
116
117impl Default for FontMetrics {
118 fn default() -> Self {
119 FontMetrics::DEFAULT
120 }
121}
122
123#[derive(Clone, Copy)]
125struct CalculatedFontMetrics {
126 advance: f32,
128 line_height: f32,
131 descent: f32,
134}
135
136impl FontMetrics {
137 fn at_font_size(self, font_size: f32) -> CalculatedFontMetrics {
139 let scale_factor = font_size / f32::from(self.units_per_em);
140 CalculatedFontMetrics {
141 advance: self.advance * scale_factor,
142 line_height: self.line_height * scale_factor,
143 descent: self.descent * scale_factor,
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub struct Rgb {
151 pub r: u8,
152 pub g: u8,
153 pub b: u8,
154}
155
156impl Display for Rgb {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 write!(f, "#{:02x?}{:02x?}{:02x}", self.r, self.g, self.b)
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub struct Cell {
165 pub c: char,
166 pub fg: Rgb,
167 pub bg: Rgb,
168 pub bold: bool,
169 pub italic: bool,
170 pub underline: bool,
171 pub strikethrough: bool,
172}
173
174impl Cell {
175 fn from_alacritty_cell(colors: &Colors, cell: &AlacrittyCell) -> Self {
176 Cell {
177 c: cell.c,
178 fg: colors.to_rgb(cell.fg),
179 bg: colors.to_rgb(cell.bg),
180 bold: cell.flags.intersects(Flags::BOLD),
181 italic: cell.flags.intersects(Flags::ITALIC),
182 underline: cell.flags.intersects(Flags::ALL_UNDERLINES),
183 strikethrough: cell.flags.intersects(Flags::STRIKEOUT),
184 }
185 }
186}
187
188#[derive(PartialEq)]
189struct TextStyle {
190 fg: Rgb,
191 bold: bool,
192 italic: bool,
193 underline: bool,
194 strikethrough: bool,
195}
196
197impl TextStyle {
198 fn from_cell(cell: &Cell) -> Self {
200 let Cell {
201 fg,
202 bold,
203 italic,
204 underline,
205 strikethrough,
206 ..
207 } = *cell;
208
209 TextStyle {
210 fg,
211 bold,
212 italic,
213 underline,
214 strikethrough,
215 }
216 }
217}
218
219struct TextLine {
220 text: Vec<char>,
221}
222
223impl TextLine {
224 fn with_capacity(capacity: usize) -> Self {
225 TextLine {
226 text: Vec::with_capacity(capacity),
227 }
228 }
229
230 fn push_cell(&mut self, char: char) {
231 self.text.push(char);
232 }
233
234 fn clear(&mut self) {
235 self.text.clear();
236 }
237
238 fn len(&self) -> usize {
239 self.text.len()
240 }
241
242 fn is_empty(&self) -> bool {
243 self.len() == 0
244 }
245
246 fn chars(&self) -> &[char] {
248 let trailing_whitespace_chars = self
249 .text
250 .iter()
251 .rev()
252 .position(|c| !c.is_whitespace())
253 .unwrap_or(self.text.len());
254 let end = self.text.len() - trailing_whitespace_chars;
255 &self.text[..end]
256 }
257}
258
259fn fmt_rect(
260 f: &mut std::fmt::Formatter<'_>,
261 x0: u16,
262 y0: u16,
263 x1: u16,
264 y1: u16,
265 color: Rgb,
266 font_metrics: &CalculatedFontMetrics,
267) -> std::fmt::Result {
268 writeln!(
269 f,
270 r#"<rect x="{x}" y="{y}" width="{width}" height="{height}" style="fill: {color};" />"#,
271 x = f32::from(x0) * font_metrics.advance,
272 y = f32::from(y0) * font_metrics.line_height,
273 width = f32::from(x1 - x0 + 1) * font_metrics.advance,
274 height = f32::from(y1 - y0 + 1) * font_metrics.line_height,
275 color = color,
276 )
277}
278
279fn fmt_text(
280 f: &mut std::fmt::Formatter<'_>,
281 x: u16,
282 y: u16,
283 text: &TextLine,
284 style: &TextStyle,
285 font_metrics: &CalculatedFontMetrics,
286) -> std::fmt::Result {
287 let chars = text.chars();
288 let text_length = chars.len() as f32 * font_metrics.advance;
289 write!(
290 f,
291 r#"<text x="{x}" y="{y}" textLength="{text_length}" style="fill: {color};"#,
292 x = f32::from(x) * font_metrics.advance,
293 y = f32::from(y + 1) * font_metrics.line_height - font_metrics.descent,
294 color = style.fg,
295 )?;
296
297 if style.bold {
298 f.write_str(" font-weight: 600;")?;
299 }
300 if style.italic {
301 f.write_str(" font-style: italic;")?;
302 }
303 if style.underline || style.strikethrough {
304 f.write_char(' ')?;
305 if style.underline {
306 f.write_str(" underline")?;
307 }
308 if style.strikethrough {
309 f.write_str(" line-through")?;
310 }
311 }
312
313 f.write_str(r#"">"#)?;
314 let mut prev_char_was_space = false;
315 for char in chars {
316 match *char {
317 ' ' => {
318 if prev_char_was_space {
319 f.write_str(" ")?
321 } else {
322 f.write_char(' ')?
323 }
324 }
325 '<' => f.write_str("<")?,
327 '&' => f.write_str("&")?,
328 c => f.write_char(c)?,
329 }
330
331 prev_char_was_space = *char == ' ';
332 }
333 f.write_str("</text>\n")?;
334
335 Ok(())
336}
337
338pub struct Screen {
340 lines: u16,
341 columns: u16,
342 cells: Vec<Cell>,
343}
344
345impl Screen {
346 pub fn to_svg<'s, 'f>(
352 &'s self,
353 fonts: &'f [&'f str],
354 font_metrics: FontMetrics,
355 ) -> impl Display + 's
356 where
357 'f: 's,
358 {
359 struct Svg<'s> {
360 screen: &'s Screen,
361 fonts: &'s [&'s str],
362 font_metrics: CalculatedFontMetrics,
363 }
364
365 impl<'s> Display for Svg<'s> {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 let font_metrics = self.font_metrics;
368
369 let Screen {
370 lines,
371 columns,
372 ref cells,
373 } = self.screen;
374
375 write!(
376 f,
377 r#"<svg viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">"#,
378 f32::from(*columns) * font_metrics.advance,
379 f32::from(*lines) * font_metrics.line_height,
380 )?;
381
382 f.write_str(
383 "
384<style>
385 .screen {
386 font-family: ",
387 )?;
388
389 for font in self.fonts {
390 f.write_char('"')?;
391 f.write_str(font)?;
392 f.write_str("\", ")?;
393 }
394
395 write!(
396 f,
397 r#"monospace;
398 font-size: {FONT_SIZE_PX}px;
399 }}
400</style>
401<g class="screen">
402"#,
403 )?;
404
405 let main_bg = colors::most_common_color(self.screen);
406 fmt_rect(
407 f,
408 0,
409 0,
410 self.screen.columns().saturating_sub(1),
411 self.screen.lines().saturating_sub(1),
412 main_bg,
413 &font_metrics,
414 )?;
415
416 let mut drawn = vec![false; usize::from(*lines) * usize::from(*columns)];
418 for y0 in 0..*lines {
419 for x0 in 0..*columns {
420 let idx = self.screen.idx(y0, x0);
421
422 if drawn[idx] {
423 continue;
424 }
425
426 let cell = &cells[idx];
427 let bg = cell.bg;
428
429 if bg == main_bg {
430 continue;
431 }
432
433 let mut end_x = x0;
434 let mut end_y = y0;
435
436 for x1 in x0 + 1..*columns {
437 let idx = self.screen.idx(y0, x1);
438 let cell = &cells[idx];
439 if cell.bg == bg {
440 end_x = x1;
441 } else {
442 break;
443 }
444 }
445
446 for y1 in y0 + 1..*lines {
447 let mut all = true;
448 for x1 in x0 + 1..*columns {
449 let idx = self.screen.idx(y1, x1);
450 let cell = &cells[idx];
451 if cell.bg != bg {
452 all = false;
453 break;
454 }
455 }
456 if !all {
457 break;
458 }
459 end_y = y1;
460 }
461
462 {
463 for y in y0..=end_y {
464 for x in x0..=end_x {
465 let idx = self.screen.idx(y, x);
466 drawn[idx] = true;
467 }
468 }
469 }
470
471 fmt_rect(f, x0, y0, end_x, end_y, bg, &font_metrics)?;
472 }
473 }
474
475 let mut text_line =
477 TextLine::with_capacity(usize::from(*columns).next_power_of_two());
478 for y in 0..*lines {
479 let idx = self.screen.idx(y, 0);
480 let cell = &cells[idx];
481 let mut style = TextStyle::from_cell(cell);
482 let mut start_x = 0;
483
484 for x in 0..*columns {
485 let idx = self.screen.idx(y, x);
486 let cell = &cells[idx];
487 let style_ = TextStyle::from_cell(cell);
488
489 if style_ != style {
490 if !text_line.is_empty() {
491 fmt_text(f, start_x, y, &text_line, &style, &font_metrics)?;
492 }
493 text_line.clear();
494 style = style_;
495 }
496
497 if text_line.is_empty() {
498 start_x = x;
499 if cell.c == ' ' {
500 continue;
501 }
502 }
503
504 text_line.push_cell(cell.c);
505 }
506
507 if !text_line.is_empty() {
508 fmt_text(f, start_x, y, &text_line, &style, &font_metrics)?;
509 text_line.clear();
510 }
511 }
512
513 f.write_str(
514 "</g>
515</svg>",
516 )?;
517
518 Ok(())
519 }
520 }
521
522 Svg {
523 screen: self,
524 fonts,
525 font_metrics: font_metrics.at_font_size(FONT_SIZE_PX),
526 }
527 }
528
529 #[inline(always)]
530 fn idx(&self, y: u16, x: u16) -> usize {
531 usize::from(y) * usize::from(self.columns) + usize::from(x)
532 }
533
534 pub fn lines(&self) -> u16 {
536 self.lines
537 }
538
539 pub fn columns(&self) -> u16 {
541 self.columns
542 }
543
544 pub fn cells(&self) -> impl Iterator<Item = &Cell> {
547 self.cells.iter()
548 }
549
550 pub fn get(&self, line: u16, column: u16) -> Option<&Cell> {
552 self.cells.get(self.idx(line, column))
553 }
554}
555
556pub trait PtyWriter {
560 fn write(&mut self, text: String);
562}
563
564impl<F: FnMut(String)> PtyWriter for F {
565 fn write(&mut self, text: String) {
566 self(text)
567 }
568}
569
570pub struct VoidPtyWriter;
572
573impl PtyWriter for VoidPtyWriter {
574 fn write(&mut self, _text: String) {}
575}
576
577struct EventProxy<Ev> {
578 handler: std::cell::RefCell<Ev>,
579}
580
581impl<W: PtyWriter> alacritty_terminal::event::EventListener for EventProxy<W> {
582 fn send_event(&self, event: alacritty_terminal::event::Event) {
583 use alacritty_terminal::event::Event as AEvent;
584 match event {
585 AEvent::PtyWrite(text) => self.handler.borrow_mut().write(text),
586 _ev => {}
587 }
588 }
589}
590
591pub struct Term<W: PtyWriter> {
593 lines: u16,
594 columns: u16,
595 term: AlacrittyTerm<EventProxy<W>>,
596 processor: Option<vte::ansi::Processor<vte::ansi::StdSyncHandler>>,
597}
598
599impl<W: PtyWriter> Term<W> {
600 pub fn new(lines: u16, columns: u16, pty_writer: W) -> Self {
605 let term = AlacrittyTerm::new(
606 Config::default(),
607 &TermSize {
608 columns: columns.into(),
609 screen_lines: lines.into(),
610 },
611 EventProxy {
612 handler: pty_writer.into(),
613 },
614 );
615
616 Term {
617 lines,
618 columns,
619 term,
620 processor: Some(Processor::new()),
621 }
622 }
623
624 pub fn process(&mut self, byte: u8) {
626 self.processor
627 .as_mut()
628 .expect("unreachable")
629 .advance(&mut self.term, byte);
630 }
631
632 pub fn process_with_callback(&mut self, byte: u8, mut cb: impl FnMut(&Self, AnsiSignal)) {
639 let mut processor = self.processor.take().expect("unreachable");
640
641 let mut handler = ansi::HandlerWrapper {
642 term: self,
643 cb: &mut cb,
644 };
645
646 processor.advance(&mut handler, byte);
647 self.processor = Some(processor);
648 }
649
650 pub fn resize(&mut self, lines: u16, columns: u16) {
652 let new_size = TermSize {
653 columns: columns.into(),
654 screen_lines: lines.into(),
655 };
656 self.lines = lines;
657 self.columns = columns;
658 self.term.resize(new_size);
659 }
660
661 pub fn current_screen(&self) -> Screen {
663 let colors = Colors::default();
665
666 Screen {
667 lines: self.lines,
668 columns: self.columns,
669 cells: self
670 .term
671 .grid()
672 .display_iter()
673 .map(|point_cell| Cell::from_alacritty_cell(&colors, point_cell.cell))
674 .collect(),
675 }
676 }
677}
678
679pub fn emulate(lines: u16, columns: u16, ansi_sequence: &[u8]) -> Screen {
681 let mut term = Term::new(lines, columns, VoidPtyWriter);
682 for &byte in ansi_sequence {
683 term.process(byte);
684 }
685 term.current_screen()
686}
687
688#[cfg(test)]
689mod test {
690 #[test]
691 fn test() {
692 let screen = super::emulate(24, 80, include_bytes!("./tests/ls.txt"));
693 let expected = "total 60
694drwxr-xr-x 6 thomas users 4096 Jun 19 15:58 .
695drwxr-xr-x 34 thomas users 4096 Jun 16 10:28 ..
696-rw-r--r-- 1 thomas users 19422 Jun 18 17:22 Cargo.lock
697-rw-r--r-- 1 thomas users 749 Jun 19 11:33 Cargo.toml
698-rw-r--r-- 1 thomas users 1940 Jun 16 11:19 flake.lock
699-rw-r--r-- 1 thomas users 640 Jun 16 11:19 flake.nix
700drwxr-xr-x 7 thomas users 4096 Jun 16 11:19 .git
701-rw-r--r-- 1 thomas users 231 Jun 16 11:30 README.md
702drwxr-xr-x 2 thomas users 4096 Jun 19 12:20 src
703drwxr-xr-x 3 thomas users 4096 Jun 18 14:36 target
704drwxr-xr-x 3 thomas users 4096 Jun 18 11:22 termsnap-lib";
705
706 let mut line = 0;
707 let mut column = 0;
708
709 for c in expected.chars() {
710 match c {
711 '\n' => {
712 for column in column..80 {
713 let idx = screen.idx(line, column);
714 assert_eq!(screen.cells[idx].c, ' ', "failed at {line}x{column}");
715 }
716 column = 0;
717 line += 1;
718 }
719 _ => {
720 let idx = screen.idx(line, column);
721 assert_eq!(screen.cells[idx].c, c, "failed at {line}x{column}");
722 column += 1;
723 }
724 }
725 }
726 }
727}