Skip to content

Commit 60aad68

Browse files
committed
Improve handling of ANSI passthrough
1 parent d89fa3e commit 60aad68

3 files changed

Lines changed: 221 additions & 35 deletions

File tree

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub(crate) mod printer;
4242
pub mod style;
4343
pub(crate) mod syntax_mapping;
4444
mod terminal;
45+
mod vscreen;
4546
pub(crate) mod wrapping;
4647

4748
pub use pretty_printer::{Input, PrettyPrinter};

src/printer.rs

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::input::OpenedInput;
3030
use crate::line_range::RangeCheckResult;
3131
use crate::preprocessor::{expand_tabs, replace_nonprintable};
3232
use crate::terminal::{as_terminal_escaped, to_ansi_color};
33+
use crate::vscreen::AnsiStyle;
3334
use crate::wrapping::WrappingMode;
3435

3536
pub(crate) trait Printer {
@@ -104,7 +105,7 @@ pub(crate) struct InteractivePrinter<'a> {
104105
config: &'a Config<'a>,
105106
decorations: Vec<Box<dyn Decoration>>,
106107
panel_width: usize,
107-
ansi_prefix_sgr: String,
108+
ansi_style: AnsiStyle,
108109
content_type: Option<ContentType>,
109110
#[cfg(feature = "git")]
110111
pub line_changes: &'a Option<LineChanges>,
@@ -188,7 +189,7 @@ impl<'a> InteractivePrinter<'a> {
188189
config,
189190
decorations,
190191
content_type: input.reader.content_type,
191-
ansi_prefix_sgr: String::new(),
192+
ansi_style: AnsiStyle::new(),
192193
#[cfg(feature = "git")]
193194
line_changes,
194195
highlighter,
@@ -466,31 +467,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
466467
} else {
467468
for &(style, region) in regions.iter() {
468469
let ansi_iterator = AnsiCodeIterator::new(region);
469-
let mut ansi_prefix: String = String::new();
470470
for chunk in ansi_iterator {
471471
match chunk {
472472
// ANSI escape passthrough.
473-
(text, true) => {
474-
let is_ansi_csi = text.starts_with("\x1B[");
475-
476-
if is_ansi_csi && text.ends_with('m') {
477-
// It's an ANSI SGR sequence.
478-
// We should be mostly safe to just append these together.
479-
ansi_prefix.push_str(text);
480-
if text == "\x1B[0m" {
481-
self.ansi_prefix_sgr = "\x1B[0m".to_owned();
482-
} else {
483-
self.ansi_prefix_sgr.push_str(text);
484-
}
485-
} else if is_ansi_csi {
486-
// It's a regular CSI sequence.
487-
// We should be mostly safe to just append these together.
488-
ansi_prefix.push_str(text);
489-
} else {
490-
// It's probably a VT100 code.
491-
// Passing it through is the safest bet.
492-
write!(handle, "{}", text)?;
493-
}
473+
(ansi, true) => {
474+
self.ansi_style.update(ansi);
475+
write!(handle, "{}", ansi)?;
494476
}
495477

496478
// Regular text.
@@ -540,10 +522,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
540522
"{}\n{}",
541523
as_terminal_escaped(
542524
style,
543-
&*format!(
544-
"{}{}{}",
545-
self.ansi_prefix_sgr, ansi_prefix, line_buf
546-
),
525+
&*format!("{}{}", self.ansi_style, line_buf),
547526
self.config.true_color,
548527
self.config.colored_output,
549528
self.config.use_italic_text,
@@ -569,19 +548,13 @@ impl<'a> Printer for InteractivePrinter<'a> {
569548
"{}",
570549
as_terminal_escaped(
571550
style,
572-
&*format!(
573-
"{}{}{}",
574-
self.ansi_prefix_sgr, ansi_prefix, line_buf
575-
),
551+
&*format!("{}{}", self.ansi_style, line_buf),
576552
self.config.true_color,
577553
self.config.colored_output,
578554
self.config.use_italic_text,
579555
background_color
580556
)
581557
)?;
582-
583-
// Clear the ANSI prefix buffer.
584-
ansi_prefix.clear();
585558
}
586559
}
587560
}

src/vscreen.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use std::fmt::{Display, Formatter};
2+
3+
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
4+
pub struct AnsiStyle {
5+
attributes: Option<Attributes>,
6+
}
7+
8+
impl AnsiStyle {
9+
pub fn new() -> Self {
10+
AnsiStyle { attributes: None }
11+
}
12+
13+
pub fn update(&mut self, sequence: &str) -> bool {
14+
match &mut self.attributes {
15+
Some(a) => a.update(sequence),
16+
None => {
17+
self.attributes = Some(Attributes::new());
18+
self.attributes.as_mut().unwrap().update(sequence)
19+
}
20+
}
21+
}
22+
}
23+
24+
impl Display for AnsiStyle {
25+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26+
match self.attributes {
27+
Some(ref a) => a.fmt(f),
28+
None => Ok(()),
29+
}
30+
}
31+
}
32+
33+
struct Attributes {
34+
foreground: String,
35+
background: String,
36+
underlined: String,
37+
38+
/// The character set to use.
39+
/// REGEX: `\^[()][AB0-3]`
40+
charset: String,
41+
42+
/// A buffer for unknown sequences.
43+
unknown_buffer: String,
44+
45+
/// ON: ^[1m
46+
/// OFF: ^[22m
47+
bold: String,
48+
49+
/// ON: ^[2m
50+
/// OFF: ^[22m
51+
dim: String,
52+
53+
/// ON: ^[4m
54+
/// OFF: ^[24m
55+
underline: String,
56+
57+
/// ON: ^[3m
58+
/// OFF: ^[23m
59+
italic: String,
60+
61+
/// ON: ^[9m
62+
/// OFF: ^[29m
63+
strike: String,
64+
}
65+
66+
impl Attributes {
67+
pub fn new() -> Self {
68+
Attributes {
69+
foreground: "".to_owned(),
70+
background: "".to_owned(),
71+
underlined: "".to_owned(),
72+
charset: "".to_owned(),
73+
unknown_buffer: "".to_owned(),
74+
bold: "".to_owned(),
75+
dim: "".to_owned(),
76+
underline: "".to_owned(),
77+
italic: "".to_owned(),
78+
strike: "".to_owned(),
79+
}
80+
}
81+
82+
/// Update the attributes with an escape sequence.
83+
/// Returns `false` if the sequence is unsupported.
84+
pub fn update(&mut self, sequence: &str) -> bool {
85+
let mut chars = sequence.char_indices().skip(1);
86+
87+
if let Some((_, t)) = chars.next() {
88+
match t {
89+
'(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
90+
')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
91+
'[' => {
92+
if let Some((i, last)) = chars.last() {
93+
// SAFETY: Always starts with ^[ and ends with m.
94+
self.update_with_csi(last, &sequence[2..i])
95+
} else {
96+
false
97+
}
98+
}
99+
_ => self.update_with_unsupported(sequence),
100+
}
101+
} else {
102+
false
103+
}
104+
}
105+
106+
fn sgr_reset(&mut self) {
107+
self.foreground.clear();
108+
self.background.clear();
109+
self.underlined.clear();
110+
self.bold.clear();
111+
self.dim.clear();
112+
self.underline.clear();
113+
self.italic.clear();
114+
self.strike.clear();
115+
}
116+
117+
fn update_with_sgr(&mut self, parameters: &str) -> bool {
118+
let mut iter = parameters
119+
.split(';')
120+
.map(|p| if p.is_empty() { "0" } else { p })
121+
.map(|p| p.parse::<u16>())
122+
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
123+
124+
while let Some(p) = iter.next() {
125+
match p {
126+
0 => self.sgr_reset(),
127+
1 => self.bold = format!("\x1B[{}m", parameters),
128+
2 => self.dim = format!("\x1B[{}m", parameters),
129+
3 => self.italic = format!("\x1B[{}m", parameters),
130+
4 => self.underline = format!("\x1B[{}m", parameters),
131+
23 => self.italic.clear(),
132+
24 => self.underline.clear(),
133+
22 => {
134+
self.bold.clear();
135+
self.dim.clear();
136+
}
137+
30..=39 => self.foreground = Self::parse_color(p, &mut iter),
138+
40..=49 => self.background = Self::parse_color(p, &mut iter),
139+
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
140+
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
141+
100..=107 => self.foreground = Self::parse_color(p, &mut iter),
142+
_ => {
143+
// Unsupported SGR sequence.
144+
// Be compatible and pretend one just wasn't was provided.
145+
}
146+
}
147+
}
148+
149+
true
150+
}
151+
152+
fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
153+
if finalizer == 'm' {
154+
self.update_with_sgr(sequence)
155+
} else {
156+
false
157+
}
158+
}
159+
160+
fn update_with_unsupported(&mut self, sequence: &str) -> bool {
161+
self.unknown_buffer.push_str(&sequence);
162+
false
163+
}
164+
165+
fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
166+
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
167+
true
168+
}
169+
170+
fn parse_color(color: u16, parameters: &mut dyn Iterator<Item = u16>) -> String {
171+
match color % 10 {
172+
8 => match parameters.next() {
173+
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
174+
Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
175+
Some(c) => format!("\x1B[{};{}m", color, c),
176+
_ => "".to_owned(),
177+
},
178+
9 => "".to_owned(),
179+
_ => format!("\x1B[{}m", color),
180+
}
181+
}
182+
}
183+
184+
impl Display for Attributes {
185+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186+
write!(
187+
f,
188+
"{}{}{}{}{}{}{}{}{}",
189+
self.foreground,
190+
self.background,
191+
self.underlined,
192+
self.charset,
193+
self.bold,
194+
self.dim,
195+
self.underline,
196+
self.italic,
197+
self.strike,
198+
)
199+
}
200+
}
201+
202+
fn join(
203+
delimiter: &str,
204+
limit: usize,
205+
iterator: &mut dyn Iterator<Item = impl ToString>,
206+
) -> String {
207+
iterator
208+
.take(limit)
209+
.map(|i| i.to_string())
210+
.collect::<Vec<String>>()
211+
.join(delimiter)
212+
}

0 commit comments

Comments
 (0)