1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//!
//! Handles terminal ANSI -> TUI Style conversion
//!
//! # Example
//!
//! ```
//! use std::process::Command;
//!
//! let c = Command::new("ls")
//!     .args(&["--color=always", "~"])
//!     .output()
//!     .unwrap();
//!
//! let text = ansi4tui::bytes_to_text(c.stdout);
//! ```
//!

use termwiz::cell::{Blink, Intensity, Underline};
use termwiz::color::ColorSpec;
use termwiz::escape::{
    csi::{Sgr, CSI},
    parser::Parser,
    Action, ControlCode,
};
use tui::style::{Color, Modifier, Style};
use tui::text::{Span, Spans, Text};

/// Converts ANSI-escaped strings to tui-rs compatible text
pub fn bytes_to_text<'a, B: AsRef<[u8]>>(bytes: B) -> Text<'a> {
    let mut parser = Parser::new();
    let parsed = parser.parse_as_vec(bytes.as_ref());

    // each span will be a line
    let mut spans = Vec::<Spans>::new();

    // create span buffer
    let mut span_style = Style::default();
    let mut span_text = String::new();

    // list of spans that makes up a Spans
    let mut current_line = Vec::<Span>::new();

    for item in parsed {
        match item {
            // Somehow pass in terminal size here for string buffering, and handle linefeeds and carriage returns
            // separately rather than assuming linefeed includes cr?
            Action::Print(c) => {
                span_text.push(c);
            }
            Action::Control(ControlCode::LineFeed) => {
                // finish the current span
                current_line.push(Span::styled(span_text, span_style.into()));
                span_text = String::new();

                // finish the current line
                spans.push(Spans::from(current_line));
                current_line = Vec::new();
            }
            Action::CSI(CSI::Sgr(sgr)) => {
                // ignore this single condition, for the rest we'll close the current span
                if let Sgr::Font(_) = sgr {
                    continue;
                }

                // finish the current span
                current_line.push(Span::styled(span_text, span_style.into()));
                span_text = String::new();

                match sgr {
                    Sgr::Reset => span_style = Style::default(),
                    Sgr::Intensity(i) => match i {
                        Intensity::Bold => {
                            span_style = span_style.remove_modifier(Modifier::DIM);
                            span_style = span_style.add_modifier(Modifier::BOLD);
                        }
                        Intensity::Half => {
                            span_style = span_style.add_modifier(Modifier::DIM);
                            span_style = span_style.remove_modifier(Modifier::BOLD);
                        }
                        Intensity::Normal => {
                            span_style = span_style.remove_modifier(Modifier::DIM);
                            span_style = span_style.remove_modifier(Modifier::BOLD);
                        }
                    },
                    Sgr::Underline(u) => match u {
                        Underline::Double | Underline::Single => {
                            span_style = span_style.add_modifier(Modifier::UNDERLINED);
                        }
                        _ => span_style = span_style.remove_modifier(Modifier::UNDERLINED),
                    },
                    Sgr::Blink(b) => match b {
                        Blink::Slow => {
                            span_style = span_style.add_modifier(Modifier::SLOW_BLINK);
                            span_style = span_style.remove_modifier(Modifier::RAPID_BLINK);
                        }
                        Blink::Rapid => {
                            span_style = span_style.remove_modifier(Modifier::SLOW_BLINK);
                            span_style = span_style.add_modifier(Modifier::RAPID_BLINK);
                        }
                        Blink::None => {
                            span_style = span_style.remove_modifier(Modifier::SLOW_BLINK);
                            span_style = span_style.remove_modifier(Modifier::RAPID_BLINK);
                        }
                    },
                    Sgr::Italic(true) => span_style = span_style.add_modifier(Modifier::ITALIC),
                    Sgr::Italic(false) => span_style = span_style.remove_modifier(Modifier::ITALIC),
                    Sgr::Inverse(true) => span_style = span_style.add_modifier(Modifier::REVERSED),
                    Sgr::Inverse(false) => {
                        span_style = span_style.remove_modifier(Modifier::REVERSED)
                    }
                    Sgr::Invisible(true) => span_style = span_style.add_modifier(Modifier::HIDDEN),
                    Sgr::Invisible(false) => {
                        span_style = span_style.remove_modifier(Modifier::HIDDEN)
                    }
                    Sgr::StrikeThrough(true) => {
                        span_style = span_style.add_modifier(Modifier::CROSSED_OUT)
                    }
                    Sgr::StrikeThrough(false) => {
                        span_style = span_style.remove_modifier(Modifier::CROSSED_OUT)
                    }
                    Sgr::Foreground(c) => match c {
                        ColorSpec::Default => span_style = span_style.fg(Color::Reset),
                        ColorSpec::PaletteIndex(i) => span_style = span_style.fg(Color::Indexed(i)),
                        ColorSpec::TrueColor(rgb) => {
                            span_style = span_style.fg(Color::Rgb(rgb.red, rgb.green, rgb.blue))
                        }
                    },
                    Sgr::Background(c) => match c {
                        ColorSpec::Default => span_style = span_style.bg(Color::Reset),
                        ColorSpec::PaletteIndex(i) => span_style = span_style.bg(Color::Indexed(i)),
                        ColorSpec::TrueColor(rgb) => {
                            span_style = span_style.bg(Color::Rgb(rgb.red, rgb.green, rgb.blue))
                        }
                    },
                    _ => {}
                }
            }
            _ => {}
        }
    }

    // push any remaining data
    if !span_text.is_empty() {
        // finish the current span
        current_line.push(Span::styled(span_text, span_style.into()));
        // finish the current line
        spans.push(Spans::from(current_line));
    }

    spans.into()
}