-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support --color-moved #267
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cd7953c
Initial implementation of color-moved support
dandavison d9a0767
Emit raw lines instead of explicitly handling --color-moved
dandavison e03a12d
Initialize parser code based on alacritty ansi.rs
dandavison c6f66ab
Parse ANSI escape sequences as ansi_term Style
dandavison 5f97f32
Add inspect-raw-lines {true,false} option
dandavison File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
pub mod parse; | ||
|
||
use std::cmp::min; | ||
|
||
use console; | ||
use itertools::Itertools; | ||
|
||
pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K"; | ||
pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K"; | ||
pub const ANSI_SGR_RESET: &str = "\x1b[0m"; | ||
|
||
pub fn string_starts_with_ansi_escape_sequence(s: &str) -> bool { | ||
console::AnsiCodeIterator::new(s) | ||
.nth(0) | ||
.map(|(_, is_ansi)| is_ansi) | ||
.unwrap_or(false) | ||
} | ||
|
||
/// Return string formed from a byte slice starting at byte position `start`, where the index | ||
/// counts bytes in non-ANSI-escape-sequence content only. All ANSI escape sequences in the | ||
/// original string are preserved. | ||
pub fn ansi_preserving_slice(s: &str, start: usize) -> String { | ||
console::AnsiCodeIterator::new(s) | ||
.scan(0, |i, (substring, is_ansi)| { | ||
// i is the index in non-ANSI-escape-sequence content. | ||
let substring_slice = if is_ansi || *i > start { | ||
substring | ||
} else { | ||
&substring[min(substring.len(), start - *i)..] | ||
}; | ||
if !is_ansi { | ||
*i += substring.len(); | ||
} | ||
Some(substring_slice) | ||
}) | ||
.join("") | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
|
||
use crate::ansi::ansi_preserving_slice; | ||
use crate::ansi::string_starts_with_ansi_escape_sequence; | ||
|
||
#[test] | ||
fn test_string_starts_with_ansi_escape_sequence() { | ||
assert!(!string_starts_with_ansi_escape_sequence("")); | ||
assert!(!string_starts_with_ansi_escape_sequence("-")); | ||
assert!(string_starts_with_ansi_escape_sequence( | ||
"\x1b[31m-XXX\x1b[m\n" | ||
)); | ||
assert!(string_starts_with_ansi_escape_sequence("\x1b[32m+XXX")); | ||
} | ||
|
||
#[test] | ||
fn test_ansi_preserving_slice() { | ||
assert_eq!(ansi_preserving_slice("", 0), ""); | ||
assert_eq!(ansi_preserving_slice("a", 0), "a"); | ||
assert_eq!(ansi_preserving_slice("a", 1), ""); | ||
assert_eq!( | ||
ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 1), | ||
"\x1b[1;35m2222.2222.2222.2222\x1b[0m" | ||
); | ||
assert_eq!( | ||
ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 15), | ||
"\x1b[1;35m.2222\x1b[0m" | ||
); | ||
assert_eq!( | ||
ansi_preserving_slice("\x1b[1;36m-\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n", 1), | ||
"\x1b[1;36m\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n" | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
use ansi_term; | ||
use vte; | ||
|
||
pub fn parse_first_style(bytes: impl Iterator<Item = u8>) -> Option<ansi_term::Style> { | ||
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<ansi_term::Style>, | ||
} | ||
|
||
// 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<ansi_term::Color> { | ||
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); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dandavison there's a typo in the URL :) it says "2" when it should say "72".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!