use crate::configs::StarshipRootConfig; use crate::utils; use ansi_term::{Color, Style}; use indexmap::IndexMap; use std::clone::Clone; use std::collections::HashMap; use std::io::ErrorKind; use std::marker::Sized; use std::env; use toml::Value; /// Root config of a module. pub trait RootModuleConfig<'a> where Self: ModuleConfig<'a>, { /// Create a new root module config with default values. fn new() -> Self; /// Load root module config from given Value and fill unset variables with default /// values. fn load(config: &'a Value) -> Self { if config.get("prompt_order").is_some() { log::warn!("\"prompt_order\" has been removed in favor of \"format\". For more details, see: https://starship.rs/migrating-to-0.45.0/") } Self::new().load_config(config) } /// Helper function that will call RootModuleConfig::load(config) if config is Some, /// or RootModuleConfig::new() if config is None. fn try_load(config: Option<&'a Value>) -> Self { if let Some(config) = config { Self::load(config) } else { Self::new() } } } /// Parsable config. pub trait ModuleConfig<'a> where Self: Sized + Clone, { /// Construct a `ModuleConfig` from a toml value. fn from_config(_config: &'a Value) -> Option { None } /// Merge `self` with config from a toml table. fn load_config(&self, config: &'a Value) -> Self { Self::from_config(config).unwrap_or_else(|| self.clone()) } } // TODO: Add logging to default implementations impl<'a> ModuleConfig<'a> for &'a str { fn from_config(config: &'a Value) -> Option { config.as_str() } } impl<'a> ModuleConfig<'a> for Style { fn from_config(config: &Value) -> Option { parse_style_string(config.as_str()?) } } impl<'a> ModuleConfig<'a> for bool { fn from_config(config: &Value) -> Option { config.as_bool() } } impl<'a> ModuleConfig<'a> for i64 { fn from_config(config: &Value) -> Option { config.as_integer() } } impl<'a> ModuleConfig<'a> for u64 { fn from_config(config: &Value) -> Option { match config { Value::Integer(value) => { // Converting i64 to u64 if *value > 0 { Some(*value as u64) } else { None } } Value::String(value) => value.parse::().ok(), _ => None, } } } impl<'a> ModuleConfig<'a> for f64 { fn from_config(config: &Value) -> Option { config.as_float() } } impl<'a> ModuleConfig<'a> for usize { fn from_config(config: &Value) -> Option { match config { Value::Integer(value) => { if *value > 0 { Some(*value as usize) } else { None } } Value::String(value) => value.parse::().ok(), _ => None, } } } impl<'a, T> ModuleConfig<'a> for Vec where T: ModuleConfig<'a>, { fn from_config(config: &'a Value) -> Option { config .as_array()? .iter() .map(|value| T::from_config(value)) .collect() } } impl<'a, T, S: ::std::hash::BuildHasher + Default> ModuleConfig<'a> for HashMap where T: ModuleConfig<'a>, S: Clone, { fn from_config(config: &'a Value) -> Option { let mut hm = HashMap::default(); for (x, y) in config.as_table()?.iter() { hm.insert(x.clone(), T::from_config(y)?); } Some(hm) } } impl<'a, T, S: ::std::hash::BuildHasher + Default> ModuleConfig<'a> for IndexMap where T: ModuleConfig<'a>, S: Clone, { fn from_config(config: &'a Value) -> Option { let mut im = IndexMap::default(); for (x, y) in config.as_table()?.iter() { im.insert(x.clone(), T::from_config(y)?); } Some(im) } } impl<'a, T> ModuleConfig<'a> for Option where T: ModuleConfig<'a> + Sized, { fn from_config(config: &'a Value) -> Option { Some(T::from_config(config)) } } /// A wrapper around `Vec` that implements `ModuleConfig`, and either /// accepts a value of type `T` or a list of values of type `T`. #[derive(Clone, Default)] pub struct VecOr(pub Vec); impl<'a, T> ModuleConfig<'a> for VecOr where T: ModuleConfig<'a> + Sized, { fn from_config(config: &'a Value) -> Option { if let Some(item) = T::from_config(config) { return Some(VecOr(vec![item])); } let vec = config .as_array()? .iter() .map(|value| T::from_config(value)) .collect::>>()?; Some(VecOr(vec)) } } /// Root config of starship. pub struct StarshipConfig { pub config: Option, } impl StarshipConfig { /// Initialize the Config struct pub fn initialize() -> Self { if let Some(file_data) = Self::config_from_file() { StarshipConfig { config: Some(file_data), } } else { StarshipConfig { config: Some(Value::Table(toml::value::Table::new())), } } } /// Create a config from a starship configuration file fn config_from_file() -> Option { let file_path = if let Ok(path) = env::var("STARSHIP_CONFIG") { // Use $STARSHIP_CONFIG as the config path if available log::debug!("STARSHIP_CONFIG is set: {}", &path); path } else { // Default to using ~/.config/starship.toml log::debug!("STARSHIP_CONFIG is not set"); let config_path = dirs_next::home_dir()?.join(".config/starship.toml"); let config_path_str = config_path.to_str()?.to_owned(); log::debug!("Using default config path: {}", config_path_str); config_path_str }; let toml_content = match utils::read_file(&file_path) { Ok(content) => { log::trace!("Config file content: \"\n{}\"", &content); Some(content) } Err(e) => { let level = if e.kind() == ErrorKind::NotFound { log::Level::Debug } else { log::Level::Error }; log::log!(level, "Unable to read config file content: {}", &e); None } }?; match toml::from_str(&toml_content) { Ok(parsed) => { log::debug!("Config parsed: {:?}", &parsed); Some(parsed) } Err(error) => { log::error!("Unable to parse the config file: {}", error); None } } } /// Get the subset of the table for a module by its name pub fn get_module_config(&self, module_name: &str) -> Option<&Value> { let module_config = self.get_config(&[module_name]); if module_config.is_some() { log::debug!( "Config found for \"{}\": {:?}", &module_name, &module_config ); } module_config } /// Get the value of the config in a specific path pub fn get_config(&self, path: &[&str]) -> Option<&Value> { let mut prev_table = self.config.as_ref()?.as_table()?; assert_ne!( path.len(), 0, "Starship::get_config called with an empty path" ); let (table_options, _) = path.split_at(path.len() - 1); // Assumes all keys except the last in path has a table for option in table_options { match prev_table.get(*option) { Some(value) => match value.as_table() { Some(value) => { prev_table = value; } None => { log::trace!( "No config found for \"{}\": \"{}\" is not a table", path.join("."), &option ); return None; } }, None => { log::trace!( "No config found for \"{}\": Option \"{}\" not found", path.join("."), &option ); return None; } } } let last_option = path.last().unwrap(); let value = prev_table.get(*last_option); if value.is_none() { log::trace!( "No config found for \"{}\": Option \"{}\" not found", path.join("."), &last_option ); }; value } /// Get the subset of the table for a custom module by its name pub fn get_custom_module_config(&self, module_name: &str) -> Option<&Value> { let module_config = self.get_config(&["custom", module_name]); if module_config.is_some() { log::debug!( "Custom config found for \"{}\": {:?}", &module_name, &module_config ); } module_config } /// Get the table of all the registered custom modules, if any pub fn get_custom_modules(&self) -> Option<&toml::value::Table> { self.get_config(&["custom"])?.as_table() } pub fn get_root_config(&self) -> StarshipRootConfig { if let Some(root_config) = &self.config { StarshipRootConfig::load(root_config) } else { StarshipRootConfig::new() } } } /** 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' - 'italic' - '' (see the parse_color_string doc for valid color strings) */ pub fn parse_style_string(style_string: &str) -> Option { style_string .split_whitespace() .fold(Some(ansi_term::Style::new()), |maybe_style, token| { maybe_style.and_then(|style| { let token = token.to_lowercase(); // Check for FG/BG identifiers and strip them off if appropriate // If col_fg is true, color the foreground. If it's false, color the background. let (token, col_fg) = if token.as_str().starts_with("fg:") { (token.trim_start_matches("fg:").to_owned(), true) } else if token.as_str().starts_with("bg:") { (token.trim_start_matches("bg:").to_owned(), false) } else { (token, true) // Bare colors are assumed to color the foreground }; match token.as_str() { "underline" => Some(style.underline()), "bold" => Some(style.bold()), "italic" => Some(style.italic()), "dimmed" => Some(style.dimmed()), // When the string is supposed to be a color: // Decide if we yield none, reset background or set color. color_string => { if color_string == "none" && col_fg { None // fg:none yields no style. } else { // Either bg or valid color or both. let parsed = parse_color_string(color_string); // bg + invalid color = reset the background to default. if !col_fg && parsed.is_none() { let mut new_style = style; new_style.background = Option::None; Some(new_style) } else { // Valid color, apply color to either bg or fg parsed.map(|ansi_color| { if col_fg { style.fg(ansi_color) } else { style.on(ansi_color) } }) } } } } }) }) } /** 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 ); if color_string.len() != 7 { log::debug!("Could not parse hexadecimal string: {}", color_string); return None; } 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); } else { log::debug!("Could not parse color in string: {}", color_string); } predefined_color } #[cfg(test)] mod tests { use super::*; use starship_module_config_derive::ModuleConfig; #[test] fn test_load_config() { #[derive(Clone, ModuleConfig)] struct TestConfig<'a> { pub symbol: &'a str, pub disabled: bool, pub some_array: Vec<&'a str>, } let config = toml::toml! { symbol = "T " disabled = true some_array = ["A"] }; let default_config = TestConfig { symbol: "S ", disabled: false, some_array: vec!["A", "B", "C"], }; let rust_config = default_config.load_config(&config); assert_eq!(rust_config.symbol, "T "); assert_eq!(rust_config.disabled, true); assert_eq!(rust_config.some_array, vec!["A"]); } #[test] fn test_load_nested_config() { #[derive(Clone, ModuleConfig)] struct TestConfig<'a> { pub untracked: SegmentDisplayConfig<'a>, pub modified: SegmentDisplayConfig<'a>, } #[derive(PartialEq, Debug, Clone, ModuleConfig)] struct SegmentDisplayConfig<'a> { pub value: &'a str, pub style: Style, } let config = toml::toml! { untracked.value = "x" modified = { value = "•", style = "red" } }; let default_config = TestConfig { untracked: SegmentDisplayConfig { value: "?", style: Color::Red.bold(), }, modified: SegmentDisplayConfig { value: "!", style: Color::Red.bold(), }, }; let git_status_config = default_config.load_config(&config); assert_eq!( git_status_config.untracked, SegmentDisplayConfig { value: "x", style: Color::Red.bold(), } ); assert_eq!( git_status_config.modified, SegmentDisplayConfig { value: "•", style: Color::Red.normal(), } ); } #[test] fn test_load_optional_config() { #[derive(Clone, ModuleConfig)] struct TestConfig<'a> { pub optional: Option<&'a str>, pub hidden: Option<&'a str>, } let config = toml::toml! { optional = "test" }; let default_config = TestConfig { optional: None, hidden: None, }; let rust_config = default_config.load_config(&config); assert_eq!(rust_config.optional, Some("test")); assert_eq!(rust_config.hidden, None); } #[test] fn test_load_enum_config() { #[derive(Clone, ModuleConfig)] struct TestConfig { pub switch_a: Switch, pub switch_b: Switch, pub switch_c: Switch, } #[derive(Debug, PartialEq, Clone)] enum Switch { ON, OFF, } impl<'a> ModuleConfig<'a> for Switch { fn from_config(config: &'a Value) -> Option { match config.as_str()? { "on" => Some(Self::ON), "off" => Some(Self::OFF), _ => None, } } } let config = toml::toml! { switch_a = "on" switch_b = "any" }; let default_config = TestConfig { switch_a: Switch::OFF, switch_b: Switch::OFF, switch_c: Switch::OFF, }; let rust_config = default_config.load_config(&config); assert_eq!(rust_config.switch_a, Switch::ON); assert_eq!(rust_config.switch_b, Switch::OFF); assert_eq!(rust_config.switch_c, Switch::OFF); } #[test] fn test_from_string() { let config = Value::String(String::from("S")); assert_eq!(<&str>::from_config(&config).unwrap(), "S"); } #[test] fn test_from_bool() { let config = Value::Boolean(true); assert_eq!(::from_config(&config).unwrap(), true); } #[test] fn test_from_i64() { let config = Value::Integer(42); assert_eq!(::from_config(&config).unwrap(), 42); } #[test] fn test_from_style() { let config = Value::from("red bold"); assert_eq!(