From 77276c6babc7991b0e3e0e0ab322061e6a1e78f9 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 --- CHANGELOG.md | 17 ++++--- cli/src/config-schema.json | 16 ++++++- cli/src/formatter.rs | 92 ++++++++++++++++++++++++++++++++++---- docs/config.md | 16 ++++--- 4 files changed, 120 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c271987f..2da65689cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* cli now supports rgb hex colors (in the form `#rrggbb`) wherev +er existing color names are supported. + * `ui.default-command` now accepts multiple string arguments, for more complex default `jj` commands. @@ -133,7 +136,7 @@ No code changes (fixing Rust `Cargo.toml` stuff). When symlink support is unavailable, they will be materialized as regular files in the working copy (instead of resulting in a crash). [#2](https://github.com/martinvonz/jj/issues/2) - + * On Windows, the `:builtin` pager is now used by default, rather than being disabled entirely. @@ -180,7 +183,7 @@ Thanks to the people who made this release happen! copy commit on top of a single specified revision, i.e. with one parent. `merge` creates a new working copy commit on top of *at least* two specified revisions, i.e. with two or more parents. - + The only difference between these commands and `jj new`, which *also* creates a new working copy commit, is that `new` can create a working copy commit on top of any arbitrary number of revisions, so it can handle both the previous @@ -337,7 +340,7 @@ Thanks to the people who made this release happen! * `jj branch set` no longer creates a new branch. Use `jj branch create` instead. - + * `jj init --git` in an existing Git repository now errors and exits rather than creating a second Git store. @@ -501,8 +504,8 @@ Thanks to the people who made this release happen! ### New features -* The `ancestors()` revset function now takes an optional `depth` argument - to limit the depth of the ancestor set. For example, use `jj log -r +* The `ancestors()` revset function now takes an optional `depth` argument + to limit the depth of the ancestor set. For example, use `jj log -r 'ancestors(@, 5)` to view the last 5 commits. * Support for the Watchman filesystem monitor is now bundled by default. Set @@ -667,13 +670,13 @@ Thanks to the people who made this release happen! respectively. * `jj log` timestamp format now accepts `.utc()` to convert a timestamp to UTC. - + * templates now support additional string methods `.starts_with(x)`, `.ends_with(x)` `.remove_prefix(x)`, `.remove_suffix(x)`, and `.substr(start, end)`. * `jj next` and `jj prev` are added, these allow you to traverse the history in a linear style. For people coming from Sapling and `git-branchles` - see [#2126](https://github.com/martinvonz/jj/issues/2126) for + see [#2126](https://github.com/martinvonz/jj/issues/2126) for further pending improvements. * `jj diff --stat` has been implemented. It shows a histogram of the changes, diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 8bbc7fa68e..5d96d03aba 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 a215f87fa2..e6d443f24b 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,36 @@ 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}" - ))), + _ => { + if name_or_hex.len() == 7 && name_or_hex.starts_with('#') { + color_for_hex(name_or_hex) + } else { + Err(config::ConfigError::Message(format!( + "invalid color: {}", + name_or_hex + ))) + } + } + } +} + +fn color_for_hex(color: &str) -> Result { + if color.len() == 7 && color.starts_with('#') { + 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)) => Ok(Color::Rgb { r, g, b }), + _ => Err(config::ConfigError::Message(format!( + "invalid color: {}", + color + ))), + } + } else { + Err(config::ConfigError::Message(format!( + "invalid color: {}", + color + ))) } } @@ -676,6 +703,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.replace(' ', "-")), 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 +938,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 c30dd49c54..aa25989f52 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 @@ -524,7 +530,7 @@ conflict is considered fully resolved when there are no conflict markers left. ## Commit Signing -`jj` can be configured to sign and verify the commits it creates using either +`jj` can be configured to sign and verify the commits it creates using either GnuPG or SSH signing keys. To do this you need to configure a signing backend. @@ -575,8 +581,8 @@ signing.backends.ssh.program = "/path/to/ssh-keygen" When verifying commit signatures the ssh backend needs to be provided with an allowed-signers file containing the public keys of authors whose signatures you want to be able to verify. -You can find the format for this file in the -[ssh-keygen man page](https://man.openbsd.org/ssh-keygen#ALLOWED_SIGNERS). This can be provided +You can find the format for this file in the +[ssh-keygen man page](https://man.openbsd.org/ssh-keygen#ALLOWED_SIGNERS). This can be provided as follows: ```toml