From 19e23c321b3d6bbdcddc2011314192e79a4edcac Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Tue, 3 Jan 2023 00:07:03 -0800 Subject: [PATCH] formatter: parse color config eagerly It's simpler to parse the color config eagerly. It might also be faster. --- src/formatter.rs | 224 ++++++++++++++++++++++++++--------------------- 1 file changed, 122 insertions(+), 102 deletions(-) diff --git a/src/formatter.rs b/src/formatter.rs index 2233a7735..a724411e9 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -20,6 +20,7 @@ use std::{fmt, io}; use crossterm::queue; use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor}; +use itertools::Itertools; // Lets the caller label strings and translates the labels to colors pub trait Formatter: Write { @@ -91,15 +92,15 @@ pub struct FormatterFactory { enum FormatterFactoryKind { PlainText, Color { - colors: Arc>, + rules: Arc, Style>>, }, } impl FormatterFactory { pub fn prepare(config: &config::Config, color: bool) -> Self { let kind = if color { - let colors = Arc::new(config_colors(config)); - FormatterFactoryKind::Color { colors } + let rules = Arc::new(rules_from_config(config)); + FormatterFactoryKind::Color { rules } } else { FormatterFactoryKind::PlainText }; @@ -112,8 +113,8 @@ impl FormatterFactory { ) -> Box { match &self.kind { FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)), - FormatterFactoryKind::Color { colors } => { - Box::new(ColorFormatter::new(output, colors.clone())) + FormatterFactoryKind::Color { rules } => { + Box::new(ColorFormatter::new(output, rules.clone())) } } } @@ -150,8 +151,8 @@ impl Formatter for PlainTextFormatter { } #[derive(Clone, Debug, PartialEq, Eq)] -struct Style { - fg_color: Color, +pub struct Style { + pub fg_color: Color, } impl Default for Style { @@ -164,43 +165,38 @@ impl Default for Style { pub struct ColorFormatter { output: W, - colors: Arc>, + rules: Arc, Style>>, labels: Vec, cached_styles: HashMap, Style>, current_style: Style, } -fn config_colors(config: &config::Config) -> HashMap { - let mut result = HashMap::new(); - if let Ok(table) = config.get_table("colors") { - for (key, value) in table { - result.insert(key, value.to_string()); - } - } - result -} - impl ColorFormatter { - pub fn new(output: W, colors: Arc>) -> ColorFormatter { + pub fn new(output: W, rules: Arc, Style>>) -> ColorFormatter { ColorFormatter { output, - colors, + rules, labels: vec![], cached_styles: HashMap::new(), current_style: Style::default(), } } + pub fn for_config(output: W, config: &config::Config) -> ColorFormatter { + let rules = rules_from_config(config); + Self::new(output, Arc::new(rules)) + } + fn current_style(&mut self) -> Style { if let Some(cached) = self.cached_styles.get(&self.labels) { cached.clone() } else { - let mut best_match = (-1, ""); - for (key, value) in self.colors.as_ref() { + let mut best_match = (-1, Style::default()); + for (labels, style) in self.rules.as_ref() { let mut num_matching = 0; let mut labels_iter = self.labels.iter(); let mut valid = true; - for required_label in key.split_whitespace() { + for required_label in labels { loop { match labels_iter.next() { Some(label) if label == required_label => { @@ -220,12 +216,11 @@ impl ColorFormatter { continue; } if num_matching >= best_match.0 { - best_match = (num_matching, value) + best_match = (num_matching, style.clone()) } } - let fg_color = color_for_name(best_match.1); - let style = Style { fg_color }; + let style = best_match.1; self.cached_styles .insert(self.labels.clone(), style.clone()); style @@ -265,6 +260,23 @@ fn is_bright(color: &Color) -> bool { ) } +fn rules_from_config(config: &config::Config) -> HashMap, Style> { + let mut result = HashMap::new(); + if let Ok(table) = config.get_table("colors") { + for (key, value) in table { + let labels = key + .split_whitespace() + .map(ToString::to_string) + .collect_vec(); + let style = Style { + fg_color: color_for_name(&value.to_string()), + }; + result.insert(labels, style); + } + } + result +} + fn color_for_name(color_name: &str) -> Color { match color_name { "black" => Color::Black, @@ -311,10 +323,15 @@ impl Formatter for ColorFormatter { #[cfg(test)] mod tests { - use maplit::hashmap; - use super::*; + fn config_from_string(text: &str) -> config::Config { + config::Config::builder() + .add_source(config::File::from_str(text, config::FileFormat::Toml)) + .build() + .unwrap() + } + #[test] fn test_plaintext_formatter() { // Test that PlainTextFormatter ignores labels. @@ -347,13 +364,16 @@ mod tests { "bright cyan", "bright white", ]; - let mut color_config = HashMap::new(); - for color in &colors { + let mut config_builder = config::Config::builder(); + for color in colors { // Use the color name as the label. - color_config.insert(color.replace(' ', "-").to_string(), color.to_string()); + config_builder = config_builder + .set_override(format!("colors.{}", color.replace(' ', "-")), color) + .unwrap(); } let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new(&mut output, Arc::new(color_config)); + let mut formatter = + ColorFormatter::for_config(&mut output, &config_builder.build().unwrap()); for color in colors { formatter.add_label(&color.replace(' ', "-")).unwrap(); formatter.write_str(&format!(" {color} ")).unwrap(); @@ -384,13 +404,13 @@ mod tests { fn test_color_formatter_single_label() { // Test that a single label can be colored and that the color is reset // afterwards. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "inside".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors.inside = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.write_str(" before ").unwrap(); formatter.add_label("inside").unwrap(); formatter.write_str(" inside ").unwrap(); @@ -402,14 +422,14 @@ mod tests { #[test] fn test_color_formatter_no_space() { // Test that two different colors can touch. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "red".to_string() => "red".to_string(), - "green".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors.red = "red" + colors.green = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.write_str("before").unwrap(); formatter.add_label("red").unwrap(); formatter.write_str("first").unwrap(); @@ -424,13 +444,13 @@ mod tests { #[test] fn test_color_formatter_ansi_codes_in_text() { // Test that ANSI codes in the input text are escaped. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "red".to_string() => "red".to_string(), - }), + let config = config_from_string( + r#" + colors.red = "red" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("red").unwrap(); formatter .write_str("\x1b[1mnot actually bold\x1b[0m") @@ -445,15 +465,15 @@ mod tests { // A color can be associated with a combination of labels. A more specific match // overrides a less specific match. After the inner label is removed, the outer // color is used again (we don't reset). - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "outer".to_string() => "blue".to_string(), - "inner".to_string() => "red".to_string(), - "outer inner".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors.outer = "blue" + colors.inner = "red" + colors."outer inner" = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.write_str(" before outer ").unwrap(); formatter.add_label("outer").unwrap(); formatter.write_str(" before inner ").unwrap(); @@ -470,13 +490,13 @@ mod tests { #[test] fn test_color_formatter_partial_match() { // A partial match doesn't count - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "outer inner".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors."outer inner" = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("outer").unwrap(); formatter.write_str(" not colored ").unwrap(); formatter.add_label("inner").unwrap(); @@ -491,14 +511,14 @@ mod tests { #[test] fn test_color_formatter_unrecognized_color() { // An unrecognized color is ignored; it doesn't reset the color. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "outer".to_string() => "red".to_string(), - "outer inner".to_string() => "bloo".to_string(), - }), + let config = config_from_string( + r#" + colors."outer" = "red" + colors."outer inner" = "bloo" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("outer").unwrap(); formatter.write_str(" red before ").unwrap(); formatter.add_label("inner").unwrap(); @@ -514,14 +534,14 @@ mod tests { #[test] fn test_color_formatter_sibling() { // A partial match on one rule does not eliminate other rules. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "outer1 inner1".to_string() => "red".to_string(), - "inner2".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors."outer1 inner1" = "red" + colors.inner2 = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("outer1").unwrap(); formatter.add_label("inner2").unwrap(); formatter.write_str(" hello ").unwrap(); @@ -534,13 +554,13 @@ mod tests { #[test] fn test_color_formatter_reverse_order() { // Rules don't match labels out of order - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "inner outer".to_string() => "green".to_string(), - }), + let config = config_from_string( + r#" + colors."inner outer" = "green" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("outer").unwrap(); formatter.add_label("inner").unwrap(); formatter.write_str(" hello ").unwrap(); @@ -552,15 +572,15 @@ mod tests { #[test] fn test_color_formatter_number_of_matches_matters() { // Rules that match more labels take precedence. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "a b".to_string() => "red".to_string(), - "c".to_string() => "green".to_string(), - "b c d".to_string() => "blue".to_string(), - }), + let config = config_from_string( + r#" + colors."a b" = "red" + colors."c" = "green" + colors."b c d" = "blue" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("a").unwrap(); formatter.write_str(" a1 ").unwrap(); formatter.add_label("b").unwrap(); @@ -583,16 +603,16 @@ mod tests { #[test] fn test_color_formatter_innermost_wins() { // When two labels match, the innermost one wins. - let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::new( - &mut output, - Arc::new(hashmap! { - "a".to_string() => "red".to_string(), - "b".to_string() => "green".to_string(), - "a c".to_string() => "blue".to_string(), - "b c".to_string() => "yellow".to_string(), - }), + let config = config_from_string( + r#" + colors."a" = "red" + colors."b" = "green" + colors."a c" = "blue" + colors."b c" = "yellow" + "#, ); + let mut output: Vec = vec![]; + let mut formatter = ColorFormatter::for_config(&mut output, &config); formatter.add_label("a").unwrap(); formatter.write_str(" a1 ").unwrap(); formatter.add_label("b").unwrap();