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.
This commit is contained in:
Kevin Song
2019-09-07 19:33:06 -05:00
committed by GitHub
parent 3e5cac9852
commit 9721666d33
21 changed files with 485 additions and 184 deletions
+199
View File
@@ -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<Table>;
@@ -14,6 +16,7 @@ pub trait Config {
fn get_as_str(&self, key: &str) -> Option<&str>;
fn get_as_i64(&self, key: &str) -> Option<i64>;
fn get_as_array(&self, key: &str) -> Option<&Vec<toml::value::Value>>;
fn get_as_ansi_style(&self, key: &str) -> Option<ansi_term::Style>;
// 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<ansi_term::Style> {
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:<color>' (specifies that the color read should be a foreground color)
- 'bg:<color>' (specifies that the color read should be a background color)
- 'underline'
- 'bold'
- '<color>' (see the parse_color_string doc for valid color strings)
*/
fn parse_style_string(style_string: &str) -> Option<ansi_term::Style> {
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<ansi_term::Color> {
// 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::<u8>() {
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))
);
}
}