use std::{
fmt::{Display, Formatter},
iter::Peekable,
str::CharIndices,
};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
attributes: Option<Attributes>,
}
impl AnsiStyle {
pub fn new() -> Self {
AnsiStyle { attributes: None }
}
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
self.attributes = Some(Attributes::new());
self.attributes.as_mut().unwrap().update(sequence)
}
}
}
pub fn to_reset_sequence(&self) -> String {
match self.attributes {
Some(ref a) => a.to_reset_sequence(),
None => String::new(),
}
}
}
impl Display for AnsiStyle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.attributes {
Some(ref a) => a.fmt(f),
None => Ok(()),
}
}
}
struct Attributes {
has_sgr_sequences: bool,
foreground: String,
background: String,
underlined: String,
/// The character set to use.
/// REGEX: `\^[()][AB0-3]`
charset: String,
/// A buffer for unknown sequences.
unknown_buffer: String,
/// ON: ^[1m
/// OFF: ^[22m
bold: String,
/// ON: ^[2m
/// OFF: ^[22m
dim: String,
/// ON: ^[4m
/// OFF: ^[24m
underline: String,
/// ON: ^[3m
/// OFF: ^[23m
italic: String,
/// ON: ^[9m
/// OFF: ^[29m
strike: String,
/// The hyperlink sequence.
/// FORMAT: \x1B]8;{ID};{URL}\e\\
///
/// `\e\\` may be replaced with BEL `\x07`.
/// Setting both {ID} and {URL} to an empty string represents no hyperlink.
hyperlink: String,
}
impl Attributes {
pub fn new() -> Self {
Attributes {
has_sgr_sequences: false,
foreground: "".to_owned(),
background: "".to_owned(),
underlined: "".to_owned(),
charset: "".to_owned(),
unknown_buffer: "".to_owned(),
bold: "".to_owned(),
dim: "".to_owned(),
underline: "".to_owned(),
italic: "".to_owned(),
strike: "".to_owned(),
hyperlink: "".to_owned(),
}
}
/// Update the attributes with an escape sequence.
/// Returns `false` if the sequence is unsupported.
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
use EscapeSequence::*;
match sequence {
Text(_) => return false,
Unknown(_) => { /* defer to update_with_unsupported */ }
OSC {
raw_sequence,
command,
..
} => {
if command.starts_with("8;") {
return self.update_with_hyperlink(raw_sequence);
}
/* defer to update_with_unsupported */
}
CSI {
final_byte,
parameters,
..
} => {
match final_byte {
"m" => return self.update_with_sgr(parameters),
_ => {
// NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation.
/* defer to update_with_unsupported */
}
}
}
NF { nf_sequence, .. } => {
let mut iter = nf_sequence.chars();
match iter.next() {
Some('(') => return self.update_with_charset('(', iter),
Some(')') => return self.update_with_charset(')', iter),
_ => { /* defer to update_with_unsupported */ }
}
}
}
self.update_with_unsupported(sequence.raw())
}
fn sgr_reset(&mut self) {
self.has_sgr_sequences = false;
self.foreground.clear();
self.background.clear();
self.underlined.clear();
self.bold.clear();
self.dim.clear();
self.underline.clear();
self.italic.clear();
self.strike.clear();
}
fn update_with_sgr(&mut self, parameters: &str) -> bool {
let mut iter = parameters
.split(';')
.map(|p| if p.is_empty() { "0" } else { p })
.map(|p| p.parse::<u16>())
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
self.has_sgr_sequences = true;
while let Some(p) = iter.next() {
match p {
0