use std::borrow::Cow;
use std::fmt;
use std::hash::{Hash, Hasher};
use lazy_static::lazy_static;
use crate::ansi;
use crate::color;
use crate::git_config::GitConfig;
#[derive(Clone, Copy, PartialEq, Default)]
pub struct Style {
pub ansi_term_style: ansi_term::Style,
pub is_emph: bool,
pub is_omitted: bool,
pub is_raw: bool,
pub is_syntax_highlighted: bool,
pub decoration_style: DecorationStyle,
}
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let ansi = if self.ansi_term_style.is_plain() {
"<a".into()
} else {
format!("ansi_term_style: {:?}, <", self.ansi_term_style)
};
let deco = if self.decoration_style == DecorationStyle::NoDecoration {
"d>".into()
} else {
format!(">, decoration_style: {:?}", self.decoration_style)
};
let is_set = |c: char, set: bool| -> String {
if set {
c.to_uppercase().to_string()
} else {
c.to_lowercase().to_string()
}
};
write!(
f,
"Style {{ {}{}{}{}{}{} }}",
ansi,
is_set('e', self.is_emph),
is_set('o', self.is_omitted),
is_set('r', self.is_raw),
is_set('s', self.is_syntax_highlighted),
deco
)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DecorationStyle {
Box(ansi_term::Style),
Underline(ansi_term::Style),
Overline(ansi_term::Style),
UnderOverline(ansi_term::Style),
BoxWithUnderline(ansi_term::Style),
BoxWithOverline(ansi_term::Style),
BoxWithUnderOverline(ansi_term::Style),
NoDecoration,
}
impl Default for DecorationStyle {
fn default() -> Self {
Self::NoDecoration
}
}
impl Style {
pub fn new() -> Self {
Self {
ansi_term_style: ansi_term::Style::new(),
is_emph: false,
is_omitted: false,
is_raw: false,
is_syntax_highlighted: false,
decoration_style: DecorationStyle::NoDecoration,
}
}
pub fn from_colors(
foreground: Option<ansi_term::Color>,
background: Option<ansi_term::Color>,
) -> Self {
Self {
ansi_term_style: ansi_term::Style {
foreground,
background,
..ansi_term::Style::new()
},
..Self::new()
}
}
pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(
self,
input: I,
) -> ansi_term::ANSIGenericString<'a, S>
where
I: Into<Cow<'a, S>>,
<S as ToOwned>::Owned: fmt::Debug,
{
self.ansi_term_style.paint(input)
}
pub fn get_background_color(&self) -> Option<ansi_term::Color> {
if self.ansi_term_style.is_reverse {
self.ansi_term_style.foreground
} else {
self.ansi_term_style.background
}
}
pub fn is_applied_to(&self, s: &str) -> bool {
match ansi::parse_first_style(s) {
Some(parsed_style) => ansi_term_style_equality(parsed_style, self.ansi_term_style),
None => false,
}
}
pub fn to_painted_string(self) -> ansi_term::ANSIGenericString<'static, str> {
self.paint(self.to_string())
}
}
pub fn paint_color_string<'a>(
color_string: &'a str,
true_color: bool,
git_config: Option<&GitConfig>,
) -> ansi_term::ANSIGenericString<'a, str> {
if let Some(color) = color::parse_color(color_string, true_color, git_config) {
let style = ansi_term::Style {
background: Some(color),
..ansi_term::Style::default()
};
style.paint(color_string)
} else {
ansi_term::ANSIGenericString::from(color_string)
}
}
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_raw {
return write!(f, "raw");
}
let mut words = Vec::<String>::new();
if self.is_omitted {
words.push("omit".to_string());
}
if self.ansi_term_style.is_blink {
words.push("blink".to_string());
}
if self.ansi_term_style.is_bold {
words.push("bold".to_string());
}
if self.ansi_term_style.is_dimmed {
words.push("dim".to_string());
}
if self.ansi_term_style.is_italic {
words.push("italic".to_string());
}
if self.ansi_term_style.is_reverse {
words.push("reverse".to_string());
}
if self.ansi_term_style.is_strikethrough {
words.push("strike".to_string());
}
if self.ansi_term_style.is_underline {
words.push("ul".to_string());
}
match (self.is_syntax_highlighted, self.ansi_term_style.foreground) {
(true, _) => words.push("syntax".to_string()),
(false, Some(color)) => {
words.push(color::color_to_string(color));
}
(false, None) => words.push("normal".to_string()),
}
if let Some(color) = self.ansi_term_style.background {
words.push(color::color_to_string(color))
}
let style_str = words.join(" ");
write!(f, "{}", style_str)
}
}
pub 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 {
false
} else {
ansi_term_color_equality(a.foreground, b.foreground)
& ansi_term_color_equality(a.background, b.background)
}
}
#[derive(Clone)]
pub struct AnsiTermStyleEqualityKey {
attrs_key: (bool, bool, bool, bool, bool, bool, bool, bool),
foreground_key: Option<(u8, u8, u8, u8)>,
background_key: Option<(u8, u8, u8, u8)>,
}
impl PartialEq for AnsiTermStyleEqualityKey {
fn eq(&self, other: &Self) -> bool {
let option_eq = |opt_a, opt_b| match (opt_a, opt_b) {
(Some(a), Some(b)) => a == b,
(None, None) => true,
_ => false,
};
if self.attrs_key != other.attrs_key {
false
} else {
option_eq(self.foreground_key, other.foreground_key)
&& option_eq(self.background_key, other.background_key)
}
}
}
impl Eq for AnsiTermStyleEqualityKey {}
impl Hash for AnsiTermStyleEqualityKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.attrs_key.hash(state);
self.foreground_key.hash(state);
self.background_key.hash(state);
}
}
pub fn ansi_term_style_equality_key(style: ansi_term::Style) -> AnsiTermStyleEqualityKey {
let attrs_key = (
style.is_bold,
style.is_dimmed,
style.is_italic,
style.is_underline,
style.is_blink,
style.is_reverse,
style.is_hidden,
style.is_strikethrough,
);
AnsiTermStyleEqualityKey {
attrs_key,
foreground_key: style.foreground.map(ansi_term_color_equality_key),
background_key: style.background.map(ansi_term_color_equality_key),
}
}
impl fmt::Debug for AnsiTermStyleEqualityKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let is_set = |c: char, set: bool| -> String {
if set {
c.to_uppercase().to_string()
} else {
c.to_lowercase().to_string()
}
};
let (bold, dimmed, italic, underline, blink, reverse, hidden, strikethrough) =
self.attrs_key;
write!(
f,
"ansi_term::Style {{ {:?} {:?} {}{}{}{}{}{}{}{} }}",
self.foreground_key,
self.background_key,
is_set('b', bold),
is_set('d', dimmed),
is_set('i', italic),
is_set('u', underline),
is_set('l', blink),
is_set('r', reverse),
is_set('h', hidden),
is_set('s', strikethrough),
)
}
}
fn ansi_term_color_equality(a: Option<ansi_term::Color>, b: Option<ansi_term::Color>) -> 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 {
matches!(
(a, b),
(ansi_term::Color::Fixed(0), ansi_term::Color::Black)
| (ansi_term::Color::Fixed(1), ansi_term::Color::Red)
| (ansi_term::Color::Fixed(2), ansi_term::Color::Green)
| (ansi_term::Color::Fixed(3), ansi_term::Color::Yellow)
| (ansi_term::Color::Fixed(4), ansi_term::Color::Blue)
| (ansi_term::Color::Fixed(5), ansi_term::Color::Purple)
| (ansi_term::Color::Fixed(6), ansi_term::Color::Cyan)
| (ansi_term::Color::Fixed(7), ansi_term::Color::White)
)
}
fn ansi_term_color_equality_key(color: ansi_term::Color) -> (u8, u8, u8, u8) {
let default = 0xFF;
match color {
ansi_term::Color::Fixed(0) | ansi_term::Color::Black => (0, default, default, default),
ansi_term::Color::Fixed(1) | ansi_term::Color::Red => (1, default, default, default),
ansi_term::Color::Fixed(2) | ansi_term::Color::Green => (2, default, default, default),
ansi_term::Color::Fixed(3) | ansi_term::Color::Yellow => (3, default, default, default),
ansi_term::Color::Fixed(4) | ansi_term::Color::Blue => (4, default, default, default),
ansi_term::Color::Fixed(5) | ansi_term::Color::Purple => (5, default, default, default),
ansi_term::Color::Fixed(6) | ansi_term::Color::Cyan => (6, default, default, default),
ansi_term::Color::Fixed(7) | ansi_term::Color::White => (7, default, default, default),
ansi_term::Color::Fixed(n) => (n, default, default, default),
ansi_term::Color::RGB(r, g, b) => (r, g, b, 0),
}
}
lazy_static! {
pub static ref GIT_DEFAULT_MINUS_STYLE: Style = Style {
ansi_term_style: ansi_term::Color::Red.normal(),
..Style::new()
};
pub static ref GIT_DEFAULT_PLUS_STYLE: Style = Style {
ansi_term_style: ansi_term::Color::Green.normal(),
..Style::new()
};
}
pub fn line_has_style_other_than(line: &str, styles: &[Style]) -> bool {
if !ansi::string_starts_with_ansi_style_sequence(line) {
return false;
}
for style in styles {
if style.is_applied_to(line) {
return false;
}
}
true
}
#[cfg(test)]
pub mod tests {
use super::*;
lazy_static! {
pub static ref GIT_STYLE_STRING_EXAMPLES: Vec<(&'static str, &'static str)> = vec![
("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;255mtext\x1b[m\n"),
("brightred", "\x1b[91m+\x1b[m\x1b[91mtext\x1b[m\n"),
("normal", "\x1b[mtext\x1b[m\n"),
("blink", "\x1b[5m+\x1b[m\x1b[5mtext\x1b[m\n"),
];
}
#[test]
fn test_parse_git_style_string_and_ansi_code_iterator() {
for (git_style_string, git_output) in &*GIT_STYLE_STRING_EXAMPLES {
assert!(Style::from_git_str(git_style_string).is_applied_to(git_output));
}
}
#[test]
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]
fn test_git_default_styles() {
let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n";
let plus_line_from_unconfigured_git = "\x1b[32m+\x1b[m\x1b[32m____\x1b[m\n";
assert!(GIT_DEFAULT_MINUS_STYLE.is_applied_to(minus_line_from_unconfigured_git));
assert!(!GIT_DEFAULT_MINUS_STYLE.is_applied_to(plus_line_from_unconfigured_git));
assert!(GIT_DEFAULT_PLUS_STYLE.is_applied_to(plus_line_from_unconfigured_git));
assert!(!GIT_DEFAULT_PLUS_STYLE.is_applied_to(minus_line_from_unconfigured_git));
}
#[test]
fn test_line_has_style_other_than() {
let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n";
let plus_line_from_unconfigured_git = "\x1b[32m+\x1b[m\x1b[32m____\x1b[m\n";
assert!(!line_has_style_other_than("", &[]));
assert!(!line_has_style_other_than("", &[*GIT_DEFAULT_MINUS_STYLE]));
assert!(!line_has_style_other_than(
minus_line_from_unconfigured_git,
&[*GIT_DEFAULT_MINUS_STYLE]
));
assert!(!line_has_style_other_than(
plus_line_from_unconfigured_git,
&[*GIT_DEFAULT_PLUS_STYLE]
));
assert!(line_has_style_other_than(
minus_line_from_unconfigured_git,
&[*GIT_DEFAULT_PLUS_STYLE]
));
assert!(line_has_style_other_than(
minus_line_from_unconfigured_git,
&[]
));
assert!(line_has_style_other_than(
plus_line_from_unconfigured_git,
&[*GIT_DEFAULT_MINUS_STYLE]
));
assert!(line_has_style_other_than(
plus_line_from_unconfigured_git,
&[]
));
}
#[test]
fn test_style_compact_debug_fmt() {
let mut s = Style::new();
assert_eq!(format!("{:?}", s), "Style { <aeorsd> }");
s.is_emph = true;
assert_eq!(format!("{:?}", s), "Style { <aEorsd> }");
s.ansi_term_style = ansi_term::Style::new().bold();
assert_eq!(
format!("{:?}", s),
"Style { ansi_term_style: Style { bold }, <Eorsd> }"
);
s.decoration_style = DecorationStyle::Underline(s.ansi_term_style.clone());
assert_eq!(
format!("{:?}", s),
"Style { ansi_term_style: Style { bold }, <Eors>, \
decoration_style: Underline(Style { bold }) }"
);
s.ansi_term_style = ansi_term::Style::default();
assert_eq!(
format!("{:?}", s),
"Style { <aEors>, decoration_style: Underline(Style { bold }) }"
);
}
}