From 9721666d331ddfd7a04bbb434cccf38b44701b00 Mon Sep 17 00:00:00 2001 From: Kevin Song Date: Sat, 7 Sep 2019 19:33:06 -0500 Subject: feat: Add the ability to configure per-module color styles (#285) Add parsing logic, config support, docs, and integration with other modules for custom styling of each module. --- src/config.rs | 199 ++++++++++++++++++++++++++++++++++++++++++++ src/module.rs | 5 ++ src/modules/battery.rs | 5 +- src/modules/character.rs | 18 ++-- src/modules/cmd_duration.rs | 4 +- src/modules/directory.rs | 4 +- src/modules/git_branch.rs | 6 +- src/modules/git_state.rs | 5 +- src/modules/git_status.rs | 5 +- src/modules/golang.rs | 6 +- src/modules/hostname.rs | 8 +- src/modules/jobs.rs | 7 +- src/modules/nix_shell.rs | 6 +- src/modules/nodejs.rs | 6 +- src/modules/package.rs | 6 +- src/modules/python.rs | 4 +- src/modules/ruby.rs | 6 +- src/modules/rust.rs | 6 +- src/modules/username.rs | 25 +++--- 19 files changed, 285 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 5c0ce5bcd..fc23a327e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,8 @@ use std::env; use dirs::home_dir; use toml::value::Table; +use ansi_term::Color; + pub trait Config { fn initialize() -> Table; fn config_from_file() -> Option; @@ -14,6 +16,7 @@ pub trait Config { fn get_as_str(&self, key: &str) -> Option<&str>; fn get_as_i64(&self, key: &str) -> Option; fn get_as_array(&self, key: &str) -> Option<&Vec>; + fn get_as_ansi_style(&self, key: &str) -> Option; // Internal implementation for accessors fn get_config(&self, key: &str) -> Option<&toml::value::Value>; @@ -157,11 +160,132 @@ impl Config for Table { } array_value } + + /// Get a text key and attempt to interpret it into an ANSI style. + fn get_as_ansi_style(&self, key: &str) -> Option { + let style_string = self.get_as_str(key)?; + parse_style_string(style_string) + } +} + +/** Parse a style string which represents an ansi style. Valid tokens in the style + string include the following: + - 'fg:' (specifies that the color read should be a foreground color) + - 'bg:' (specifies that the color read should be a background color) + - 'underline' + - 'bold' + - '' (see the parse_color_string doc for valid color strings) +*/ +fn parse_style_string(style_string: &str) -> Option { + let tokens = style_string.split_whitespace(); + let mut style = ansi_term::Style::new(); + + // If col_fg is true, color the foreground. If it's false, color the background. + let mut col_fg: bool; + + for token in tokens { + let token = token.to_lowercase(); + + // Check for FG/BG identifiers and strip them off if appropriate + let token = if token.as_str().starts_with("fg:") { + col_fg = true; + token.trim_start_matches("fg:").to_owned() + } else if token.as_str().starts_with("bg:") { + col_fg = false; + token.trim_start_matches("bg:").to_owned() + } else { + col_fg = true; // Bare colors are assumed to color the foreground + token + }; + + match token.as_str() { + "underline" => style = style.underline(), + "bold" => style = style.bold(), + "dimmed" => style = style.dimmed(), + "none" => return Some(ansi_term::Style::new()), // Overrides other toks + + // Try to see if this token parses as a valid color string + color_string => { + // Match found: set either fg or bg color + if let Some(ansi_color) = parse_color_string(color_string) { + if col_fg { + style = style.fg(ansi_color); + } else { + style = style.on(ansi_color); + } + } else { + // Match failed: skip this token and log it + log::debug!("Could not parse token in color string: {}", token) + } + } + } + } + + Some(style) +} + +/** Parse a string that represents a color setting, returning None if this fails + There are three valid color formats: + - #RRGGBB (a hash followed by an RGB hex) + - u8 (a number from 0-255, representing an ANSI color) + - colstring (one of the 16 predefined color strings) +*/ +fn parse_color_string(color_string: &str) -> Option { + // Parse RGB hex values + log::trace!("Parsing color_string: {}", color_string); + if color_string.starts_with('#') { + log::trace!( + "Attempting to read hexadecimal color string: {}", + color_string + ); + let r: u8 = u8::from_str_radix(&color_string[1..3], 16).ok()?; + let g: u8 = u8::from_str_radix(&color_string[3..5], 16).ok()?; + let b: u8 = u8::from_str_radix(&color_string[5..7], 16).ok()?; + log::trace!("Read RGB color string: {},{},{}", r, g, b); + return Some(Color::RGB(r, g, b)); + } + + // Parse a u8 (ansi color) + if let Result::Ok(ansi_color_num) = color_string.parse::() { + log::trace!("Read ANSI color string: {}", ansi_color_num); + return Some(Color::Fixed(ansi_color_num)); + } + + // Check for any predefined color strings + // There are no predefined enums for bright colors, so we use Color::Fixed + let predefined_color = match color_string.to_lowercase().as_str() { + "black" => Some(Color::Black), + "red" => Some(Color::Red), + "green" => Some(Color::Green), + "yellow" => Some(Color::Yellow), + "blue" => Some(Color::Blue), + "purple" => Some(Color::Purple), + "cyan" => Some(Color::Cyan), + "white" => Some(Color::White), + "bright-black" => Some(Color::Fixed(8)), // "bright-black" is dark grey + "bright-red" => Some(Color::Fixed(9)), + "bright-green" => Some(Color::Fixed(10)), + "bright-yellow" => Some(Color::Fixed(11)), + "bright-blue" => Some(Color::Fixed(12)), + "bright-purple" => Some(Color::Fixed(13)), + "bright-cyan" => Some(Color::Fixed(14)), + "bright-white" => Some(Color::Fixed(15)), + _ => None, + }; + + if predefined_color.is_some() { + log::trace!("Read predefined color: {}", color_string); + return predefined_color; + } + + // All attempts to parse have failed + None } #[cfg(test)] mod tests { use super::*; + use ansi_term::Style; #[test] fn table_get_as_bool() { @@ -210,4 +334,79 @@ mod tests { ); assert_eq!(table.get_as_bool("string"), None); } + + #[test] + fn table_get_styles_simple() { + let mut table = toml::value::Table::new(); + + // Test for a bold underline green module (with SiLlY cApS) + table.insert( + String::from("mystyle"), + toml::value::Value::String(String::from("bOlD uNdErLiNe GrEeN")), + ); + assert!(table.get_as_ansi_style("mystyle").unwrap().is_bold); + assert!(table.get_as_ansi_style("mystyle").unwrap().is_underline); + assert_eq!( + table.get_as_ansi_style("mystyle").unwrap(), + ansi_term::Style::new().bold().underline().fg(Color::Green) + ); + + // Test a "plain" style with no formatting + table.insert( + String::from("plainstyle"), + toml::value::Value::String(String::from("")), + ); + assert_eq!( + table.get_as_ansi_style("plainstyle").unwrap(), + ansi_term::Style::new() + ); + + // Test a string that's clearly broken + table.insert( + String::from("broken"), + toml::value::Value::String(String::from("djklgfhjkldhlhk;j")), + ); + assert_eq!( + table.get_as_ansi_style("broken").unwrap(), + ansi_term::Style::new() + ); + + // Test a string that's nullified by `none` + table.insert( + String::from("nullified"), + toml::value::Value::String(String::from("fg:red bg:green bold none")), + ); + assert_eq!( + table.get_as_ansi_style("nullified").unwrap(), + ansi_term::Style::new() + ); + } + + #[test] + fn table_get_styles_ordered() { + let mut table = toml::value::Table::new(); + + // Test a background style with inverted order (also test hex + ANSI) + table.insert( + String::from("flipstyle"), + toml::value::Value::String(String::from("bg:#050505 underline fg:120")), + ); + assert_eq!( + table.get_as_ansi_style("flipstyle").unwrap(), + Style::new() + .underline() + .fg(Color::Fixed(120)) + .on(Color::RGB(5, 5, 5)) + ); + + // Test that the last color style is always the one used + table.insert( + String::from("multistyle"), + toml::value::Value::String(String::from("bg:120 bg:125 bg:127 fg:127 122 125")), + ); + assert_eq!( + table.get_as_ansi_style("multistyle").unwrap(), + Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127)) + ); + } } diff --git a/src/module.rs b/src/module.rs index ab1783e4f..80f7bc8d4 100644 --- a/src/module.rs +++ b/src/module.rs @@ -139,6 +139,11 @@ impl<'a> Module<'a> { pub fn config_value_bool(&self, key: &str) -> Option { self.config.and_then(|config| config.get_as_bool(key)) } + + /// Get a module's config value as a style + pub fn config_value_style(&self, key: &str) -> Option