ansi_to_tui/lib.rs
1#![allow(unused_imports)]
2#![warn(missing_docs)]
3
4//! Convert ANSI color and style codes into Ratatui [`Text`][Text].
5//!
6//! This crate parses bytes containing ANSI SGR escape sequences (like `\x1b[31m`).
7//! It produces a Ratatui [`Text`][Text] with equivalent foreground/background [`Color`][Color] and
8//! [`Modifier`][Modifier] settings via [`Style`][Style].
9//!
10//! Unknown or malformed escape sequences are ignored, so you can feed it real terminal output
11//! without having to pre-clean it.
12//!
13//! # Features
14//!
15//! - UTF-8 decoding via `String::from_utf8` (default) or [`simdutf8`][simdutf8] (`simd` feature).
16//! - SGR styles such as bold, italic, underline, and strikethrough.
17//! - Colors: named (3/4-bit, 8/16-color), indexed (8-bit, 256-color), and truecolor (24-bit RGB).
18//! - Optional `zero-copy` API that borrows from the input.
19//!
20//! # Supported Color Codes
21//!
22//! | Color Mode | Supported | SGR Example | Ratatui `Color` Example |
23//! | --------------------------- | :-------: | ------------------------ | ----------------------- |
24//! | Named (3/4-bit, 8/16-color) | ✓ | `\x1b[30..37;40..47m` | `Color::Blue` |
25//! | Indexed (8-bit, 256-color) | ✓ | `\x1b[38;5;<N>m` | `Color::Indexed(1)` |
26//! | Truecolor (24-bit RGB) | ✓ | `\x1b[38;2;<R>;<G>;<B>m` | `Color::Rgb(255, 0, 0)` |
27//!
28//! The SGR examples above set the foreground color (`38`). For background colors, replace `38`
29//! with `48` (for example, `\x1b[48;5;<N>m` and `\x1b[48;2;<R>;<G>;<B>m`).
30//!
31//! # Example
32//!
33//! The input type implements `AsRef<[u8]>`, so it is not consumed.
34//!
35//! ```rust
36//! # fn doctest() -> eyre::Result<()> {
37//! use ansi_to_tui::IntoText as _;
38//! let bytes = b"\x1b[38;2;225;192;203mAAAAA\x1b[0m".to_vec();
39//! let text = bytes.into_text()?;
40//! # Ok(()) }
41//! ```
42//!
43//! Parsing from a file.
44//!
45//! ```rust
46//! # fn doctest() -> eyre::Result<()> {
47//! use ansi_to_tui::IntoText as _;
48//! let buffer = std::fs::read("ascii/text.ascii")?;
49//! let text = buffer.into_text()?;
50//! # Ok(()) }
51//! ```
52//!
53//! [Text]: https://docs.rs/ratatui-core/latest/ratatui_core/text/struct.Text.html
54//! [Color]: https://docs.rs/ratatui-core/latest/ratatui_core/style/enum.Color.html
55//! [Style]: https://docs.rs/ratatui-core/latest/ratatui_core/style/struct.Style.html
56//! [Modifier]: https://docs.rs/ratatui-core/latest/ratatui_core/style/struct.Modifier.html
57//! [simdutf8]: https://github.com/rusticstuff/simdutf8
58
59pub use error::Error;
60use ratatui_core::text::Text;
61
62mod code;
63mod error;
64mod parser;
65#[cfg(test)]
66mod tests;
67
68/// Parse ANSI SGR styled bytes into a Ratatui [`Text`].
69///
70/// This trait is implemented for all `T: AsRef<[u8]>`, so most byte containers can call
71/// [`IntoText::into_text`]. With the `zero-copy` feature enabled, you can also call
72/// [`IntoText::to_text`].
73///
74/// For example, `String`, `&str`, `Vec<u8>`, and `&[u8]` all implement `AsRef<[u8]>`.
75///
76/// You may also implement this trait for your own types if you want custom conversions.
77///
78/// # Example
79///
80/// ```rust
81/// use ansi_to_tui::IntoText as _;
82///
83/// let s: &str = "\x1b[34mblue\x1b[0m";
84/// let _text = s.into_text()?;
85///
86/// let owned: String = "\x1b[31mred\x1b[0m".to_owned();
87/// let _text = owned.into_text()?;
88/// # Ok::<(), ansi_to_tui::Error>(())
89/// ```
90pub trait IntoText {
91 /// Convert the type to an owned `Text`.
92 ///
93 /// This always returns a `Text<'static>`, so it allocates owned strings for the parsed spans.
94 #[allow(clippy::wrong_self_convention)]
95 fn into_text(&self) -> Result<Text<'static>, Error>;
96
97 /// Convert the type to a borrowed `Text` while trying to copy as little as possible.
98 ///
99 /// This method borrows the span contents from the input instead of allocating new strings,
100 /// so the returned `Text` is only valid as long as the input is alive.
101 ///
102 /// Use this when you only need the parsed `Text` temporarily (for example, render it
103 /// immediately). If you need to store the result beyond the lifetime of the input, use
104 /// [`IntoText::into_text`] instead.
105 ///
106 /// # Example
107 ///
108 /// ```rust
109 /// # #[cfg(feature = "zero-copy")]
110 /// # {
111 /// use ansi_to_tui::IntoText as _;
112 ///
113 /// let bytes = b"\x1b[32mgreen\x1b[0m";
114 /// let _text = bytes.to_text()?;
115 /// # }
116 /// # Ok::<(), ansi_to_tui::Error>(())
117 /// ```
118 #[cfg(feature = "zero-copy")]
119 fn to_text(&self) -> Result<Text<'_>, Error>;
120}
121
122/// Blanket implementation for all `AsRef<[u8]>` types.
123impl<T> IntoText for T
124where
125 T: AsRef<[u8]>,
126{
127 fn into_text(&self) -> Result<Text<'static>, Error> {
128 Ok(crate::parser::text(self.as_ref())?.1)
129 }
130
131 #[cfg(feature = "zero-copy")]
132 fn to_text(&self) -> Result<Text<'_>, Error> {
133 Ok(crate::parser::text_fast(self.as_ref())?.1)
134 }
135}