forked from mirrors/jj
formatter: use crossterm
for colors
Let's use `crossterm` to make `ColorFormatter` a little more readable, and maybe also more portable. This uses the `SetForegroundColor()` function, which uses the escapes for 256-color support (code 38) instead of the 8-color escapes (codes 30-37) combined with bold/bright (code 1) we were using before. IIUC, most terminals support the 16 base colors when using the 256-color escape even if they don't support all the 256 colors. It seems like an improvement to use actual color codes for the bright colors too, instead of assuming that terminals render bold as bright (even though most terminals do). Before this commit, we relied on ANSI escape 1 - which is specified to make the font bold - to make the color brighter. That's why we call the colors "bright blue" etc. When we switch from using code 30-37 to using 38 to let our color config just control the color (not using escape1), we therefore lose the bold font on many terminals (at least in iTerm2 and in the terminal application on my Debian work computer). As a workaround, I made us still use escape 1 when the bright colors are used. I'll make boldness a separately configurable attribute soon. Then we'll be able to remove this hack. With the switch to `crossterm`, we also reset just the foreground color (code 39) instead of resetting all attributes (code 0). That also seems like an improvement, probably making it easier for us to later support different background colors, underlining, etc.
This commit is contained in:
parent
fbab5e1bd9
commit
5cf2b6615a
6 changed files with 106 additions and 82 deletions
130
src/formatter.rs
130
src/formatter.rs
|
@ -18,6 +18,9 @@ use std::io::{Error, Write};
|
|||
use std::sync::Arc;
|
||||
use std::{fmt, io};
|
||||
|
||||
use crossterm::queue;
|
||||
use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor};
|
||||
|
||||
// Lets the caller label strings and translates the labels to colors
|
||||
pub trait Formatter: Write {
|
||||
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
|
||||
|
@ -150,8 +153,8 @@ pub struct ColorFormatter<W> {
|
|||
output: W,
|
||||
colors: Arc<HashMap<String, String>>,
|
||||
labels: Vec<String>,
|
||||
cached_colors: HashMap<Vec<String>, Vec<u8>>,
|
||||
current_color: Vec<u8>,
|
||||
cached_colors: HashMap<Vec<String>, Color>,
|
||||
current_color: Color,
|
||||
}
|
||||
|
||||
fn config_colors(config: &config::Config) -> HashMap<String, String> {
|
||||
|
@ -171,13 +174,13 @@ impl<W: Write> ColorFormatter<W> {
|
|||
colors,
|
||||
labels: vec![],
|
||||
cached_colors: HashMap::new(),
|
||||
current_color: b"\x1b[0m".to_vec(),
|
||||
current_color: Color::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_color(&mut self) -> Vec<u8> {
|
||||
fn current_color(&mut self) -> Color {
|
||||
if let Some(cached) = self.cached_colors.get(&self.labels) {
|
||||
cached.clone()
|
||||
*cached
|
||||
} else {
|
||||
let mut best_match = (-1, "");
|
||||
for (key, value) in self.colors.as_ref() {
|
||||
|
@ -209,8 +212,7 @@ impl<W: Write> ColorFormatter<W> {
|
|||
}
|
||||
|
||||
let color = color_for_name(best_match.1);
|
||||
self.cached_colors
|
||||
.insert(self.labels.clone(), color.clone());
|
||||
self.cached_colors.insert(self.labels.clone(), color);
|
||||
color
|
||||
}
|
||||
}
|
||||
|
@ -218,32 +220,55 @@ impl<W: Write> ColorFormatter<W> {
|
|||
fn write_new_color(&mut self) -> io::Result<()> {
|
||||
let new_color = self.current_color();
|
||||
if new_color != self.current_color {
|
||||
self.output.write_all(&new_color)?;
|
||||
// For now, make bright colors imply bold font. That better matches our
|
||||
// behavior from when we used ANSI codes 30-37 plus an optional 1 for
|
||||
// bold/bright (we now use code 38 for setting foreground color).
|
||||
// TODO: Make boldness configurable separately from color
|
||||
if !is_bright(&self.current_color) && is_bright(&new_color) {
|
||||
queue!(self.output, SetAttribute(Attribute::Bold))?;
|
||||
} else if !is_bright(&new_color) && is_bright(&self.current_color) {
|
||||
queue!(self.output, SetAttribute(Attribute::Reset))?;
|
||||
}
|
||||
queue!(self.output, SetForegroundColor(new_color))?;
|
||||
self.current_color = new_color;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_name(color_name: &str) -> Vec<u8> {
|
||||
fn is_bright(color: &Color) -> bool {
|
||||
matches!(
|
||||
color,
|
||||
Color::DarkGrey
|
||||
| Color::Red
|
||||
| Color::Green
|
||||
| Color::Yellow
|
||||
| Color::Blue
|
||||
| Color::Magenta
|
||||
| Color::Cyan
|
||||
| Color::White
|
||||
)
|
||||
}
|
||||
|
||||
fn color_for_name(color_name: &str) -> Color {
|
||||
match color_name {
|
||||
"black" => b"\x1b[30m".to_vec(),
|
||||
"red" => b"\x1b[31m".to_vec(),
|
||||
"green" => b"\x1b[32m".to_vec(),
|
||||
"yellow" => b"\x1b[33m".to_vec(),
|
||||
"blue" => b"\x1b[34m".to_vec(),
|
||||
"magenta" => b"\x1b[35m".to_vec(),
|
||||
"cyan" => b"\x1b[36m".to_vec(),
|
||||
"white" => b"\x1b[37m".to_vec(),
|
||||
"bright black" => b"\x1b[1;30m".to_vec(),
|
||||
"bright red" => b"\x1b[1;31m".to_vec(),
|
||||
"bright green" => b"\x1b[1;32m".to_vec(),
|
||||
"bright yellow" => b"\x1b[1;33m".to_vec(),
|
||||
"bright blue" => b"\x1b[1;34m".to_vec(),
|
||||
"bright magenta" => b"\x1b[1;35m".to_vec(),
|
||||
"bright cyan" => b"\x1b[1;36m".to_vec(),
|
||||
"bright white" => b"\x1b[1;37m".to_vec(),
|
||||
_ => b"\x1b[0m".to_vec(),
|
||||
"black" => Color::Black,
|
||||
"red" => Color::DarkRed,
|
||||
"green" => Color::DarkGreen,
|
||||
"yellow" => Color::DarkYellow,
|
||||
"blue" => Color::DarkBlue,
|
||||
"magenta" => Color::DarkMagenta,
|
||||
"cyan" => Color::DarkCyan,
|
||||
"white" => Color::Grey,
|
||||
"bright black" => Color::DarkGrey,
|
||||
"bright red" => Color::Red,
|
||||
"bright green" => Color::Green,
|
||||
"bright yellow" => Color::Yellow,
|
||||
"bright blue" => Color::Blue,
|
||||
"bright magenta" => Color::Magenta,
|
||||
"bright cyan" => Color::Cyan,
|
||||
"bright white" => Color::White,
|
||||
_ => Color::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,22 +346,22 @@ mod tests {
|
|||
formatter.write_str("\n").unwrap();
|
||||
}
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"
|
||||
[30m black [0m
|
||||
[31m red [0m
|
||||
[32m green [0m
|
||||
[33m yellow [0m
|
||||
[34m blue [0m
|
||||
[35m magenta [0m
|
||||
[36m cyan [0m
|
||||
[37m white [0m
|
||||
[1;30m bright black [0m
|
||||
[1;31m bright red [0m
|
||||
[1;32m bright green [0m
|
||||
[1;33m bright yellow [0m
|
||||
[1;34m bright blue [0m
|
||||
[1;35m bright magenta [0m
|
||||
[1;36m bright cyan [0m
|
||||
[1;37m bright white [0m
|
||||
[38;5;0m black [39m
|
||||
[38;5;1m red [39m
|
||||
[38;5;2m green [39m
|
||||
[38;5;3m yellow [39m
|
||||
[38;5;4m blue [39m
|
||||
[38;5;5m magenta [39m
|
||||
[38;5;6m cyan [39m
|
||||
[38;5;7m white [39m
|
||||
[1m[38;5;8m bright black [0m[39m
|
||||
[1m[38;5;9m bright red [0m[39m
|
||||
[1m[38;5;10m bright green [0m[39m
|
||||
[1m[38;5;11m bright yellow [0m[39m
|
||||
[1m[38;5;12m bright blue [0m[39m
|
||||
[1m[38;5;13m bright magenta [0m[39m
|
||||
[1m[38;5;14m bright cyan [0m[39m
|
||||
[1m[38;5;15m bright white [0m[39m
|
||||
"###);
|
||||
}
|
||||
|
||||
|
@ -356,7 +381,7 @@ mod tests {
|
|||
formatter.write_str(" inside ").unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
formatter.write_str(" after ").unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" before [32m inside [0m after ");
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" before [38;5;2m inside [39m after ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -378,7 +403,7 @@ mod tests {
|
|||
formatter.write_str("second").unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
formatter.write_str("after").unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"before[31mfirst[0m[32msecond[0mafter");
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"before[38;5;1mfirst[39m[38;5;2msecond[39mafter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -397,7 +422,7 @@ mod tests {
|
|||
.unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
// TODO: Replace the ANSI escape (\x1b) by something else (🌈?)
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"[31m[1mnot actually bold[0m[0m");
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"[38;5;1m[1mnot actually bold[0m[39m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -424,7 +449,7 @@ mod tests {
|
|||
formatter.remove_label().unwrap();
|
||||
formatter.write_str(" after outer ").unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@" before outer [34m before inner [32m inside inner [34m after inner [0m after outer ");
|
||||
@" before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -445,7 +470,7 @@ mod tests {
|
|||
formatter.write_str(" not colored ").unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@" not colored [32m colored [0m not colored ");
|
||||
@" not colored [38;5;2m colored [39m not colored ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -468,7 +493,7 @@ mod tests {
|
|||
formatter.remove_label().unwrap();
|
||||
// TODO: Make this not reset the color inside
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@"[31m red before [0m still red inside [31m also red afterwards [0m");
|
||||
@"[38;5;1m red before [39m still red inside [38;5;1m also red afterwards [39m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -488,7 +513,7 @@ mod tests {
|
|||
formatter.remove_label().unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@"[32m hello [0m");
|
||||
@"[38;5;2m hello [39m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -506,8 +531,7 @@ mod tests {
|
|||
formatter.write_str(" hello ").unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@" hello ");
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" hello ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -538,7 +562,7 @@ mod tests {
|
|||
formatter.write_str(" a2 ").unwrap();
|
||||
formatter.remove_label().unwrap();
|
||||
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
@" a1 [31m b1 c1 [34m d [31m c2 b2 [0m a2 ");
|
||||
@" a1 [38;5;1m b1 c1 [38;5;4m d [38;5;1m c2 b2 [39m a2 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -567,6 +591,6 @@ mod tests {
|
|||
formatter.remove_label().unwrap();
|
||||
// TODO: This is currently not deterministic.
|
||||
// insta::assert_snapshot!(String::from_utf8(output).unwrap(),
|
||||
// @"[31m a1 [32m b1 [33m c [32m b2 [31m a2 [0m");
|
||||
// @"[38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ fn test_alias_global_args_in_definition() {
|
|||
// The global argument in the alias is respected
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["l"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
o [34m0000000000000000000000000000000000000000[0m
|
||||
o [38;5;4m0000000000000000000000000000000000000000[39m
|
||||
"###);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,22 +75,22 @@ fn test_log_default() {
|
|||
// Color
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "--color=always"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
@ [1;35mffdaa62087a2[0m [1;33mtest.user@example.com[0m [1;36m2001-02-03 04:05:09.000 +07:00[0m [1;35mmy-branch[0m [1;34m9de54178d59d[0m
|
||||
| [1;37mdescription 1[0m
|
||||
o [35m9a45c67d3e96[0m [33mtest.user@example.com[0m [36m2001-02-03 04:05:08.000 +07:00[0m [34m4291e264ae97[0m
|
||||
@ [1m[38;5;13mffdaa62087a2[0m[39m [1m[38;5;11mtest.user@example.com[0m[39m [1m[38;5;14m2001-02-03 04:05:09.000 +07:00[0m[39m [1m[38;5;13mmy-branch[0m[39m [1m[38;5;12m9de54178d59d[0m[39m
|
||||
| [1m[38;5;15mdescription 1[0m[39m
|
||||
o [38;5;5m9a45c67d3e96[39m [38;5;3mtest.user@example.com[39m [38;5;6m2001-02-03 04:05:08.000 +07:00[39m [38;5;4m4291e264ae97[39m
|
||||
| add a file
|
||||
o [35m000000000000[0m [33m[0m [36m1970-01-01 00:00:00.000 +00:00[0m [34m000000000000[0m
|
||||
o [38;5;5m000000000000[39m [38;5;3m[39m [38;5;6m1970-01-01 00:00:00.000 +00:00[39m [38;5;4m000000000000[39m
|
||||
(no description set)
|
||||
"###);
|
||||
|
||||
// Color without graph
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "--color=always", "--no-graph"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
[1;35mffdaa62087a2[0m [1;33mtest.user@example.com[0m [1;36m2001-02-03 04:05:09.000 +07:00[0m [1;35mmy-branch[0m [1;34m9de54178d59d[0m
|
||||
[1;37mdescription 1[0m
|
||||
[35m9a45c67d3e96[0m [33mtest.user@example.com[0m [36m2001-02-03 04:05:08.000 +07:00[0m [34m4291e264ae97[0m
|
||||
[1m[38;5;13mffdaa62087a2[0m[39m [1m[38;5;11mtest.user@example.com[0m[39m [1m[38;5;14m2001-02-03 04:05:09.000 +07:00[0m[39m [1m[38;5;13mmy-branch[0m[39m [1m[38;5;12m9de54178d59d[0m[39m
|
||||
[1m[38;5;15mdescription 1[0m[39m
|
||||
[38;5;5m9a45c67d3e96[39m [38;5;3mtest.user@example.com[39m [38;5;6m2001-02-03 04:05:08.000 +07:00[39m [38;5;4m4291e264ae97[39m
|
||||
add a file
|
||||
[35m000000000000[0m [33m[0m [36m1970-01-01 00:00:00.000 +00:00[0m [34m000000000000[0m
|
||||
[38;5;5m000000000000[39m [38;5;3m[39m [38;5;6m1970-01-01 00:00:00.000 +00:00[39m [38;5;4m000000000000[39m
|
||||
(no description set)
|
||||
"###);
|
||||
}
|
||||
|
@ -131,11 +131,11 @@ fn test_log_default_divergence() {
|
|||
// Color
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "--color=always"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
o [31m9a45c67d3e96??[0m [33mtest.user@example.com[0m [36m2001-02-03 04:05:10.000 +07:00[0m [34m8979953d4c67[0m
|
||||
o [38;5;1m9a45c67d3e96??[39m [38;5;3mtest.user@example.com[39m [38;5;6m2001-02-03 04:05:10.000 +07:00[39m [38;5;4m8979953d4c67[39m
|
||||
| description 2
|
||||
| @ [1;31m9a45c67d3e96??[0m [1;33mtest.user@example.com[0m [1;36m2001-02-03 04:05:08.000 +07:00[0m [1;34m7a17d52e633c[0m
|
||||
|/ [1;37mdescription 1[0m
|
||||
o [35m000000000000[0m [33m[0m [36m1970-01-01 00:00:00.000 +00:00[0m [34m000000000000[0m
|
||||
| @ [1m[38;5;9m9a45c67d3e96??[0m[39m [1m[38;5;11mtest.user@example.com[0m[39m [1m[38;5;14m2001-02-03 04:05:08.000 +07:00[0m[39m [1m[38;5;12m7a17d52e633c[0m[39m
|
||||
|/ [1m[38;5;15mdescription 1[0m[39m
|
||||
o [38;5;5m000000000000[39m [38;5;3m[39m [38;5;6m1970-01-01 00:00:00.000 +00:00[39m [38;5;4m000000000000[39m
|
||||
(no description set)
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -182,8 +182,8 @@ fn test_color_config() {
|
|||
// Test that --color=always is respected.
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["--color=always", "log", "-T", "commit_id"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
@ [1;34m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m
|
||||
o [34m0000000000000000000000000000000000000000[0m
|
||||
@ [1m[38;5;12m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m[39m
|
||||
o [38;5;4m0000000000000000000000000000000000000000[39m
|
||||
"###);
|
||||
|
||||
// Test that color is used if it's requested in the config file
|
||||
|
@ -193,8 +193,8 @@ color="always""#,
|
|||
);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
@ [1;34m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m
|
||||
o [34m0000000000000000000000000000000000000000[0m
|
||||
@ [1m[38;5;12m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m[39m
|
||||
o [38;5;4m0000000000000000000000000000000000000000[39m
|
||||
"###);
|
||||
|
||||
// Test that --color=never overrides the config.
|
||||
|
@ -249,8 +249,8 @@ color="always""#,
|
|||
test_env.add_env_var("NO_COLOR", "");
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
@ [1;34m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m
|
||||
o [34m0000000000000000000000000000000000000000[0m
|
||||
@ [1m[38;5;12m230dd059e1b059aefc0da06a2e5a7dbf22362f22[0m[39m
|
||||
o [38;5;4m0000000000000000000000000000000000000000[39m
|
||||
"###);
|
||||
|
||||
// Test that per-repo config overrides the user config.
|
||||
|
|
|
@ -573,13 +573,13 @@ fn test_graph_template_color() {
|
|||
// TODO: The color codes shouldn't span the graph lines, and we shouldn't get an
|
||||
// extra line at the end
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
@ [32msingle line
|
||||
| [0m
|
||||
o [31mfirst line
|
||||
@ [38;5;2msingle line
|
||||
| [39m
|
||||
o [38;5;1mfirst line
|
||||
| second line
|
||||
| third line
|
||||
| [0m
|
||||
o [31m(no description set)
|
||||
[0m
|
||||
| [39m
|
||||
o [38;5;1m(no description set)
|
||||
[39m
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -386,7 +386,7 @@ fn test_too_many_parents() {
|
|||
// Test warning color
|
||||
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
|
||||
@r###"
|
||||
file [33m[31m3-sided[33m conflict[0m
|
||||
file [38;5;3m[38;5;1m3-sided[38;5;3m conflict[39m
|
||||
"###);
|
||||
|
||||
let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]);
|
||||
|
@ -518,7 +518,7 @@ fn test_description_with_dir_and_deletion() {
|
|||
// Test warning color. The deletion is fine, so it's not highlighted
|
||||
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
|
||||
@r###"
|
||||
file [33m[31m3-sided[33m conflict including 1 deletion and [31ma directory[33m[0m
|
||||
file [38;5;3m[38;5;1m3-sided[38;5;3m conflict including 1 deletion and [38;5;1ma directory[38;5;3m[39m
|
||||
"###);
|
||||
let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]);
|
||||
insta::assert_snapshot!(error, @r###"
|
||||
|
@ -618,8 +618,8 @@ fn test_multiple_conflicts() {
|
|||
// Test colors
|
||||
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
|
||||
@r###"
|
||||
another_file [33m2-sided conflict[0m
|
||||
this_file_has_a_very_long_name_to_test_padding [33m2-sided conflict[0m
|
||||
another_file [38;5;3m2-sided conflict[39m
|
||||
this_file_has_a_very_long_name_to_test_padding [38;5;3m2-sided conflict[39m
|
||||
"###);
|
||||
|
||||
let editor_script = test_env.set_up_fake_editor();
|
||||
|
|
Loading…
Reference in a new issue