Skip to content

Commit

Permalink
cli: allow colors in form #rrggbb
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tomafro committed Mar 18, 2024
1 parent 3ffe3d9 commit 933150d
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 15 additions & 1 deletion cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
"type": "object",
"description": "Mapping from jj formatter labels to colors",
"definitions": {
"colors": {
"colorNames": {
"enum": [
"default",
"black",
Expand All @@ -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",
Expand Down
81 changes: 73 additions & 8 deletions cli/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ fn rules_from_config(config: &config::Config) -> Result<Rules, config::ConfigErr
match value.kind {
config::ValueKind::String(color_name) => {
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,
Expand All @@ -344,12 +344,12 @@ fn rules_from_config(config: &config::Config) -> Result<Rules, config::ConfigErr
let mut style = Style::default();
if let Some(value) = style_table.get("fg") {
if let config::ValueKind::String(color_name) = &value.kind {
style.fg_color = Some(color_for_name(color_name)?);
style.fg_color = Some(color_for_name_or_hex(color_name)?);
}
}
if let Some(value) = style_table.get("bg") {
if let config::ValueKind::String(color_name) = &value.kind {
style.bg_color = Some(color_for_name(color_name)?);
style.bg_color = Some(color_for_name_or_hex(color_name)?);
}
}
if let Some(value) = style_table.get("bold") {
Expand All @@ -370,8 +370,8 @@ fn rules_from_config(config: &config::Config) -> Result<Rules, config::ConfigErr
Ok(result)
}

fn color_for_name(color_name: &str) -> Result<Color, config::ConfigError> {
match color_name {
fn color_for_name_or_hex(name_or_hex: &str) -> Result<Color, config::ConfigError> {
match name_or_hex {
"default" => Ok(Color::Reset),
"black" => Ok(Color::Black),
"red" => Ok(Color::DarkRed),
Expand All @@ -389,9 +389,25 @@ fn color_for_name(color_name: &str) -> Result<Color, config::ConfigError> {
"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<Color> {
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
}
}

Expand Down Expand Up @@ -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<u8> = 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
Expand Down Expand Up @@ -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<u8> = 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
Expand Down
10 changes: 8 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 933150d

Please sign in to comment.