From 933150d819c92ddcb4e1712f5b834dd1b7254083 Mon Sep 17 00:00:00 2001 From: Tom Ward Date: Sat, 16 Mar 2024 08:45:40 +0000 Subject: [PATCH] cli: allow colors in form #rrggbb Changes the formatter to accept not only existing color names (such as "red" or "green") but also those in the form #rrggbb, where rr, gg, and bb are two-digit hexadecimal numbers. This allows much finer control over colors used. --- CHANGELOG.md | 2 + cli/src/config-schema.json | 16 +++++++- cli/src/formatter.rs | 81 ++++++++++++++++++++++++++++++++++---- docs/config.md | 10 ++++- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c271987..1ac76aa69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* Config now supports rgb hex colors (in the form `#rrggbb`) wherever existing color names are supported. + * `ui.default-command` now accepts multiple string arguments, for more complex default `jj` commands. diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 8bbc7fa68..5d96d03ab 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -162,7 +162,7 @@ "type": "object", "description": "Mapping from jj formatter labels to colors", "definitions": { - "colors": { + "colorNames": { "enum": [ "default", "black", @@ -183,6 +183,20 @@ "bright white" ] }, + "hexColor": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "colors": { + "oneOf": [ + { + "$ref": "#/properties/colors/definitions/colorNames" + }, + { + "$ref": "#/properties/colors/definitions/hexColor" + } + ] + }, "basicFormatterLabels": { "enum": [ "description", diff --git a/cli/src/formatter.rs b/cli/src/formatter.rs index a215f87fa..38e9d5892 100644 --- a/cli/src/formatter.rs +++ b/cli/src/formatter.rs @@ -333,7 +333,7 @@ fn rules_from_config(config: &config::Config) -> Result { let style = Style { - fg_color: Some(color_for_name(&color_name)?), + fg_color: Some(color_for_name_or_hex(&color_name)?), bg_color: None, bold: None, underlined: None, @@ -344,12 +344,12 @@ fn rules_from_config(config: &config::Config) -> Result Result Result { - match color_name { +fn color_for_name_or_hex(name_or_hex: &str) -> Result { + match name_or_hex { "default" => Ok(Color::Reset), "black" => Ok(Color::Black), "red" => Ok(Color::DarkRed), @@ -389,9 +389,25 @@ fn color_for_name(color_name: &str) -> Result { "bright magenta" => Ok(Color::Magenta), "bright cyan" => Ok(Color::Cyan), "bright white" => Ok(Color::White), - _ => Err(config::ConfigError::Message(format!( - "invalid color: {color_name}" - ))), + _ => color_for_hex(name_or_hex) + .ok_or_else(|| config::ConfigError::Message(format!("invalid color: {}", name_or_hex))), + } +} + +fn color_for_hex(color: &str) -> Option { + if color.len() == 7 + && color.starts_with('#') + && color[1..].chars().all(|c| c.is_ascii_hexdigit()) + { + let r = u8::from_str_radix(&color[1..3], 16); + let g = u8::from_str_radix(&color[3..5], 16); + let b = u8::from_str_radix(&color[5..7], 16); + match (r, g, b) { + (Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb { r, g, b }), + _ => None, + } + } else { + None } } @@ -676,6 +692,38 @@ mod tests { "###); } + #[test] + fn test_color_formatter_hex_colors() { + // Test the color code for each color. + let labels_and_colors = [ + ["black", "#000000"], + ["white", "#ffffff"], + ["pastel-blue", "#AFE0D9"], + ]; + let mut config_builder = config::Config::builder(); + for [label, color] in labels_and_colors { + // Use the color name as the label. + config_builder = config_builder + .set_override(format!("colors.{}", label), color) + .unwrap(); + } + let mut output: Vec = vec![]; + let mut formatter = + ColorFormatter::for_config(&mut output, &config_builder.build().unwrap()).unwrap(); + for [label, _] in labels_and_colors { + formatter.push_label(&label.replace(' ', "-")).unwrap(); + formatter.write_str(&format!(" {label} ")).unwrap(); + formatter.pop_label().unwrap(); + formatter.write_str("\n").unwrap(); + } + drop(formatter); + insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###" +  black  +  white  +  pastel-blue  + "###); + } + #[test] fn test_color_formatter_single_label() { // Test that a single label can be colored and that the color is reset @@ -879,6 +927,23 @@ mod tests { @"invalid color: bloo"); } + #[test] + fn test_color_formatter_unrecognized_hex_color() { + // An unrecognized hex color causes an error. + let config = config_from_string( + r##" + colors."outer" = "red" + colors."outer inner" = "#ffgggg" + "##, + ); + let mut output: Vec = vec![]; + let err = ColorFormatter::for_config(&mut output, &config) + .unwrap_err() + .to_string(); + insta::assert_snapshot!(err, + @"invalid color: #ffgggg"); + } + #[test] fn test_color_formatter_normal_color() { // The "default" color resets the color. It is possible to reset only the diff --git a/docs/config.md b/docs/config.md index c30dd49c5..b7a8dcde7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -113,12 +113,18 @@ All of them but "default" come in a bright version too, e.g. "bright red". The "default" color can be used to override a color defined by a parent style (explained below). -If you use a string value for a color, as in the example above, it will be used +You can also use a 6-digit hex code for more control over the exact color used: + +```toml +colors.change_id = "#ff1525" +``` + +If you use a string value for a color, as in the examples above, it will be used for the foreground color. You can also set the background color, or make the text bold or underlined. For that, you need to use a table: ```toml -colors.commit_id = { fg = "green", bg = "red", bold = true, underline = true } +colors.commit_id = { fg = "green", bg = "#ff1525", bold = true, underline = true } ``` The key names are called "labels". The above used `commit_id` as label. You can