mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 12:05:34 +00:00
fix(previewers): handle crlf sequences when parsing ansi into ratatui objects (#119)
This commit is contained in:
parent
937d0f0758
commit
ea752b13e6
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -56,19 +56,6 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "ansi-to-tui"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"ratatui",
|
||||
"simdutf8",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_colours"
|
||||
version = "1.2.3"
|
||||
@ -3000,7 +2987,6 @@ dependencies = [
|
||||
name = "television"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"better-panic",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
@ -3075,11 +3061,16 @@ dependencies = [
|
||||
"color-eyre",
|
||||
"devicons",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"parking_lot",
|
||||
"ratatui",
|
||||
"regex",
|
||||
"simdutf8",
|
||||
"smallvec",
|
||||
"syntect",
|
||||
"television-channels",
|
||||
"television-utils",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@ -3088,7 +3079,6 @@ dependencies = [
|
||||
name = "television-screen"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"color-eyre",
|
||||
"ratatui",
|
||||
"serde",
|
||||
|
@ -81,7 +81,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
|
||||
unicode-width = "0.2.0"
|
||||
human-panic = "2.0.2"
|
||||
copypasta = "0.10.1"
|
||||
ansi-to-tui = "7.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5.1"
|
||||
|
@ -23,4 +23,14 @@ devicons = "0.6.11"
|
||||
color-eyre = "0.6.3"
|
||||
regex = "1.11.1"
|
||||
lazy_static = "1.5.0"
|
||||
nom = "7.1"
|
||||
tui = { version = "0.29", default-features = false, package = "ratatui" }
|
||||
thiserror = "1.0"
|
||||
simdutf8 = { version = "0.1", optional = true }
|
||||
smallvec = { version = "1.10.0", features = ["const_generics"] }
|
||||
|
||||
[features]
|
||||
simd = ["dep:simdutf8"]
|
||||
zero-copy = []
|
||||
default = ["zero-copy", "simd"]
|
||||
|
||||
|
34
crates/television-previewers/src/ansi.rs
Normal file
34
crates/television-previewers/src/ansi.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![allow(unused_imports)]
|
||||
//! This module provides a way to parse ansi escape codes and convert them to ratatui objects.
|
||||
//!
|
||||
//! This code is a modified version of [ansi_to_tui](https://github.com/ratatui/ansi-to-tui).
|
||||
|
||||
// mod ansi;
|
||||
pub mod code;
|
||||
pub mod error;
|
||||
pub 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.
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn into_text(&self) -> Result<Text<'static>, Error>;
|
||||
/// Convert the type to a Text while trying to copy as less as possible
|
||||
#[cfg(feature = "zero-copy")]
|
||||
fn to_text(&self) -> Result<Text<'_>, Error>;
|
||||
}
|
||||
impl<T> IntoText for T
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn into_text(&self) -> Result<Text<'static>, Error> {
|
||||
Ok(crate::ansi::parser::text(self.as_ref())?.1)
|
||||
}
|
||||
|
||||
#[cfg(feature = "zero-copy")]
|
||||
fn to_text(&self) -> Result<Text<'_>, Error> {
|
||||
Ok(crate::ansi::parser::text_fast(self.as_ref())?.1)
|
||||
}
|
||||
}
|
7
crates/television-previewers/src/ansi/LICENSE
Normal file
7
crates/television-previewers/src/ansi/LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 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.
|
139
crates/television-previewers/src/ansi/code.rs
Normal file
139
crates/television-previewers/src/ansi/code.rs
Normal file
@ -0,0 +1,139 @@
|
||||
use 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
|
||||
#[allow(dead_code)]
|
||||
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<u8>),
|
||||
}
|
||||
|
||||
impl From<u8> 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,
|
||||
90 => AnsiCode::ForegroundColor(Color::DarkGray),
|
||||
91 => AnsiCode::ForegroundColor(Color::LightRed),
|
||||
92 => AnsiCode::ForegroundColor(Color::LightGreen),
|
||||
93 => AnsiCode::ForegroundColor(Color::LightYellow),
|
||||
94 => AnsiCode::ForegroundColor(Color::LightBlue),
|
||||
95 => AnsiCode::ForegroundColor(Color::LightMagenta),
|
||||
96 => AnsiCode::ForegroundColor(Color::LightCyan),
|
||||
97 => AnsiCode::ForegroundColor(Color::White),
|
||||
100 => AnsiCode::BackgroundColor(Color::DarkGray),
|
||||
101 => AnsiCode::BackgroundColor(Color::LightRed),
|
||||
102 => AnsiCode::BackgroundColor(Color::LightGreen),
|
||||
103 => AnsiCode::BackgroundColor(Color::LightYellow),
|
||||
104 => AnsiCode::BackgroundColor(Color::LightBlue),
|
||||
105 => AnsiCode::BackgroundColor(Color::LightMagenta),
|
||||
106 => AnsiCode::BackgroundColor(Color::LightCyan),
|
||||
107 => AnsiCode::ForegroundColor(Color::White),
|
||||
code => AnsiCode::Code(vec![code]),
|
||||
}
|
||||
}
|
||||
}
|
24
crates/television-previewers/src/ansi/error.rs
Normal file
24
crates/television-previewers/src/ansi/error.rs
Normal file
@ -0,0 +1,24 @@
|
||||
/// This enum stores the error types
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Stack is empty (should never happen)
|
||||
#[error("Internal error: stack is empty")]
|
||||
NomError(String),
|
||||
|
||||
/// Error parsing the input as utf-8
|
||||
#[cfg(feature = "simd")]
|
||||
/// Cannot determine the foreground or background
|
||||
#[error("{0:?}")]
|
||||
Utf8Error(#[from] simdutf8::basic::Utf8Error),
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
/// Cannot determine the foreground or background
|
||||
#[error("{0:?}")]
|
||||
Utf8Error(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl From<nom::Err<nom::error::Error<&[u8]>>> for Error {
|
||||
fn from(e: nom::Err<nom::error::Error<&[u8]>>) -> Self {
|
||||
Self::NomError(format!("{:?}", e))
|
||||
}
|
||||
}
|
401
crates/television-previewers/src/ansi/parser.rs
Normal file
401
crates/television-previewers/src/ansi/parser.rs
Normal file
@ -0,0 +1,401 @@
|
||||
use crate::ansi::code::AnsiCode;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::*,
|
||||
character::{complete::*, is_alphabetic},
|
||||
combinator::{map_res, opt, recognize, value},
|
||||
error::{self, FromExternalError},
|
||||
multi::*,
|
||||
sequence::{delimited, preceded, terminated, tuple},
|
||||
IResult, Parser,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use tui::{
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, 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<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct AnsiStates {
|
||||
pub items: smallvec::SmallVec<[AnsiItem; 2]>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl From<AnsiStates> for tui::style::Style {
|
||||
fn from(states: AnsiStates) -> Self {
|
||||
let mut style = states.style;
|
||||
for item in states.items {
|
||||
match item.code {
|
||||
AnsiCode::Bold => style = style.add_modifier(Modifier::BOLD),
|
||||
AnsiCode::Faint => style = style.add_modifier(Modifier::DIM),
|
||||
AnsiCode::Normal => {
|
||||
style = style
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_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::SetForegroundColor => {
|
||||
if let Some(color) = item.color {
|
||||
style = style.fg(color)
|
||||
}
|
||||
}
|
||||
AnsiCode::ForegroundColor(color) => style = style.fg(color),
|
||||
AnsiCode::BackgroundColor(color) => style = style.bg(color),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> {
|
||||
let mut lines = Vec::new();
|
||||
let mut last = Style::new();
|
||||
while let Ok((_s, (line, style))) = line(last)(s) {
|
||||
lines.push(line);
|
||||
last = style;
|
||||
s = _s;
|
||||
if s.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok((s, Text::from(lines)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "zero-copy")]
|
||||
pub(crate) fn text_fast(mut s: &[u8]) -> IResult<&[u8], Text<'_>> {
|
||||
let mut lines = Vec::new();
|
||||
let mut last = Style::new();
|
||||
while let Ok((_s, (line, style))) = line_fast(last)(s) {
|
||||
lines.push(line);
|
||||
last = style;
|
||||
s = _s;
|
||||
if s.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok((s, Text::from(lines)))
|
||||
}
|
||||
|
||||
fn line(
|
||||
style: Style,
|
||||
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'static>, Style)> {
|
||||
// let style_: Style = Default::default();
|
||||
move |s: &[u8]| -> IResult<&[u8], (Line<'static>, Style)> {
|
||||
let (s, mut text) = not_line_ending(s)?;
|
||||
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
|
||||
let mut spans = Vec::new();
|
||||
let mut last = style;
|
||||
while let Ok((s, span)) = span(last)(text) {
|
||||
// Since reset now tracks seperately we can skip the reset check
|
||||
last = last.patch(span.style);
|
||||
|
||||
if !span.content.is_empty() {
|
||||
spans.push(span);
|
||||
}
|
||||
text = s;
|
||||
if text.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((s, (Line::from(spans), last)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "zero-copy")]
|
||||
fn line_fast(
|
||||
style: Style,
|
||||
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'_>, Style)> {
|
||||
// let style_: Style = Default::default();
|
||||
move |s: &[u8]| -> IResult<&[u8], (Line<'_>, Style)> {
|
||||
let (s, mut text) = not_line_ending(s)?;
|
||||
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
|
||||
let mut spans = Vec::new();
|
||||
let mut last = style;
|
||||
while let Ok((s, span)) = span_fast(last)(text) {
|
||||
last = last.patch(span.style);
|
||||
// If the spans is empty then it might be possible that the style changes
|
||||
// but there is no text change
|
||||
if !span.content.is_empty() {
|
||||
spans.push(span);
|
||||
}
|
||||
text = s;
|
||||
if text.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((s, (Line::from(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'), |t| {
|
||||
simdutf8::basic::from_utf8(t)
|
||||
})(s)?;
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
let (s, text) =
|
||||
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
|
||||
s,
|
||||
)?;
|
||||
|
||||
if let Some(style) = style.flatten() {
|
||||
last = last.patch(style);
|
||||
}
|
||||
|
||||
Ok((s, Span::styled(text.to_owned(), last)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "zero-copy")]
|
||||
fn span_fast(
|
||||
last: Style,
|
||||
) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'_>, nom::error::Error<&[u8]>> {
|
||||
move |s: &[u8]| -> IResult<&[u8], Span<'_>> {
|
||||
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'), |t| {
|
||||
simdutf8::basic::from_utf8(t)
|
||||
})(s)?;
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
let (s, text) =
|
||||
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
|
||||
s,
|
||||
)?;
|
||||
|
||||
if let Some(style) = style.flatten() {
|
||||
last = last.patch(style);
|
||||
}
|
||||
|
||||
Ok((s, Span::styled(text, last)))
|
||||
}
|
||||
}
|
||||
|
||||
fn style(
|
||||
style: Style,
|
||||
) -> impl Fn(&[u8]) -> IResult<&[u8], Option<Style>, nom::error::Error<&[u8]>>
|
||||
{
|
||||
move |s: &[u8]| -> IResult<&[u8], Option<Style>> {
|
||||
let (s, r) = match opt(ansi_sgr_code)(s)? {
|
||||
(s, Some(r)) => (s, Some(r)),
|
||||
(s, None) => {
|
||||
let (s, _) = any_escape_sequence(s)?;
|
||||
(s, None)
|
||||
}
|
||||
};
|
||||
Ok((s, r.map(|r| Style::from(AnsiStates { style, items: r }))))
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete ANSI SGR code
|
||||
fn ansi_sgr_code(
|
||||
s: &[u8],
|
||||
) -> IResult<&[u8], smallvec::SmallVec<[AnsiItem; 2]>, nom::error::Error<&[u8]>>
|
||||
{
|
||||
delimited(
|
||||
tag("\x1b["),
|
||||
fold_many0(
|
||||
ansi_sgr_item,
|
||||
smallvec::SmallVec::new,
|
||||
|mut items, item| {
|
||||
items.push(item);
|
||||
items
|
||||
},
|
||||
),
|
||||
char('m'),
|
||||
)(s)
|
||||
}
|
||||
|
||||
fn any_escape_sequence(s: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
|
||||
// Attempt to consume most escape codes, including a single escape char.
|
||||
//
|
||||
// Most escape codes begin with ESC[ and are terminated by an alphabetic character,
|
||||
// but OSC codes begin with ESC] and are terminated by an ascii bell (\x07)
|
||||
// and a truncated/invalid code may just be a standalone ESC or not be terminated.
|
||||
//
|
||||
// We should try to consume as much of it as possible to match behavior of most terminals;
|
||||
// where we fail at that we should at least consume the escape char to avoid infinitely looping
|
||||
|
||||
let (input, garbage) = preceded(
|
||||
char('\x1b'),
|
||||
opt(alt((
|
||||
delimited(char('['), take_till(is_alphabetic), opt(take(1u8))),
|
||||
delimited(char(']'), take_till(|c| c == b'\x07'), opt(take(1u8))),
|
||||
))),
|
||||
)(s)?;
|
||||
Ok((input, garbage))
|
||||
}
|
||||
|
||||
/// An ANSI SGR attribute
|
||||
fn ansi_sgr_item(s: &[u8]) -> IResult<&[u8], AnsiItem> {
|
||||
let (s, c) = u8(s)?;
|
||||
let code = AnsiCode::from(c);
|
||||
let (s, color) = match code {
|
||||
AnsiCode::SetForegroundColor | AnsiCode::SetBackgroundColor => {
|
||||
let (s, _) = opt(tag(";"))(s)?;
|
||||
let (s, color) = color(s)?;
|
||||
(s, Some(color))
|
||||
}
|
||||
_ => (s, None),
|
||||
};
|
||||
let (s, _) = opt(tag(";"))(s)?;
|
||||
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));
|
||||
let err = color(b"10;255");
|
||||
assert_ne!(err, Ok(c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_items_test() {
|
||||
let sc = Default::default();
|
||||
let t = style(sc)(b"\x1b[38;2;3;3;3m").unwrap().1.unwrap();
|
||||
assert_eq!(
|
||||
t,
|
||||
Style::from(AnsiStates {
|
||||
style: sc,
|
||||
items: vec![AnsiItem {
|
||||
code: AnsiCode::SetForegroundColor,
|
||||
color: Some(Color::Rgb(3, 3, 3))
|
||||
}]
|
||||
.into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style(sc)(b"\x1b[38;5;3m").unwrap().1.unwrap(),
|
||||
Style::from(AnsiStates {
|
||||
style: sc,
|
||||
items: vec![AnsiItem {
|
||||
code: AnsiCode::SetForegroundColor,
|
||||
color: Some(Color::Indexed(3))
|
||||
}]
|
||||
.into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style(sc)(b"\x1b[38;5;3;48;5;3m").unwrap().1.unwrap(),
|
||||
Style::from(AnsiStates {
|
||||
style: sc,
|
||||
items: vec![
|
||||
AnsiItem {
|
||||
code: AnsiCode::SetForegroundColor,
|
||||
color: Some(Color::Indexed(3))
|
||||
},
|
||||
AnsiItem {
|
||||
code: AnsiCode::SetBackgroundColor,
|
||||
color: Some(Color::Indexed(3))
|
||||
}
|
||||
]
|
||||
.into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style(sc)(b"\x1b[38;5;3;48;5;3;1m").unwrap().1.unwrap(),
|
||||
Style::from(AnsiStates {
|
||||
style: sc,
|
||||
items: vec![
|
||||
AnsiItem {
|
||||
code: AnsiCode::SetForegroundColor,
|
||||
color: Some(Color::Indexed(3))
|
||||
},
|
||||
AnsiItem {
|
||||
code: AnsiCode::SetBackgroundColor,
|
||||
color: Some(Color::Indexed(3))
|
||||
},
|
||||
AnsiItem {
|
||||
code: AnsiCode::Bold,
|
||||
color: None
|
||||
}
|
||||
]
|
||||
.into()
|
||||
})
|
||||
);
|
||||
}
|
@ -1 +1,2 @@
|
||||
pub mod ansi;
|
||||
pub mod previewers;
|
||||
|
@ -97,6 +97,8 @@ impl CommandPreviewer {
|
||||
&last_previewed,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
debug!("Too many concurrent preview tasks running");
|
||||
}
|
||||
|
||||
self.last_previewed.lock().clone()
|
||||
|
@ -20,5 +20,4 @@ television-utils = { path = "../television-utils", version = "0.0.10" }
|
||||
television-channels = { path = "../television-channels", version = "0.0.10" }
|
||||
television-previewers = { path = "../television-previewers", version = "0.0.10" }
|
||||
color-eyre = "0.6.3"
|
||||
ansi-to-tui = "7.0.0"
|
||||
syntect = "5.2.0"
|
||||
|
@ -1,16 +1,22 @@
|
||||
use crate::cache::RenderedPreviewCache;
|
||||
use crate::colors::{Colorscheme, PreviewColorscheme};
|
||||
use ansi_to_tui::IntoText;
|
||||
use crate::{
|
||||
cache::RenderedPreviewCache,
|
||||
colors::{Colorscheme, PreviewColorscheme},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
prelude::{Color, Line, Modifier, Span, Style, Stylize, Text},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use television_channels::entry::Entry;
|
||||
use television_previewers::previewers::{
|
||||
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
||||
use television_previewers::{
|
||||
ansi::IntoText,
|
||||
previewers::{
|
||||
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
||||
},
|
||||
};
|
||||
use television_utils::strings::{
|
||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||
|
Loading…
x
Reference in New Issue
Block a user