ok/jj
1
0
Fork 0
forked from mirrors/jj

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.
This commit is contained in:
Tom Ward 2024-03-16 08:45:40 +00:00
parent 3ffe3d9ed8
commit 933150d819
4 changed files with 98 additions and 11 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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,
@ -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") {
@ -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),
@ -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
}
}
@ -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
@ -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

View file

@ -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