ansi-to-tui-2.0.0/.cargo_vcs_info.json0000644000000001360000000000100132040ustar { "git": { "sha1": "f2b0c70c079f5374f4cb4a2f373994921c81be71" }, "path_in_vcs": "" }ansi-to-tui-2.0.0/.drone.yml000064400000000000000000000002311046102023000137000ustar 00000000000000--- kind: pipeline type: docker name: default steps: - name: Build & Test image: rust:latest commands: - cargo build - cargo test ansi-to-tui-2.0.0/.github/workflows/build.yaml000064400000000000000000000004751046102023000173620ustar 00000000000000name: Build on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose ansi-to-tui-2.0.0/.gitignore000064400000000000000000000000231046102023000137570ustar 00000000000000/target Cargo.lock ansi-to-tui-2.0.0/Cargo.toml0000644000000021530000000000100112030ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "ansi-to-tui" version = "2.0.0" authors = ["Uttarayan Mondal "] description = "A library to convert ansi color coded text into tui::text::Text type from tui-rs library" readme = "README.md" keywords = [ "ansi", "ascii", "tui", "parser", ] license = "MIT" repository = "https://github.com/uttarayan21/ansi-to-tui" [dependencies.nom] version = "7.1.1" [dependencies.simdutf8] version = "0.1.4" optional = true [dependencies.thiserror] version = "1.0.31" [dependencies.tui] version = "0.*" default-features = false [dev-dependencies.anyhow] version = "1.0.58" [features] simd = ["simdutf8"] ansi-to-tui-2.0.0/Cargo.toml.orig000064400000000000000000000011131046102023000146570ustar 00000000000000[package] name = "ansi-to-tui" version = "2.0.0" authors = ["Uttarayan Mondal "] edition = "2018" description = "A library to convert ansi color coded text into tui::text::Text type from tui-rs library" keywords = ["ansi", "ascii", "tui", "parser"] license = "MIT" readme = "README.md" repository = "https://github.com/uttarayan21/ansi-to-tui" [dependencies] nom = "7.1.1" simdutf8 = { version = "0.1.4", optional = true } tui = { version = "0.*", default-features = false } thiserror = "1.0.31" [dev-dependencies] anyhow = "1.0.58" [features] simd = ["simdutf8"] ansi-to-tui-2.0.0/LICENSE000064400000000000000000000020401046102023000127750ustar 00000000000000Copyright 2021 Uttarayan Mondal Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ansi-to-tui-2.0.0/README.md000064400000000000000000000020031046102023000132460ustar 00000000000000# ansi-to-tui [![drone build](https://img.shields.io/drone/build/uttarayan/ansi-to-tui?server=https%3A%2F%2Fdrone.uttarayan.me)][mirror] [![github build](https://github.com/uttarayan21/ansi-to-tui/actions/workflows/build.yaml/badge.svg)][ansi-to-tui] [![downloads](https://img.shields.io/crates/d/ansi-to-tui)](https://crates.io/crates/ansi-to-tui) A nom parser to parse text with ANSI color codes and turn them into [`tui::text::Text`][Text]. | Color | Supported | Examples | | --- | :---: | --- | | 24 bit | ✓ | `\x1b[38;2;;;m` | | 8 bit | ✓ | `\x1b[38;5;m` | | 4 bit | ✓ | `\x1b[30..37;40..47m` | ## Example ```rust use ansi_to_tui::IntoText; let buffer = std::fs::read("ascii/text.ascii").unwrap(); let output = buffer.into_text(); ``` [Text]: https://docs.rs/tui/0.16.0/tui/text/struct.Text.html [ansi-to-tui]: https://github.com/uttarayan21/ansi-to-tui [mirror]: https://git.uttarayan.me/uttarayan/ansi-to-tui ansi-to-tui-2.0.0/ascii/arch.ascii000064400000000000000000000022771046102023000150230ustar 00000000000000    ..         cl.      .llll.    ,oc  :l'  ..      .. ansi-to-tui-2.0.0/ascii/archlinux.ascii000064400000000000000000000250021046102023000160720ustar 00000000000000              ..                             cl                            :ooc                          ;oooo:                        .looooo:                      ;c;:looooc                    :ooooooooooc                  :ooooooooooool                coooool;;loooool.            .looooo'    .oooooo.          .ooooooc      ;oooocl'        'ooooooo:      'ooooo:,       ,oool:,..        ..,:looo;    :c,.                    .,c:  ..                          .' ansi-to-tui-2.0.0/ascii/text.ascii000064400000000000000000000000731046102023000150620ustar 00000000000000AAAABBBB TRUECOLOR ansi-to-tui-2.0.0/src/code.rs000064400000000000000000000076541046102023000140570ustar 00000000000000use tui::style::Color; /// This enum stores most types of ansi escape sequences /// /// You can turn an escape sequence to this enum variant using /// AnsiCode::from(code: u8) /// This doesn't support all of them but does support most of them. #[derive(Debug, PartialEq, Clone)] #[non_exhaustive] pub enum AnsiCode { /// Reset the terminal Reset, /// Set font to bold Bold, /// Set font to faint Faint, /// Set font to italic Italic, /// Set font to underline Underline, /// Set cursor to slowblink SlowBlink, /// Set cursor to rapidblink RapidBlink, /// Invert the colors Reverse, /// Conceal text Conceal, /// Display crossed out text CrossedOut, /// Choose primary font PrimaryFont, /// Choose alternate font AlternateFont, /// Choose alternate fonts 1-9 AlternateFonts(u8), // = 11..19, // from 11 to 19 /// Fraktur ? No clue Fraktur, /// Turn off bold BoldOff, /// Set text to normal Normal, /// Turn off Italic NotItalic, /// Turn off underline UnderlineOff, /// Turn off blinking BlinkOff, // 26 ? /// Don't invert colors InvertOff, /// Reveal text Reveal, /// Turn off Crossedout text CrossedOutOff, /// Set foreground color (4-bit) ForegroundColor(Color), //, 31..37//Issue 60553 https://github.com/rust-lang/rust/issues/60553 /// Set foreground color (8-bit and 24-bit) SetForegroundColor, /// Default foreground color DefaultForegroundColor, /// Set background color (4-bit) BackgroundColor(Color), // 41..47 /// Set background color (8-bit and 24-bit) SetBackgroundColor, /// Default background color DefaultBackgroundColor, // 49 /// Other / non supported escape codes Code(Vec), } impl From for AnsiCode { fn from(code: u8) -> Self { match code { 0 => AnsiCode::Reset, 1 => AnsiCode::Bold, 2 => AnsiCode::Faint, 3 => AnsiCode::Italic, 4 => AnsiCode::Underline, 5 => AnsiCode::SlowBlink, 6 => AnsiCode::RapidBlink, 7 => AnsiCode::Reverse, 8 => AnsiCode::Conceal, 9 => AnsiCode::CrossedOut, 10 => AnsiCode::PrimaryFont, 11 => AnsiCode::AlternateFont, // AnsiCode::// AlternateFont = 11..19, // from 11 to 19 20 => AnsiCode::Fraktur, 21 => AnsiCode::BoldOff, 22 => AnsiCode::Normal, 23 => AnsiCode::NotItalic, 24 => AnsiCode::UnderlineOff, 25 => AnsiCode::BlinkOff, // 26 ? 27 => AnsiCode::InvertOff, 28 => AnsiCode::Reveal, 29 => AnsiCode::CrossedOutOff, 30 => AnsiCode::ForegroundColor(Color::Black), 31 => AnsiCode::ForegroundColor(Color::Red), 32 => AnsiCode::ForegroundColor(Color::Green), 33 => AnsiCode::ForegroundColor(Color::Yellow), 34 => AnsiCode::ForegroundColor(Color::Blue), 35 => AnsiCode::ForegroundColor(Color::Magenta), 36 => AnsiCode::ForegroundColor(Color::Cyan), 37 => AnsiCode::ForegroundColor(Color::Gray), 38 => AnsiCode::SetForegroundColor, 39 => AnsiCode::DefaultForegroundColor, 40 => AnsiCode::BackgroundColor(Color::Black), 41 => AnsiCode::BackgroundColor(Color::Red), 42 => AnsiCode::BackgroundColor(Color::Green), 43 => AnsiCode::BackgroundColor(Color::Yellow), 44 => AnsiCode::BackgroundColor(Color::Blue), 45 => AnsiCode::BackgroundColor(Color::Magenta), 46 => AnsiCode::BackgroundColor(Color::Cyan), 47 => AnsiCode::BackgroundColor(Color::Gray), 48 => AnsiCode::SetBackgroundColor, 49 => AnsiCode::DefaultBackgroundColor, code => AnsiCode::Code(vec![code]), } } } ansi-to-tui-2.0.0/src/error.rs000064400000000000000000000013361046102023000142650ustar 00000000000000/// This enum stores the error types #[derive(Debug, thiserror::Error, PartialEq)] pub enum Error { /// Stack is empty (should never happen) #[error("Nom Error")] NomError(String), /// Error parsing the input as utf-8 #[cfg(feature = "simdutf8")] /// Cannot determine the foreground or background #[error("{0:?}")] Utf8Error(#[from] simdutf8::basic::Utf8Error), #[cfg(not(feature = "simdutf8"))] /// Cannot determine the foreground or background #[error("{0:?}")] Utf8Error(#[from] std::string::FromUtf8Error), } impl From>> for Error { fn from(e: nom::Err>) -> Self { Self::NomError(format!("{:?}", e)) } } ansi-to-tui-2.0.0/src/lib.rs000064400000000000000000000033441046102023000137030ustar 00000000000000#![allow(unused_imports)] #![warn(missing_docs)] //! Parses a `Vec` as an byte sequence with ansi colors to //! [`tui::text::Text`][Text]. //! //! Invalid ansi colors / sequences will be ignored. //! //! //! Supported features //! - UTF-8 using `String::from_utf8` or [`simdutf8`][simdutf8]. //! - Most stuff like **Bold** / *Italic* / Underline / ~~Strikethrough~~. //! - Supports 4-bit color palletes. //! - Supports 8-bit color. //! - Supports True color ( RGB / 24-bit color ). //! //! //! ## Example //! The argument to the function `ansi_to_text` implements `IntoIterator` so it will be consumed on //! use. //! ```rust //! use ansi_to_tui::IntoText; //! let bytes = b"\x1b[38;2;225;192;203mAAAAA\x1b[0m".to_owned().to_vec(); //! let text = bytes.into_text().unwrap(); //! ``` //! Example parsing from a file. //! ```rust //! use ansi_to_tui::IntoText; //! let buffer = std::fs::read("ascii/text.ascii").unwrap(); //! let text = buffer.into_text().unwrap(); //! ``` //! //! If you want to use [`simdutf8`][simdutf8] instead of `String::from_utf8()` //! for parsing UTF-8 then enable optional feature `simd` //! //! [Text]: https://docs.rs/tui/0.15.0/tui/text/struct.Text.html //! [ansi-to-tui]: https://github.com/uttarayan21/ansi-to-tui //! [simdutf8]: https://github.com/rusticstuff/simdutf8 // mod ansi; mod code; mod error; mod parser; pub use error::Error; use tui::text::Text; /// IntoText will convert any type that has a AsRef<[u8]> to a Text. pub trait IntoText { /// Convert the type to a Text. fn into_text(&self) -> Result, Error>; } impl IntoText for T where T: AsRef<[u8]>, { fn into_text(&self) -> Result, Error> { Ok(crate::parser::text(self.as_ref())?.1) } } ansi-to-tui-2.0.0/src/parser.rs000064400000000000000000000161221046102023000144270ustar 00000000000000use crate::code::AnsiCode; use nom::{ branch::alt, bytes::complete::*, character::complete::*, combinator::{map_res, opt, recognize, value}, error, error::FromExternalError, multi::*, sequence::tuple, IResult, Parser, }; use std::str::FromStr; use tui::{ style::{Color, Modifier, Style}, text::{Span, Spans, Text}, }; #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum ColorType { /// Eight Bit color EightBit, /// 24-bit color or true color TrueColor, } #[derive(Debug, Clone, PartialEq)] struct AnsiItem { code: AnsiCode, color: Option, } #[derive(Debug, Clone, PartialEq)] struct AnsiStates { pub items: Vec, pub style: Style, } impl From for tui::style::Style { fn from(states: AnsiStates) -> Self { let mut style = states.style; for item in states.items { match item.code { AnsiCode::Reset => style = Style::default(), AnsiCode::Bold => style = style.add_modifier(Modifier::BOLD), AnsiCode::Faint => style = style.add_modifier(Modifier::DIM), AnsiCode::Italic => style = style.add_modifier(Modifier::ITALIC), AnsiCode::Underline => style = style.add_modifier(Modifier::UNDERLINED), AnsiCode::SlowBlink => style = style.add_modifier(Modifier::SLOW_BLINK), AnsiCode::RapidBlink => style = style.add_modifier(Modifier::RAPID_BLINK), AnsiCode::Reverse => style = style.add_modifier(Modifier::REVERSED), AnsiCode::Conceal => style = style.add_modifier(Modifier::HIDDEN), AnsiCode::CrossedOut => style = style.add_modifier(Modifier::CROSSED_OUT), AnsiCode::DefaultForegroundColor => style = style.fg(Color::Reset), AnsiCode::DefaultBackgroundColor => style = style.bg(Color::Reset), AnsiCode::SetForegroundColor => { if let Some(color) = item.color { style = style.fg(color) } } AnsiCode::SetBackgroundColor => { if let Some(color) = item.color { style = style.fg(color) } } AnsiCode::ForegroundColor(color) => style = style.fg(color), AnsiCode::BackgroundColor(color) => style = style.bg(color), AnsiCode::AlternateFonts(_) => (), _ => (), } } style } } pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> { let mut line_spans = Vec::new(); let mut last = Default::default(); while let Ok((_s, (spans, style))) = spans(last)(s) { line_spans.push(spans); last = style; s = _s; if s.is_empty() { break; } } Ok((s, Text::from(line_spans))) } fn spans(style: Style) -> impl Fn(&[u8]) -> IResult<&[u8], (Spans<'static>, Style)> { // let style_: Style = Default::default(); move |s: &[u8]| -> IResult<&[u8], (Spans<'static>, Style)> { let (s, mut text) = take_while(|c| c != b'\n')(s)?; let (s, _) = opt(tag("\n"))(s)?; let mut spans = Vec::new(); let mut last = style; loop { let (s, span) = span(last)(text)?; last = span.style; spans.push(span); text = s; if text.is_empty() { break; } } Ok((s, (Spans(spans), last))) } } // fn span(s: &[u8]) -> IResult<&[u8], tui::text::Span> { fn span(last: Style) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'static>, nom::error::Error<&[u8]>> { move |s: &[u8]| -> IResult<&[u8], Span<'static>> { let mut last = last; let (s, style) = opt(style(last))(s)?; #[cfg(feature = "simd")] let (s, text) = map_res(take_while(|c| c != b'\x1b' | b'\n'), |t| { simdutf8::basic::from_utf8(t) })(s)?; #[cfg(not(feature = "simd"))] let (s, text) = map_res(take_while(|c| c != b'\x1b' | b'\n'), |t| { std::str::from_utf8(t) })(s)?; if let Some(style) = style { if style == Default::default() { last = Default::default(); } else { last = last.patch(style); } } Ok((s, Span::styled(text.to_owned(), last))) } } fn style(style: Style) -> impl Fn(&[u8]) -> IResult<&[u8], Style, nom::error::Error<&[u8]>> { move |s: &[u8]| -> IResult<&[u8], Style> { let (s, _) = tag("\x1b[")(s)?; let (s, r) = separated_list0(tag(";"), ansi_item)(s)?; // the ones ending with m are styles and the ones ending with h are screen mode escapes let (s, _) = alt((char('m'), alt((char('h'), char('l')))))(s)?; Ok((s, Style::from(AnsiStates { style, items: r }))) } } /// An ansi item is a code with a possible color. fn ansi_item(s: &[u8]) -> IResult<&[u8], AnsiItem> { // Screen escape modes start with '?' or '=' or non-number let (s, nc) = opt(alt((char('?'), char('='))))(s)?; let (mut s, c) = i64(s)?; if let Some(nc) = nc { return Ok(( s, AnsiItem { code: AnsiCode::Code(vec![nc as u8]), color: None, }, )); } let code = AnsiCode::from(c as u8); let color = if matches!( code, AnsiCode::SetBackgroundColor | AnsiCode::SetForegroundColor ) { let (_s, _) = opt(tag(";"))(s)?; let (_s, color) = color(_s)?; s = _s; Some(color) } else { None }; Ok((s, AnsiItem { code, color })) } fn color(s: &[u8]) -> IResult<&[u8], Color> { let (s, c_type) = color_type(s)?; let (s, _) = opt(tag(";"))(s)?; match c_type { ColorType::TrueColor => { let (s, (r, _, g, _, b)) = tuple((u8, tag(";"), u8, tag(";"), u8))(s)?; Ok((s, Color::Rgb(r, g, b))) } ColorType::EightBit => { let (s, index) = u8(s)?; Ok((s, Color::Indexed(index))) } } } fn color_type(s: &[u8]) -> IResult<&[u8], ColorType> { let (s, t) = i64(s)?; // NOTE: This isn't opt because a color type must always be followed by a color // let (s, _) = opt(tag(";"))(s)?; let (s, _) = tag(";")(s)?; match t { 2 => Ok((s, ColorType::TrueColor)), 5 => Ok((s, ColorType::EightBit)), _ => Err(nom::Err::Error(nom::error::Error::new( s, nom::error::ErrorKind::Alt, ))), } } #[test] fn color_test() { let c = color(b"2;255;255;255").unwrap(); assert_eq!(c.1, Color::Rgb(255, 255, 255)); let c = color(b"5;255").unwrap(); assert_eq!(c.1, Color::Indexed(255)); } #[test] fn ansi_items_test() { let sc = Default::default(); let t = style(sc)(b"\x1b[38;2;3;3;3m").unwrap(); assert_eq!( t.1, Style::from(AnsiStates { style: sc, items: vec![AnsiItem { code: AnsiCode::SetForegroundColor, color: Some(Color::Rgb(3, 3, 3)) }] }) ); } ansi-to-tui-2.0.0/tests/tests.rs000064400000000000000000000104071046102023000146500ustar 00000000000000// use ansi_to_tui::{ansi_to_text, ansi_to_text_override_style}; use ansi_to_tui::IntoText; use tui::{ style::{Color, Style}, text::{Span, Spans, Text}, }; // #[test] // fn test_anyhow() -> anyhow::Result<()> { // let text = ansi_to_text("foo".bytes())?; // println!("{:#?}", text); // Ok(()) // } // #[test] // #[ignore] // fn test_bytes() { // let bytes: Vec = vec![27_u8, 91, 51, 49, 109, 65, 65, 65]; // println!("{:#?}", ansi_to_text(bytes)) // } // #[test] // fn test_generic() { // let string = "\x1b[33mYellow\x1b[31mRed\x1b[32mGreen\x1b[0m"; // println!("{:?}\n\n", string); // // ansi_to_text(string.bytes()).unwrap(); // println!("{:#?}", ansi_to_text(string.bytes())); // } #[test] fn test_string() { use ansi_to_tui::IntoText; let string: Vec = "FOO".to_string().bytes().collect(); println!("{:?}", string.into_text().unwrap()); } #[test] fn test_unicode() { // these are 8 byte unicode charachters // first 4 bytes are for the unicode and the last 4 bytes are for the color / variant let bytes = "AAA🅱️🅱️🅱️".as_bytes().to_vec(); let output = some_text("AAA🅱️🅱️🅱️"); assert_eq!(bytes.into_text(), output); } #[test] fn test_ascii_rgb() { let bytes: Vec = b"\x1b[38;2;100;100;100mAAABBB".to_vec(); let output = Ok(Text::from(Spans::from(Span::styled( "AAABBB", Style { fg: Some(Color::Rgb(100, 100, 100)), ..Default::default() }, )))); assert_eq!(bytes.into_text(), output); } // #[test] // fn test_ascii_multi() { // let bytes = "\x1b[31m\x1b[4m\x1b[1mHELLO".as_bytes().to_vec(); // println!("{:#?}", ansi_to_text(bytes)); // } #[test] fn test_ascii_newlines() { let bytes = "LINE_1\n\n\n\n\n\n\nLINE_8".as_bytes().to_vec(); let output = Ok(Text::from(vec![ Spans::from(Span::raw("LINE_1")), Spans::from(Span::raw("")), Spans::from(Span::raw("")), Spans::from(Span::raw("")), Spans::from(Span::raw("")), Spans::from(Span::raw("")), Spans::from(Span::raw("")), Spans::from(Span::raw("LINE_8")), ])); // println!("{:#?}", bytes.into_text()); assert_eq!(bytes.into_text(), output); } // #[test] // #[ignore = "Gives a lot of output"] // fn test_arch_ascii() { // use crate::ansi_to_text; // use std::{fs::File, io::Read}; // let mut ascii = File::open("ascii/arch.ascii").unwrap(); // let mut buffer: Vec = Vec::new(); // ascii.read_to_end(&mut buffer).unwrap(); // let text = ansi_to_text(buffer).unwrap(); // println!("{:#?}", text); // } // #[test] // #[ignore = "Gives a lot of output"] // fn test_archlinux_ascii() { // use crate::ansi_to_text; // use std::{fs::File, io::Read}; // let mut ascii = File::open("ascii/archlinux.ascii").unwrap(); // let mut buffer: Vec = Vec::new(); // ascii.read_to_end(&mut buffer).unwrap(); // let text = ansi_to_text(buffer).unwrap(); // println!("{:#?}", text); // } // #[test] // #[ignore] // fn test_command() { // use std::process::Command; // let c = Command::new("ls") // .args(&["--color=always", "/"]) // .output() // .unwrap(); // let text = ansi_to_text(c.stdout); // println!("{:?}", text); // } #[test] fn test_reset() { let string = "\x1b[33mA\x1b[0mB"; let output = Ok(Text { lines: vec![Spans(vec![ Span::styled( "A", Style { fg: Some(Color::Yellow), ..Default::default() }, ), Span::styled( "B", Style { ..Default::default() }, ), ])], }); assert_eq!(string.into_text(), output); } #[test] fn test_screen_modes() { let bytes: Vec = b"\x1b[?25hAAABBB".to_vec(); let output = Ok(Text::from(Spans::from(Span::styled( "AAABBB", // or "AAABBB" Style::default(), )))); assert_eq!(bytes.into_text(), output); } fn some_text(s: &'static str) -> Result, ansi_to_tui::Error> { Ok(Text { lines: vec![Spans(vec![Span { content: s.into(), style: Default::default(), }])], }) }