diff --git a/src/ansi.rs b/src/ansi/mod.rs similarity index 99% rename from src/ansi.rs rename to src/ansi/mod.rs index c885c304e..158b562c2 100644 --- a/src/ansi.rs +++ b/src/ansi/mod.rs @@ -1,3 +1,5 @@ +pub mod parse; + use std::cmp::min; use console; diff --git a/src/ansi/parse.rs b/src/ansi/parse.rs new file mode 100644 index 000000000..bd9b899c5 --- /dev/null +++ b/src/ansi/parse.rs @@ -0,0 +1,205 @@ +use ansi_term; +use vte; + +pub fn parse_first_style(bytes: impl Iterator) -> Option { + let mut machine = vte::Parser::new(); + let mut performer = Performer { style: None }; + for b in bytes { + if performer.style.is_some() { + return performer.style; + } + machine.advance(&mut performer, b) + } + None +} + +struct Performer { + style: Option, +} + +// Based on https://github.com/alacritty/vte/blob/0310be12d3007e32be614c5df94653d29fcc1a8b/examples/parselog.rs +impl vte::Perform for Performer { + fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { + if ignore || intermediates.len() > 1 { + return; + } + + match (c, intermediates.get(0)) { + ('m', None) => { + if params.is_empty() { + // Attr::Reset; + } else { + self.style = Some(ansi_term_style_from_sgr_parameters(params)) + } + } + _ => {} + } + } + + fn print(&mut self, _c: char) {} + + fn execute(&mut self, _byte: u8) {} + + fn hook(&mut self, _params: &[i64], _intermediates: &[u8], _ignore: bool, _c: char) {} + + fn put(&mut self, _byte: u8) {} + + fn unhook(&mut self) {} + + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} + + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {} +} + +// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1168 +fn ansi_term_style_from_sgr_parameters(parameters: &[i64]) -> ansi_term::Style { + let mut i = 0; + let mut style = ansi_term::Style::new(); + loop { + if i >= parameters.len() { + break; + } + + match parameters[i] { + // 0 => Some(Attr::Reset), + 1 => style.is_bold = true, + 2 => style.is_dimmed = true, + 3 => style.is_italic = true, + 4 => style.is_underline = true, + // 5 => Some(Attr::BlinkSlow), + // 6 => Some(Attr::BlinkFast), + 7 => style.is_reverse = true, + 8 => style.is_hidden = true, + 9 => style.is_strikethrough = true, + // 21 => Some(Attr::CancelBold), + // 22 => Some(Attr::CancelBoldDim), + // 23 => Some(Attr::CancelItalic), + // 24 => Some(Attr::CancelUnderline), + // 25 => Some(Attr::CancelBlink), + // 27 => Some(Attr::CancelReverse), + // 28 => Some(Attr::CancelHidden), + // 29 => Some(Attr::CancelStrike), + 30 => style.foreground = Some(ansi_term::Color::Black), + 31 => style.foreground = Some(ansi_term::Color::Red), + 32 => style.foreground = Some(ansi_term::Color::Green), + 33 => style.foreground = Some(ansi_term::Color::Yellow), + 34 => style.foreground = Some(ansi_term::Color::Blue), + 35 => style.foreground = Some(ansi_term::Color::Purple), + 36 => style.foreground = Some(ansi_term::Color::Cyan), + 37 => style.foreground = Some(ansi_term::Color::White), + 38 => { + let mut start = 0; + if let Some(color) = parse_sgr_color(¶meters[i..], &mut start) { + i += start; + style.foreground = Some(color); + } + } + // 39 => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))), + 40 => style.background = Some(ansi_term::Color::Black), + 41 => style.background = Some(ansi_term::Color::Red), + 42 => style.background = Some(ansi_term::Color::Green), + 43 => style.background = Some(ansi_term::Color::Yellow), + 44 => style.background = Some(ansi_term::Color::Blue), + 45 => style.background = Some(ansi_term::Color::Purple), + 46 => style.background = Some(ansi_term::Color::Cyan), + 47 => style.background = Some(ansi_term::Color::White), + 48 => { + let mut start = 0; + if let Some(color) = parse_sgr_color(¶meters[i..], &mut start) { + i += start; + style.background = Some(color); + } + } + // 49 => Some(Attr::Background(Color::Named(NamedColor::Background))), + // "bright" colors. ansi_term doesn't offer a way to emit them as, e.g., 90m; instead + // that would be 38;5;8. + 90 => style.foreground = Some(ansi_term::Color::Fixed(8)), + 91 => style.foreground = Some(ansi_term::Color::Fixed(9)), + 92 => style.foreground = Some(ansi_term::Color::Fixed(10)), + 93 => style.foreground = Some(ansi_term::Color::Fixed(11)), + 94 => style.foreground = Some(ansi_term::Color::Fixed(12)), + 95 => style.foreground = Some(ansi_term::Color::Fixed(13)), + 96 => style.foreground = Some(ansi_term::Color::Fixed(14)), + 97 => style.foreground = Some(ansi_term::Color::Fixed(15)), + 100 => style.background = Some(ansi_term::Color::Fixed(8)), + 101 => style.background = Some(ansi_term::Color::Fixed(9)), + 102 => style.background = Some(ansi_term::Color::Fixed(10)), + 103 => style.background = Some(ansi_term::Color::Fixed(11)), + 104 => style.background = Some(ansi_term::Color::Fixed(12)), + 105 => style.background = Some(ansi_term::Color::Fixed(13)), + 106 => style.background = Some(ansi_term::Color::Fixed(14)), + 107 => style.background = Some(ansi_term::Color::Fixed(15)), + _ => {} + }; + i += 1; + } + style +} + +// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1258 +fn parse_sgr_color(attrs: &[i64], i: &mut usize) -> Option { + if attrs.len() < 2 { + return None; + } + + match attrs[*i + 1] { + 2 => { + // RGB color spec. + if attrs.len() < 5 { + // debug!("Expected RGB color spec; got {:?}", attrs); + return None; + } + + let r = attrs[*i + 2]; + let g = attrs[*i + 3]; + let b = attrs[*i + 4]; + + *i += 4; + + let range = 0..256; + if !range.contains(&r) || !range.contains(&g) || !range.contains(&b) { + // debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b); + return None; + } + + Some(ansi_term::Color::RGB(r as u8, g as u8, b as u8)) + } + 5 => { + if attrs.len() < 3 { + // debug!("Expected color index; got {:?}", attrs); + None + } else { + *i += 2; + let idx = attrs[*i]; + match idx { + 0..=255 => Some(ansi_term::Color::Fixed(idx as u8)), + _ => { + // debug!("Invalid color index: {}", idx); + None + } + } + } + } + _ => { + // debug!("Unexpected color attr: {}", attrs[*i + 1]); + None + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_parse_first_style() { + let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n"; + let style = parse_first_style(minus_line_from_unconfigured_git.bytes()); + let expected_style = ansi_term::Style { + foreground: Some(ansi_term::Color::Red), + ..ansi_term::Style::default() + }; + assert_eq!(Some(expected_style), style); + } +} diff --git a/src/ansi_parser.rs b/src/ansi_parser.rs deleted file mode 100644 index b32d8f6a4..000000000 --- a/src/ansi_parser.rs +++ /dev/null @@ -1,224 +0,0 @@ -use ansi_term; -use vte; - -struct Parser { - machine: vte::Parser, -} - -impl Parser { - pub fn new() -> Self { - Self { - machine: vte::Parser::new(), - } - } -} - -impl vte::Perform for Parser { - fn print(&mut self, c: char) { - println!("[print] {:?}", c); - } - - fn execute(&mut self, byte: u8) { - println!("[execute] {:02x}", byte); - } - - fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { - println!( - "[hook] params={:?}, intermediates={:?}, ignore={:?}, char={:?}", - params, intermediates, ignore, c - ); - } - - fn put(&mut self, byte: u8) { - println!("[put] {:02x}", byte); - } - - fn unhook(&mut self) { - println!("[unhook]"); - } - - fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { - println!( - "[osc_dispatch] params={:?} bell_terminated={}", - params, bell_terminated - ); - } - - fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { - println!( - "[csi_dispatch] params={:?}, intermediates={:?}, ignore={:?}, char={:?}", - params, intermediates, ignore, c - ); - - if ignore || intermediates.len() > 1 { - return; - } - - match (c, intermediates.get(0)) { - ('m', None) => { - if params.is_empty() { - Attr::Reset; - } else { - for attr in attrs_from_sgr_parameters(args) { - match attr { - Some(attr) => handler.terminal_attribute(attr), - None => {} - } - } - } - } - _ => {} - } - } - - fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { - println!( - "[esc_dispatch] intermediates={:?}, ignore={:?}, byte={:02x}", - intermediates, ignore, byte - ); - } -} - -fn attrs_from_sgr_parameters(parameters: &[i64]) -> Vec> { - let mut i = 0; - let mut attrs = Vec::with_capacity(parameters.len()); - loop { - if i >= parameters.len() { - break; - } - - let attr = match parameters[i] { - 0 => Some(Attr::Reset), - 1 => Some(Attr::Bold), - 2 => Some(Attr::Dim), - 3 => Some(Attr::Italic), - 4 => Some(Attr::Underline), - 5 => Some(Attr::BlinkSlow), - 6 => Some(Attr::BlinkFast), - 7 => Some(Attr::Reverse), - 8 => Some(Attr::Hidden), - 9 => Some(Attr::Strike), - 21 => Some(Attr::CancelBold), - 22 => Some(Attr::CancelBoldDim), - 23 => Some(Attr::CancelItalic), - 24 => Some(Attr::CancelUnderline), - 25 => Some(Attr::CancelBlink), - 27 => Some(Attr::CancelReverse), - 28 => Some(Attr::CancelHidden), - 29 => Some(Attr::CancelStrike), - 30 => Some(Attr::Foreground(Color::Named(NamedColor::Black))), - 31 => Some(Attr::Foreground(Color::Named(NamedColor::Red))), - 32 => Some(Attr::Foreground(Color::Named(NamedColor::Green))), - 33 => Some(Attr::Foreground(Color::Named(NamedColor::Yellow))), - 34 => Some(Attr::Foreground(Color::Named(NamedColor::Blue))), - 35 => Some(Attr::Foreground(Color::Named(NamedColor::Magenta))), - 36 => Some(Attr::Foreground(Color::Named(NamedColor::Cyan))), - 37 => Some(Attr::Foreground(Color::Named(NamedColor::White))), - 38 => { - let mut start = 0; - if let Some(color) = parse_sgr_color(¶meters[i..], &mut start) { - i += start; - Some(Attr::Foreground(color)) - } else { - None - } - } - 39 => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))), - 40 => Some(Attr::Background(Color::Named(NamedColor::Black))), - 41 => Some(Attr::Background(Color::Named(NamedColor::Red))), - 42 => Some(Attr::Background(Color::Named(NamedColor::Green))), - 43 => Some(Attr::Background(Color::Named(NamedColor::Yellow))), - 44 => Some(Attr::Background(Color::Named(NamedColor::Blue))), - 45 => Some(Attr::Background(Color::Named(NamedColor::Magenta))), - 46 => Some(Attr::Background(Color::Named(NamedColor::Cyan))), - 47 => Some(Attr::Background(Color::Named(NamedColor::White))), - 48 => { - let mut start = 0; - if let Some(color) = parse_sgr_color(¶meters[i..], &mut start) { - i += start; - Some(Attr::Background(color)) - } else { - None - } - } - 49 => Some(Attr::Background(Color::Named(NamedColor::Background))), - 90 => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlack))), - 91 => Some(Attr::Foreground(Color::Named(NamedColor::BrightRed))), - 92 => Some(Attr::Foreground(Color::Named(NamedColor::BrightGreen))), - 93 => Some(Attr::Foreground(Color::Named(NamedColor::BrightYellow))), - 94 => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlue))), - 95 => Some(Attr::Foreground(Color::Named(NamedColor::BrightMagenta))), - 96 => Some(Attr::Foreground(Color::Named(NamedColor::BrightCyan))), - 97 => Some(Attr::Foreground(Color::Named(NamedColor::BrightWhite))), - 100 => Some(Attr::Background(Color::Named(NamedColor::BrightBlack))), - 101 => Some(Attr::Background(Color::Named(NamedColor::BrightRed))), - 102 => Some(Attr::Background(Color::Named(NamedColor::BrightGreen))), - 103 => Some(Attr::Background(Color::Named(NamedColor::BrightYellow))), - 104 => Some(Attr::Background(Color::Named(NamedColor::BrightBlue))), - 105 => Some(Attr::Background(Color::Named(NamedColor::BrightMagenta))), - 106 => Some(Attr::Background(Color::Named(NamedColor::BrightCyan))), - 107 => Some(Attr::Background(Color::Named(NamedColor::BrightWhite))), - _ => None, - }; - - attrs.push(attr); - - i += 1; - } - attrs -} - -/// Parse a color specifier from list of attributes. -fn parse_sgr_color(attrs: &[i64], i: &mut usize) -> Option { - if attrs.len() < 2 { - return None; - } - - match attrs[*i + 1] { - 2 => { - // RGB color spec. - if attrs.len() < 5 { - debug!("Expected RGB color spec; got {:?}", attrs); - return None; - } - - let r = attrs[*i + 2]; - let g = attrs[*i + 3]; - let b = attrs[*i + 4]; - - *i += 4; - - let range = 0..256; - if !range.contains(&r) || !range.contains(&g) || !range.contains(&b) { - debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b); - return None; - } - - Some(Color::Spec(Rgb { - r: r as u8, - g: g as u8, - b: b as u8, - })) - } - 5 => { - if attrs.len() < 3 { - debug!("Expected color index; got {:?}", attrs); - None - } else { - *i += 2; - let idx = attrs[*i]; - match idx { - 0..=255 => Some(Color::Indexed(idx as u8)), - _ => { - debug!("Invalid color index: {}", idx); - None - } - } - } - } - _ => { - debug!("Unexpected color attr: {}", attrs[*i + 1]); - None - } - } -} diff --git a/src/main.rs b/src/main.rs index 027127bee..3ec6b09b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ extern crate error_chain; mod align; mod ansi; -mod ansi_parser; mod bat; mod cli; mod color; @@ -210,7 +209,7 @@ fn show_config(config: &config::Config) { .unwrap_or("none".to_string()), width = match config.decorations_width { cli::Width::Fixed(width) => width.to_string(), - cli::Width::Variable => "variable".to_string() + cli::Width::Variable => "variable".to_string(), }, tab_width = config.tab_width, tokenization_regex = format_option_value(&config.tokenization_regex.to_string()), diff --git a/src/style.rs b/src/style.rs index 0188922c9..2255fd479 100644 --- a/src/style.rs +++ b/src/style.rs @@ -88,7 +88,10 @@ impl Style { } pub fn is_applied_to(&self, s: &str) -> bool { - s.starts_with(&self.ansi_term_style.prefix().to_string()) + match ansi::parse::parse_first_style(s.bytes()) { + Some(parsed_style) => ansi_term_style_equality(parsed_style, self.ansi_term_style), + None => false, + } } pub fn to_painted_string(&self) -> ansi_term::ANSIGenericString { @@ -140,6 +143,54 @@ impl Style { } } +fn ansi_term_style_equality(a: ansi_term::Style, b: ansi_term::Style) -> bool { + let a_attrs = ansi_term::Style { + foreground: None, + background: None, + ..a + }; + let b_attrs = ansi_term::Style { + foreground: None, + background: None, + ..b + }; + if a_attrs != b_attrs { + return false; + } else { + return ansi_term_color_equality(a.foreground, b.foreground) + & ansi_term_color_equality(a.background, b.background); + } +} + +fn ansi_term_color_equality(a: Option, b: Option) -> bool { + match (a, b) { + (None, None) => true, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(a), Some(b)) => { + if a == b { + true + } else { + ansi_term_16_color_equality(a, b) || ansi_term_16_color_equality(b, a) + } + } + } +} + +fn ansi_term_16_color_equality(a: ansi_term::Color, b: ansi_term::Color) -> bool { + match (a, b) { + (ansi_term::Color::Fixed(0), ansi_term::Color::Black) => true, + (ansi_term::Color::Fixed(1), ansi_term::Color::Red) => true, + (ansi_term::Color::Fixed(2), ansi_term::Color::Green) => true, + (ansi_term::Color::Fixed(3), ansi_term::Color::Yellow) => true, + (ansi_term::Color::Fixed(4), ansi_term::Color::Blue) => true, + (ansi_term::Color::Fixed(5), ansi_term::Color::Purple) => true, + (ansi_term::Color::Fixed(6), ansi_term::Color::Cyan) => true, + (ansi_term::Color::Fixed(7), ansi_term::Color::White) => true, + _ => false, + } +} + lazy_static! { pub static ref GIT_DEFAULT_MINUS_STYLE: Style = Style { ansi_term_style: ansi_term::Color::Red.normal(), @@ -168,11 +219,53 @@ mod tests { use super::*; + // To add to these tests: + // 1. Stage a file with a single line containing the string "text" + // 2. git -c 'color.diff.new = $STYLE_STRING' diff --cached --color=always | cat -A + + #[test] + fn test_parsed_git_style_string_is_applied_to_git_output() { + for (git_style_string, git_output) in &[ + // "\x1b[32m+\x1b[m\x1b[32mtext\x1b[m\n" + ("0", "\x1b[30m+\x1b[m\x1b[30mtext\x1b[m\n"), + ("black", "\x1b[30m+\x1b[m\x1b[30mtext\x1b[m\n"), + ("1", "\x1b[31m+\x1b[m\x1b[31mtext\x1b[m\n"), + ("red", "\x1b[31m+\x1b[m\x1b[31mtext\x1b[m\n"), + ("0 1", "\x1b[30;41m+\x1b[m\x1b[30;41mtext\x1b[m\n"), + ("black red", "\x1b[30;41m+\x1b[m\x1b[30;41mtext\x1b[m\n"), + ("19", "\x1b[38;5;19m+\x1b[m\x1b[38;5;19mtext\x1b[m\n"), + ("black 19", "\x1b[30;48;5;19m+\x1b[m\x1b[30;48;5;19mtext\x1b[m\n"), + ("19 black", "\x1b[38;5;19;40m+\x1b[m\x1b[38;5;19;40mtext\x1b[m\n"), + ("19 20", "\x1b[38;5;19;48;5;20m+\x1b[m\x1b[38;5;19;48;5;20mtext\x1b[m\n"), + ("#aabbcc", "\x1b[38;2;170;187;204m+\x1b[m\x1b[38;2;170;187;204mtext\x1b[m\n"), + ("0 #aabbcc", "\x1b[30;48;2;170;187;204m+\x1b[m\x1b[30;48;2;170;187;204mtext\x1b[m\n"), + ("#aabbcc 0", "\x1b[38;2;170;187;204;40m+\x1b[m\x1b[38;2;170;187;204;40mtext\x1b[m\n"), + ("19 #aabbcc", "\x1b[38;5;19;48;2;170;187;204m+\x1b[m\x1b[38;5;19;48;2;170;187;204mtext\x1b[m\n"), + ("#aabbcc 19", "\x1b[38;2;170;187;204;48;5;19m+\x1b[m\x1b[38;2;170;187;204;48;5;19mtext\x1b[m\n"), + ("#aabbcc #ddeeff" , "\x1b[38;2;170;187;204;48;2;221;238;255m+\x1b[m\x1b[38;2;170;187;204;48;2;221;238;255mtext\x1b[m\n"), + ("bold #aabbcc #ddeeff" , "\x1b[1;38;2;170;187;204;48;2;221;238;255m+\x1b[m\x1b[1;38;2;170;187;204;48;2;221;238;255mtext\x1b[m\n"), + ("bold #aabbcc ul #ddeeff" , "\x1b[1;4;38;2;170;187;204;48;2;221;238;255m+\x1b[m\x1b[1;4;38;2;170;187;204;48;2;221;238;255mtext\x1b[m\n"), + ("bold #aabbcc ul #ddeeff strike" , "\x1b[1;4;9;38;2;170;187;204;48;2;221;238;255m+\x1b[m\x1b[1;4;9;38;2;170;187;204;48;2;221;238;255mtext\x1b[m\n"), + ("bold 0 ul 1 strike", "\x1b[1;4;9;30;41m+\x1b[m\x1b[1;4;9;30;41mtext\x1b[m\n"), + ("bold 0 ul 19 strike", "\x1b[1;4;9;30;48;5;19m+\x1b[m\x1b[1;4;9;30;48;5;19mtext\x1b[m\n"), + ("bold 19 ul 0 strike", "\x1b[1;4;9;38;5;19;40m+\x1b[m\x1b[1;4;9;38;5;19;40mtext\x1b[m\n"), + ("bold #aabbcc ul 0 strike", "\x1b[1;4;9;38;2;170;187;204;40m+\x1b[m\x1b[1;4;9;38;2;170;187;204;40mtext\x1b[m\n"), + ("bold #aabbcc ul 19 strike" , "\x1b[1;4;9;38;2;170;187;204;48;5;19m+\x1b[m\x1b[1;4;9;38;2;170;187;204;48;5;19mtext\x1b[m\n"), + ("bold 19 ul #aabbcc strike" , "\x1b[1;4;9;38;5;19;48;2;170;187;204m+\x1b[m\x1b[1;4;9;38;5;19;48;2;170;187;204mtext\x1b[m\n"), + ("bold 0 ul #aabbcc strike", "\x1b[1;4;9;30;48;2;170;187;204m+\x1b[m\x1b[1;4;9;30;48;2;170;187;204mtext\x1b[m\n"), + (r##"black "#ddeeff""##, "\x1b[30;48;2;221;238;255m+\x1b[m\x1b[30;48;2;221;238;255m .map(|(_, is_ansi)| is_ansi)\x1b[m\n"), + ("brightred", "\x1b[91m+\x1b[m\x1b[91mtext\x1b[m\n"), + ("normal", "+\x1b[mtext\x1b[m\n") + ] { + assert!(Style::from_git_str(git_style_string).is_applied_to(git_output)); + } + } + #[test] - fn test_is_applied_to() { - assert!(Style::from_git_str(r##"black "#ddeeff""##) - .is_applied_to( - "\x1b[30;48;2;221;238;255m+\x1b[m\x1b[30;48;2;221;238;255m .map(|(_, is_ansi)| is_ansi)\x1b[m\n")) + fn test_is_applied_to_negative_assertion() { + let style_string_from_24 = "bold #aabbcc ul 19 strike"; + let git_output_from_25 = "\x1b[1;4;9;38;5;19;48;2;170;187;204m+\x1b[m\x1b[1;4;9;38;5;19;48;2;170;187;204mtext\x1b[m\n"; + assert!(!Style::from_git_str(style_string_from_24).is_applied_to(git_output_from_25)); } #[test]