feat: add user-defined color palette (#4209)

* docs(config): add color palette to docs

* feat: add user-defined color palette

* fix: update config schema

* refactor: apply suggestions from code review

Co-authored-by: David Knaack <davidkna@users.noreply.github.com>

* fix: update new test

* feat: add support for multiple palettes

* docs(config): update docs for multiple color palettes

* docs(config): fix formatting

* test: test overriding a predefined color with itself

* docs: mention palettes cannot reference themselves

* refactor: warn when using a nonexistent palette

* test: test retrieving a nonexistent color palette

* fix: fix issues with palette log messages

* fix: update config schema

* fix: skip serializing palette if none

* refactor: change nonexistent palette message to warning

* fix: update config schema

Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
This commit is contained in:
Anomalocaridid 2022-09-27 17:08:52 -04:00 committed by GitHub
parent f9947d9f14
commit d93074d056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 15 deletions

View File

@ -1544,6 +1544,22 @@
"add_newline": { "add_newline": {
"default": true, "default": true,
"type": "boolean" "type": "boolean"
},
"palette": {
"type": [
"string",
"null"
]
},
"palettes": {
"default": {},
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -176,13 +176,15 @@ This is the list of prompt-wide configuration options.
### Options ### Options
| Option | Default | Description | | Option | Default | Description |
| ----------------- | ------------------------------ | ---------------------------------------------------------------- | | ----------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format` | [link](#default-prompt-format) | Configure the format of the prompt. | | `format` | [link](#default-prompt-format) | Configure the format of the prompt. |
| `right_format` | `""` | See [Enable Right Prompt](/advanced-config/#enable-right-prompt) | | `right_format` | `""` | See [Enable Right Prompt](/advanced-config/#enable-right-prompt) |
| `scan_timeout` | `30` | Timeout for starship to scan files (in milliseconds). | | `scan_timeout` | `30` | Timeout for starship to scan files (in milliseconds). |
| `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). | | `command_timeout` | `500` | Timeout for commands executed by starship (in milliseconds). |
| `add_newline` | `true` | Inserts blank line between shell prompts. | | `add_newline` | `true` | Inserts blank line between shell prompts. |
| `palette` | `""` | Sets which color palette from `palettes` to use. |
| `palettes` | `{}` | Collection of color palettes that assign [colors](/advanced-config/#style-strings) to user-defined names. Note that color palettes cannot reference their own color definitions. |
### Example ### Example
@ -200,6 +202,16 @@ scan_timeout = 10
# Disable the blank line at the start of the prompt # Disable the blank line at the start of the prompt
add_newline = false add_newline = false
# Set "foo" as custom color palette
palette = "foo"
# Define custom colors
[palettes.foo]
# Overwrite existing color
blue = "21"
# Define new color
mustard = "#af8700"
``` ```
### Default Prompt Format ### Default Prompt Format

View File

@ -1,3 +1,5 @@
use crate::configs::Palette;
use crate::context::Context;
use crate::serde_utils::ValueDeserializer; use crate::serde_utils::ValueDeserializer;
use crate::utils; use crate::utils;
use nu_ansi_term::Color; use nu_ansi_term::Color;
@ -7,6 +9,7 @@ use serde::{
use std::borrow::Cow; use std::borrow::Cow;
use std::clone::Clone; use std::clone::Clone;
use std::collections::HashMap;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::env; use std::env;
@ -260,7 +263,7 @@ where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Cow::<'_, str>::deserialize(de).and_then(|s| { Cow::<'_, str>::deserialize(de).and_then(|s| {
parse_style_string(s.as_ref()).ok_or_else(|| D::Error::custom("Invalid style string")) parse_style_string(s.as_ref(), None).ok_or_else(|| D::Error::custom("Invalid style string"))
}) })
} }
@ -275,7 +278,10 @@ where
- 'blink' - 'blink'
- '<color>' (see the `parse_color_string` doc for valid color strings) - '<color>' (see the `parse_color_string` doc for valid color strings)
*/ */
pub fn parse_style_string(style_string: &str) -> Option<nu_ansi_term::Style> { pub fn parse_style_string(
style_string: &str,
context: Option<&Context>,
) -> Option<nu_ansi_term::Style> {
style_string style_string
.split_whitespace() .split_whitespace()
.fold(Some(nu_ansi_term::Style::new()), |maybe_style, token| { .fold(Some(nu_ansi_term::Style::new()), |maybe_style, token| {
@ -308,7 +314,15 @@ pub fn parse_style_string(style_string: &str) -> Option<nu_ansi_term::Style> {
None // fg:none yields no style. None // fg:none yields no style.
} else { } else {
// Either bg or valid color or both. // Either bg or valid color or both.
let parsed = parse_color_string(color_string); let parsed = parse_color_string(
color_string,
context.and_then(|x| {
get_palette(
&x.root_config.palettes,
x.root_config.palette.as_deref(),
)
}),
);
// bg + invalid color = reset the background to default. // bg + invalid color = reset the background to default.
if !col_fg && parsed.is_none() { if !col_fg && parsed.is_none() {
let mut new_style = style; let mut new_style = style;
@ -335,9 +349,12 @@ pub fn parse_style_string(style_string: &str) -> Option<nu_ansi_term::Style> {
There are three valid color formats: There are three valid color formats:
- #RRGGBB (a hash followed by an RGB hex) - #RRGGBB (a hash followed by an RGB hex)
- u8 (a number from 0-255, representing an ANSI color) - u8 (a number from 0-255, representing an ANSI color)
- colstring (one of the 16 predefined color strings) - colstring (one of the 16 predefined color strings or a custom user-defined color)
*/ */
fn parse_color_string(color_string: &str) -> Option<nu_ansi_term::Color> { fn parse_color_string(
color_string: &str,
palette: Option<&Palette>,
) -> Option<nu_ansi_term::Color> {
// Parse RGB hex values // Parse RGB hex values
log::trace!("Parsing color_string: {}", color_string); log::trace!("Parsing color_string: {}", color_string);
if color_string.starts_with('#') { if color_string.starts_with('#') {
@ -362,6 +379,16 @@ fn parse_color_string(color_string: &str) -> Option<nu_ansi_term::Color> {
return Some(Color::Fixed(ansi_color_num)); return Some(Color::Fixed(ansi_color_num));
} }
// Check palette for a matching user-defined color
if let Some(palette_color) = palette.as_ref().and_then(|x| x.get(color_string)) {
log::trace!(
"Read user-defined color string: {} defined as {}",
color_string,
palette_color
);
return parse_color_string(palette_color, None);
}
// Check for any predefined color strings // Check for any predefined color strings
// There are no predefined enums for bright colors, so we use Color::Fixed // There are no predefined enums for bright colors, so we use Color::Fixed
let predefined_color = match color_string.to_lowercase().as_str() { let predefined_color = match color_string.to_lowercase().as_str() {
@ -392,6 +419,24 @@ fn parse_color_string(color_string: &str) -> Option<nu_ansi_term::Color> {
predefined_color predefined_color
} }
fn get_palette<'a>(
palettes: &'a HashMap<String, Palette>,
palette_name: Option<&str>,
) -> Option<&'a Palette> {
if let Some(palette_name) = palette_name {
let palette = palettes.get(palette_name);
if palette.is_some() {
log::trace!("Found color palette: {}", palette_name);
} else {
log::warn!("Could not find color palette: {}", palette_name);
}
palette
} else {
log::trace!("No color palette specified, using defaults");
None
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -778,4 +823,76 @@ mod tests {
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127)) Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
); );
} }
#[test]
fn table_get_colors_palette() {
// Test using colors defined in palette
let mut palette = Palette::new();
palette.insert("mustard".to_string(), "#af8700".to_string());
palette.insert("sky-blue".to_string(), "51".to_string());
palette.insert("red".to_string(), "#d70000".to_string());
palette.insert("blue".to_string(), "17".to_string());
palette.insert("green".to_string(), "green".to_string());
assert_eq!(
parse_color_string("mustard", Some(&palette)),
Some(Color::Rgb(175, 135, 0))
);
assert_eq!(
parse_color_string("sky-blue", Some(&palette)),
Some(Color::Fixed(51))
);
// Test overriding predefined colors
assert_eq!(
parse_color_string("red", Some(&palette)),
Some(Color::Rgb(215, 0, 0))
);
assert_eq!(
parse_color_string("blue", Some(&palette)),
Some(Color::Fixed(17))
);
// Test overriding a predefined color with itself
assert_eq!(
parse_color_string("green", Some(&palette)),
Some(Color::Green)
)
}
#[test]
fn table_get_palette() {
// Test retrieving color palette by name
let mut palette1 = Palette::new();
palette1.insert("test-color".to_string(), "123".to_string());
let mut palette2 = Palette::new();
palette2.insert("test-color".to_string(), "#ABCDEF".to_string());
let mut palettes = HashMap::<String, Palette>::new();
palettes.insert("palette1".to_string(), palette1);
palettes.insert("palette2".to_string(), palette2);
assert_eq!(
get_palette(&palettes, Some("palette1"))
.unwrap()
.get("test-color")
.unwrap(),
"123"
);
assert_eq!(
get_palette(&palettes, Some("palette2"))
.unwrap()
.get("test-color")
.unwrap(),
"#ABCDEF"
);
// Test retrieving nonexistent color palette
assert!(get_palette(&palettes, Some("palette3")).is_none());
// Test default behavior
assert!(get_palette(&palettes, None).is_none());
}
} }

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[cfg_attr( #[cfg_attr(
@ -16,8 +17,13 @@ pub struct StarshipRootConfig {
pub scan_timeout: u64, pub scan_timeout: u64,
pub command_timeout: u64, pub command_timeout: u64,
pub add_newline: bool, pub add_newline: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub palette: Option<String>,
pub palettes: HashMap<String, Palette>,
} }
pub type Palette = HashMap<String, String>;
// List of default prompt order // List of default prompt order
// NOTE: If this const value is changed then Default prompt order subheading inside // NOTE: If this const value is changed then Default prompt order subheading inside
// prompt heading of config docs needs to be updated according to changes made here. // prompt heading of config docs needs to be updated according to changes made here.
@ -114,6 +120,8 @@ impl Default for StarshipRootConfig {
scan_timeout: 30, scan_timeout: 30,
command_timeout: 500, command_timeout: 500,
add_newline: true, add_newline: true,
palette: None,
palettes: HashMap::default(),
} }
} }
} }

View File

@ -245,7 +245,7 @@ impl<'a> StringFormatter<'a> {
style_variables: &'a StyleVariableMapType<'a>, style_variables: &'a StyleVariableMapType<'a>,
context: Option<&Context>, context: Option<&Context>,
) -> Result<Vec<Segment>, StringFormatterError> { ) -> Result<Vec<Segment>, StringFormatterError> {
let style = parse_style(textgroup.style, style_variables); let style = parse_style(textgroup.style, style_variables, context);
parse_format( parse_format(
textgroup.format, textgroup.format,
style.transpose()?, style.transpose()?,
@ -258,6 +258,7 @@ impl<'a> StringFormatter<'a> {
fn parse_style<'a>( fn parse_style<'a>(
style: Vec<StyleElement>, style: Vec<StyleElement>,
variables: &'a StyleVariableMapType<'a>, variables: &'a StyleVariableMapType<'a>,
context: Option<&Context>,
) -> Option<Result<Style, StringFormatterError>> { ) -> Option<Result<Style, StringFormatterError>> {
let style_strings = style let style_strings = style
.into_iter() .into_iter()
@ -276,7 +277,7 @@ impl<'a> StringFormatter<'a> {
.map(|style_strings| { .map(|style_strings| {
let style_string: String = let style_string: String =
style_strings.iter().flat_map(|s| s.chars()).collect(); style_strings.iter().flat_map(|s| s.chars()).collect();
parse_style_string(&style_string) parse_style_string(&style_string, context)
}) })
.transpose() .transpose()
} }

View File

@ -14,7 +14,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
return None; return None;
} }
let style = parse_style_string(config.style); let style = parse_style_string(config.style, Some(context));
module.set_segments(vec![Segment::fill(style, config.symbol)]); module.set_segments(vec![Segment::fill(style, config.symbol)]);