diff options
author | Kevin Song <chipbuster@users.noreply.github.com> | 2019-09-07 19:33:06 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-09-07 19:33:06 -0500 |
commit | 9721666d331ddfd7a04bbb434cccf38b44701b00 (patch) | |
tree | 8edb32e1cebc6951451953a311cd9cfb4447712d /src | |
parent | 3e5cac98522feca856867e32cf3df52ff0285c62 (diff) |
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.
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 199 | ||||
-rw-r--r-- | src/module.rs | 5 | ||||
-rw-r--r-- | src/modules/battery.rs | 5 | ||||
-rw-r--r-- | src/modules/character.rs | 18 | ||||
-rw-r--r-- | src/modules/cmd_duration.rs | 4 | ||||
-rw-r--r-- | src/modules/directory.rs | 4 | ||||
-rw-r--r-- | src/modules/git_branch.rs | 6 | ||||
-rw-r--r-- | src/modules/git_state.rs | 5 | ||||
-rw-r--r-- | src/modules/git_status.rs | 5 | ||||
-rw-r--r-- | src/modules/golang.rs | 6 | ||||
-rw-r--r-- | src/modules/hostname.rs | 8 | ||||
-rw-r--r-- | src/modules/jobs.rs | 7 | ||||
-rw-r--r-- | src/modules/nix_shell.rs | 6 | ||||
-rw-r--r-- | src/modules/nodejs.rs | 6 | ||||
-rw-r--r-- | src/modules/package.rs | 6 | ||||
-rw-r--r-- | src/modules/python.rs | 4 | ||||
-rw-r--r-- | src/modules/ruby.rs | 6 | ||||
-rw-r--r-- | src/modules/rust.rs | 6 | ||||
-rw-r--r-- | src/modules/username.rs | 25 |
19 files changed, 285 insertions, 46 deletions
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<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)) + ); + } } 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<bool> { 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<Style> { + self.config.and_then(|config| config.get_as_ansi_style(key)) + } } impl<'a> fmt::Display for Module<'a> { diff --git a/src/modules/battery.rs b/src/modules/battery.rs index 800fb6d23..47c9eee73 100644 --- a/src/modules/battery.rs +++ b/src/modules/battery.rs @@ -30,7 +30,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { // TODO: Set style based on percentage when threshold is modifiable let mut module = context.new_module("battery")?; - module.set_style(Color::Red.bold()); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); + module.set_style(module_style); module.get_prefix().set_value(""); match state { diff --git a/src/modules/character.rs b/src/modules/character.rs index 9c4d7e982..704fe5c9f 100644 --- a/src/modules/character.rs +++ b/src/modules/character.rs @@ -5,10 +5,10 @@ use ansi_term::Color; /// /// The character segment prints an arrow character in a color dependant on the exit- /// code of the last executed command: -/// - If the exit-code was "0", the arrow will be formatted with `COLOR_SUCCESS` +/// - If the exit-code was "0", the arrow will be formatted with `style_success` /// (green by default) /// - If the exit-code was anything else, the arrow will be formatted with -/// `COLOR_FAILURE` (red by default) +/// `style_failure` (red by default) pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { const SUCCESS_CHAR: &str = "❯"; const FAILURE_CHAR: &str = "✖"; @@ -20,12 +20,16 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { const ASSUMED_MODE: ShellEditMode = ShellEditMode::Insert; // TODO: extend config to more modes - let color_success = Color::Green.bold(); - let color_failure = Color::Red.bold(); - let mut module = context.new_module("character")?; module.get_prefix().set_value(""); + let style_success = module + .config_value_style("style_success") + .unwrap_or_else(|| Color::Green.bold()); + let style_failure = module + .config_value_style("style_failure") + .unwrap_or_else(|| Color::Red.bold()); + let arguments = &context.arguments; let use_symbol = module .config_value_bool("use_symbol_for_status") @@ -56,9 +60,9 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { }; if exit_success { - symbol.set_style(color_success.bold()); + symbol.set_style(style_success); } else { - symbol.set_style(color_failure.bold()); + symbol.set_style(style_failure); }; Some(module) diff --git a/src/modules/cmd_duration.rs b/src/modules/cmd_duration.rs index 2f9e0c32e..ac7102284 100644 --- a/src/modules/cmd_duration.rs +++ b/src/modules/cmd_duration.rs @@ -32,7 +32,9 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let module_color = match elapsed { time if time < config_min => return None, - _ => Color::Yellow.bold(), + _ => module + .config_value_style("style") + .unwrap_or_else(|| Color::Yellow.bold()), }; module.set_style(module_color); diff --git a/src/modules/directory.rs b/src/modules/directory.rs index 8c12db59a..952701262 100644 --- a/src/modules/directory.rs +++ b/src/modules/directory.rs @@ -17,9 +17,11 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { const HOME_SYMBOL: &str = "~"; const DIR_TRUNCATION_LENGTH: i64 = 3; const FISH_STYLE_PWD_DIR_LENGTH: i64 = 0; - let module_color = Color::Cyan.bold(); let mut module = context.new_module("directory")?; + let module_color = module + .config_value_style("style") + .unwrap_or_else(|| Color::Cyan.bold()); module.set_style(module_color); let truncation_length = module diff --git a/src/modules/git_branch.rs b/src/modules/git_branch.rs index 120c58d4e..d6b64c4e0 100644 --- a/src/modules/git_branch.rs +++ b/src/modules/git_branch.rs @@ -9,9 +9,11 @@ use super::{Context, Module}; pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { const GIT_BRANCH_CHAR: &str = " "; - let segment_color = Color::Purple.bold(); - let mut module = context.new_module("git_branch")?; + + let segment_color = module + .config_value_style("style") + .unwrap_or_else(|| Color::Purple.bold()); module.set_style(segment_color); module.get_prefix().set_value("on "); diff --git a/src/modules/git_state.rs b/src/modules/git_state.rs index 55f3d4c11..8d242b0ca 100644 --- a/src/modules/git_state.rs +++ b/src/modules/git_state.rs @@ -19,9 +19,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { return None; } + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Yellow.bold()); + module.set_style(module_style); module.get_prefix().set_value("("); module.get_suffix().set_value(") "); - module.set_style(Color::Yellow.bold()); let label = match state_description { StateDescription::Label(label) => label, diff --git a/src/modules/git_status.rs b/src/modules/git_status.rs index 646f01f07..aa82fb739 100644 --- a/src/modules/git_status.rs +++ b/src/modules/git_status.rs @@ -34,10 +34,11 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let repo_root = context.repo_root.as_ref()?; let repository = Repository::open(repo_root).ok()?; - let module_style = Color::Red.bold(); let mut module = context.new_module("git_status")?; - let show_sync_count = module.config_value_bool("show_sync_count").unwrap_or(false); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); module.get_prefix().set_value("[").set_style(module_style); module.get_suffix().set_value("] ").set_style(module_style); diff --git a/src/modules/golang.rs b/src/modules/golang.rs index 0114c30de..bf632bf72 100644 --- a/src/modules/golang.rs +++ b/src/modules/golang.rs @@ -28,10 +28,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { match get_go_version() { Some(go_version) => { const GO_CHAR: &str = "🐹 "; - let module_color = Color::Cyan.bold(); let mut module = context.new_module("golang")?; - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Cyan.bold()); + module.set_style(module_style); let formatted_version = format_go_version(&go_version)?; module.new_segment("symbol", GO_CHAR); diff --git a/src/modules/hostname.rs b/src/modules/hostname.rs index 51567bcbb..9b0690d8f 100644 --- a/src/modules/hostname.rs +++ b/src/modules/hostname.rs @@ -1,6 +1,5 @@ -use ansi_term::{Color, Style}; +use ansi_term::Color; use std::env; -use std::process::Command; use super::{Context, Module}; use std::ffi::OsString; @@ -12,6 +11,9 @@ use std::ffi::OsString; /// - hostname.ssh_only is false OR the user is currently connected as an SSH session (`$SSH_CONNECTION`) pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let mut module = context.new_module("hostname")?; + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Green.bold().dimmed()); let ssh_connection = env::var("SSH_CONNECTION").ok(); if module.config_value_bool("ssh_only").unwrap_or(true) && ssh_connection.is_none() { @@ -31,7 +33,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let prefix = module.config_value_str("prefix").unwrap_or("").to_owned(); let suffix = module.config_value_str("suffix").unwrap_or("").to_owned(); - module.set_style(Color::Green.bold().dimmed()); + module.set_style(module_style); module.new_segment("hostname", &format!("{}{}{}", prefix, host, suffix)); module.get_prefix().set_value("on "); diff --git a/src/modules/jobs.rs b/src/modules/jobs.rs index 649c2f8b0..bb8a2d7d4 100644 --- a/src/modules/jobs.rs +++ b/src/modules/jobs.rs @@ -9,9 +9,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let threshold = module.config_value_i64("threshold").unwrap_or(1); const JOB_CHAR: &str = "✦"; - let module_color = Color::Blue.bold(); - - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Blue.bold()); + module.set_style(module_style); let arguments = &context.arguments; let num_of_jobs = arguments diff --git a/src/modules/nix_shell.rs b/src/modules/nix_shell.rs index b37fcdcf4..5fc98dfc9 100644 --- a/src/modules/nix_shell.rs +++ b/src/modules/nix_shell.rs @@ -46,8 +46,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { } }) .map(|segment| { - let module_color = Color::Red.bold(); - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); + module.set_style(module_style); module.new_segment("nix_shell", &segment); module }) diff --git a/src/modules/nodejs.rs b/src/modules/nodejs.rs index c7029d88c..8f8219c7a 100644 --- a/src/modules/nodejs.rs +++ b/src/modules/nodejs.rs @@ -24,10 +24,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { match get_node_version() { Some(node_version) => { const NODE_CHAR: &str = "⬢ "; - let module_color = Color::Green.bold(); let mut module = context.new_module("nodejs")?; - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Green.bold()); + module.set_style(module_style); let formatted_version = node_version.trim(); module.new_segment("symbol", NODE_CHAR); diff --git a/src/modules/package.rs b/src/modules/package.rs index c459323d9..ceb34bc6c 100644 --- a/src/modules/package.rs +++ b/src/modules/package.rs @@ -12,10 +12,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { match get_package_version() { Some(package_version) => { const PACKAGE_CHAR: &str = "📦 "; - let module_color = Color::Red.bold(); let mut module = context.new_module("package")?; - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); + module.set_style(module_style); module.get_prefix().set_value("is "); module.new_segment("symbol", PACKAGE_CHAR); diff --git a/src/modules/python.rs b/src/modules/python.rs index e12511633..61d8189db 100644 --- a/src/modules/python.rs +++ b/src/modules/python.rs @@ -36,7 +36,9 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { .unwrap_or(false); const PYTHON_CHAR: &str = "🐍 "; - let module_color = Color::Yellow.bold(); + let module_color = module + .config_value_style("style") + .unwrap_or_else(|| Color::Yellow.bold()); module.set_style(module_color); module.new_segment("symbol", PYTHON_CHAR); diff --git a/src/modules/ruby.rs b/src/modules/ruby.rs index ff3945360..a39bda75c 100644 --- a/src/modules/ruby.rs +++ b/src/modules/ruby.rs @@ -22,10 +22,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { match get_ruby_version() { Some(ruby_version) => { const RUBY_CHAR: &str = "💎 "; - let module_color = Color::Red.bold(); let mut module = context.new_module("ruby")?; - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); + module.set_style(module_style); let formatted_version = format_ruby_version(&ruby_version)?; module.new_segment("symbol", RUBY_CHAR); diff --git a/src/modules/rust.rs b/src/modules/rust.rs index c94a34d8b..401f6e4ce 100644 --- a/src/modules/rust.rs +++ b/src/modules/rust.rs @@ -22,10 +22,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { match get_rust_version() { Some(rust_version) => { const RUST_CHAR: &str = "🦀 "; - let module_color = Color::Red.bold(); let mut module = context.new_module("rust")?; - module.set_style(module_color); + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Red.bold()); + module.set_style(module_style); let formatted_version = format_rustc_version(rust_version); module.new_segment("symbol", RUST_CHAR); diff --git a/src/modules/username.rs b/src/modules/username.rs index cda223652..07ecc5b14 100644 --- a/src/modules/username.rs +++ b/src/modules/username.rs @@ -15,11 +15,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { let logname = env::var("LOGNAME").ok(); let ssh_connection = env::var("SSH_CONNECTION").ok(); - let mut module_color = Color::Yellow.bold(); - - if user != logname || ssh_connection.is_some() || is_root(&mut module_color) { + const ROOT_UID: Option<u32> = Some(0); + let user_uid = get_uid(); + if user != logname || ssh_connection.is_some() || user_uid == ROOT_UID { let mut module = context.new_module("username")?; - module.set_style(module_color); + let module_style = get_mod_style(user_uid, &module); + module.set_style(module_style); module.new_segment("username", &user?); return Some(module); @@ -37,13 +38,13 @@ fn get_uid() -> Option<u32> { } } -fn is_root(style: &mut Style) -> bool { - match get_uid() { - Some(uid) if uid == 0 => { - style.clone_from(&Color::Red.bold()); - - true - } - _ => false, +fn get_mod_style(user_uid: Option<u32>, module: &Module) -> Style { + match user_uid { + Some(0) => module + .config_value_style("style_root") + .unwrap_or_else(|| Color::Red.bold()), + _ => module + .config_value_style("style_user") + .unwrap_or_else(|| Color::Yellow.bold()), } } |