2022-11-26 23:57:50 +00:00
// Copyright 2020 The Jujutsu Authors
2020-12-12 08:00:42 +00:00
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2023-01-12 06:08:57 +00:00
use std ::borrow ::BorrowMut ;
2020-12-12 08:00:42 +00:00
use std ::collections ::HashMap ;
2023-01-12 06:06:43 +00:00
use std ::io ::{ Error , Write } ;
2023-03-05 03:33:40 +00:00
use std ::ops ::Range ;
2022-10-07 03:52:01 +00:00
use std ::sync ::Arc ;
2023-01-13 18:09:09 +00:00
use std ::{ fmt , io , mem } ;
2020-12-12 08:00:42 +00:00
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.
2022-12-27 07:23:19 +00:00
use crossterm ::queue ;
2023-01-07 17:34:15 +00:00
use crossterm ::style ::{ Attribute , Color , SetAttribute , SetBackgroundColor , SetForegroundColor } ;
2023-01-03 08:07:03 +00:00
use itertools ::Itertools ;
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.
2022-12-27 07:23:19 +00:00
2020-12-12 08:00:42 +00:00
// Lets the caller label strings and translates the labels to colors
2021-06-02 22:50:08 +00:00
pub trait Formatter : Write {
2021-04-07 06:05:16 +00:00
fn write_str ( & mut self , text : & str ) -> io ::Result < ( ) > {
self . write_all ( text . as_bytes ( ) )
2020-12-12 08:00:42 +00:00
}
2023-01-19 17:33:16 +00:00
/// Returns the backing `Write`. This is useful for writing data that is
/// already formatted, such as in the graphical log.
fn raw ( & mut self ) -> & mut dyn Write ;
2023-01-12 08:00:12 +00:00
fn push_label ( & mut self , label : & str ) -> io ::Result < ( ) > ;
2020-12-12 08:00:42 +00:00
2023-01-12 08:00:12 +00:00
fn pop_label ( & mut self ) -> io ::Result < ( ) > ;
2020-12-12 08:00:42 +00:00
}
formatter: add a `with_label()` helper
There's a risk of forgetting to call `remove_label()` and I've wanted
to reduce that risk for a long time. I considered creating RAII
adapters that implement `Drop`, but I didn't like that that would
ignore errors (such as `BrokenPipe`) that can happen while emitting an
escape sequence in `remove_label()`. I would ideally have liked
Python's context managers here, but Rust doesn't have that. Instead,
we get to use closures. That works pretty well, except that we can't
return other errors than `io::Error` inside the closures. Even with
that limitation, we can use the new `with_label()` method in all but a
few cases.
We can't define the `with_label()` method directly in the trait
because `&mut self` is not a trait object, so we can't pass it on to
the closure (which expects a trait object). The solution is to use
`impl dyn Formatter` (thanks to @kupiakos for figuring that
out!). That unfortunately means that we can *only* call the function
on trait objects, so if `f` is a concrete formatter type
(e.g. `PlainTextFormatter`), then `f.with_label()` won't
compile. Since we only ever access the formatters as trait objects,
that's not really a problem, however.
2022-10-12 18:53:37 +00:00
impl dyn Formatter + '_ {
2023-01-12 06:46:41 +00:00
pub fn labeled < S : AsRef < str > > ( & mut self , label : S ) -> LabeledWriter < & mut Self , S > {
LabeledWriter {
formatter : self ,
label ,
}
}
formatter: add a `with_label()` helper
There's a risk of forgetting to call `remove_label()` and I've wanted
to reduce that risk for a long time. I considered creating RAII
adapters that implement `Drop`, but I didn't like that that would
ignore errors (such as `BrokenPipe`) that can happen while emitting an
escape sequence in `remove_label()`. I would ideally have liked
Python's context managers here, but Rust doesn't have that. Instead,
we get to use closures. That works pretty well, except that we can't
return other errors than `io::Error` inside the closures. Even with
that limitation, we can use the new `with_label()` method in all but a
few cases.
We can't define the `with_label()` method directly in the trait
because `&mut self` is not a trait object, so we can't pass it on to
the closure (which expects a trait object). The solution is to use
`impl dyn Formatter` (thanks to @kupiakos for figuring that
out!). That unfortunately means that we can *only* call the function
on trait objects, so if `f` is a concrete formatter type
(e.g. `PlainTextFormatter`), then `f.with_label()` won't
compile. Since we only ever access the formatters as trait objects,
that's not really a problem, however.
2022-10-12 18:53:37 +00:00
pub fn with_label (
& mut self ,
label : & str ,
write_inner : impl FnOnce ( & mut dyn Formatter ) -> io ::Result < ( ) > ,
) -> io ::Result < ( ) > {
2023-01-12 08:00:12 +00:00
self . push_label ( label ) ? ;
// Call `pop_label()` whether or not `write_inner()` fails, but don't let
formatter: add a `with_label()` helper
There's a risk of forgetting to call `remove_label()` and I've wanted
to reduce that risk for a long time. I considered creating RAII
adapters that implement `Drop`, but I didn't like that that would
ignore errors (such as `BrokenPipe`) that can happen while emitting an
escape sequence in `remove_label()`. I would ideally have liked
Python's context managers here, but Rust doesn't have that. Instead,
we get to use closures. That works pretty well, except that we can't
return other errors than `io::Error` inside the closures. Even with
that limitation, we can use the new `with_label()` method in all but a
few cases.
We can't define the `with_label()` method directly in the trait
because `&mut self` is not a trait object, so we can't pass it on to
the closure (which expects a trait object). The solution is to use
`impl dyn Formatter` (thanks to @kupiakos for figuring that
out!). That unfortunately means that we can *only* call the function
on trait objects, so if `f` is a concrete formatter type
(e.g. `PlainTextFormatter`), then `f.with_label()` won't
compile. Since we only ever access the formatters as trait objects,
that's not really a problem, however.
2022-10-12 18:53:37 +00:00
// its error replace the one from `write_inner()`.
2023-01-12 08:00:12 +00:00
write_inner ( self ) . and ( self . pop_label ( ) )
formatter: add a `with_label()` helper
There's a risk of forgetting to call `remove_label()` and I've wanted
to reduce that risk for a long time. I considered creating RAII
adapters that implement `Drop`, but I didn't like that that would
ignore errors (such as `BrokenPipe`) that can happen while emitting an
escape sequence in `remove_label()`. I would ideally have liked
Python's context managers here, but Rust doesn't have that. Instead,
we get to use closures. That works pretty well, except that we can't
return other errors than `io::Error` inside the closures. Even with
that limitation, we can use the new `with_label()` method in all but a
few cases.
We can't define the `with_label()` method directly in the trait
because `&mut self` is not a trait object, so we can't pass it on to
the closure (which expects a trait object). The solution is to use
`impl dyn Formatter` (thanks to @kupiakos for figuring that
out!). That unfortunately means that we can *only* call the function
on trait objects, so if `f` is a concrete formatter type
(e.g. `PlainTextFormatter`), then `f.with_label()` won't
compile. Since we only ever access the formatters as trait objects,
that's not really a problem, however.
2022-10-12 18:53:37 +00:00
}
}
2023-01-12 06:08:57 +00:00
/// `Formatter` wrapper to write a labeled message with `write!()` or
/// `writeln!()`.
pub struct LabeledWriter < T , S > {
formatter : T ,
label : S ,
}
impl < T , S > LabeledWriter < T , S > {
pub fn new ( formatter : T , label : S ) -> Self {
LabeledWriter { formatter , label }
}
}
impl < ' a , T , S > LabeledWriter < T , S >
where
T : BorrowMut < dyn Formatter + ' a > ,
S : AsRef < str > ,
{
pub fn write_fmt ( & mut self , args : fmt ::Arguments < '_ > ) -> io ::Result < ( ) > {
self . formatter
. borrow_mut ( )
. with_label ( self . label . as_ref ( ) , | formatter | formatter . write_fmt ( args ) )
}
}
2023-01-10 16:36:24 +00:00
type Rules = Vec < ( Vec < String > , Style ) > ;
2022-10-07 03:52:01 +00:00
/// Creates `Formatter` instances with preconfigured parameters.
#[ derive(Clone, Debug) ]
pub struct FormatterFactory {
kind : FormatterFactoryKind ,
}
#[ derive(Clone, Debug) ]
enum FormatterFactoryKind {
PlainText ,
2023-01-21 21:55:34 +00:00
Sanitized ,
2023-01-10 16:36:24 +00:00
Color { rules : Arc < Rules > } ,
2022-10-07 03:52:01 +00:00
}
impl FormatterFactory {
2023-01-18 01:20:27 +00:00
pub fn prepare (
config : & config ::Config ,
color : bool ,
sanitized : bool ,
) -> Result < Self , config ::ConfigError > {
2022-10-07 03:52:01 +00:00
let kind = if color {
2023-01-18 01:20:27 +00:00
let rules = Arc ::new ( rules_from_config ( config ) ? ) ;
2023-01-03 08:07:03 +00:00
FormatterFactoryKind ::Color { rules }
2023-01-21 21:55:34 +00:00
} else if sanitized {
FormatterFactoryKind ::Sanitized
2022-10-07 03:52:01 +00:00
} else {
FormatterFactoryKind ::PlainText
} ;
2023-01-18 01:20:27 +00:00
Ok ( FormatterFactory { kind } )
2022-10-07 03:52:01 +00:00
}
2022-10-07 11:37:51 +00:00
pub fn new_formatter < ' output , W : Write + ' output > (
2022-10-07 03:52:01 +00:00
& self ,
2022-10-07 11:37:51 +00:00
output : W ,
2022-10-07 03:52:01 +00:00
) -> Box < dyn Formatter + ' output > {
match & self . kind {
FormatterFactoryKind ::PlainText = > Box ::new ( PlainTextFormatter ::new ( output ) ) ,
2023-01-21 21:55:34 +00:00
FormatterFactoryKind ::Sanitized = > Box ::new ( SanitizingFormatter ::new ( output ) ) ,
2023-01-03 08:07:03 +00:00
FormatterFactoryKind ::Color { rules } = > {
Box ::new ( ColorFormatter ::new ( output , rules . clone ( ) ) )
2022-10-07 03:52:01 +00:00
}
}
}
}
2022-10-07 11:37:51 +00:00
pub struct PlainTextFormatter < W > {
output : W ,
2020-12-12 08:00:42 +00:00
}
2022-10-07 11:37:51 +00:00
impl < W > PlainTextFormatter < W > {
pub fn new ( output : W ) -> PlainTextFormatter < W > {
2020-12-12 08:00:42 +00:00
Self { output }
}
}
2022-10-07 11:37:51 +00:00
impl < W : Write > Write for PlainTextFormatter < W > {
2020-12-12 08:00:42 +00:00
fn write ( & mut self , data : & [ u8 ] ) -> Result < usize , Error > {
self . output . write ( data )
}
fn flush ( & mut self ) -> Result < ( ) , Error > {
self . output . flush ( )
}
}
2022-10-07 11:37:51 +00:00
impl < W : Write > Formatter for PlainTextFormatter < W > {
2023-01-19 17:33:16 +00:00
fn raw ( & mut self ) -> & mut dyn Write {
& mut self . output
}
2023-01-12 08:00:12 +00:00
fn push_label ( & mut self , _label : & str ) -> io ::Result < ( ) > {
2021-04-07 06:05:16 +00:00
Ok ( ( ) )
}
2020-12-12 08:00:42 +00:00
2023-01-12 08:00:12 +00:00
fn pop_label ( & mut self ) -> io ::Result < ( ) > {
2021-04-07 06:05:16 +00:00
Ok ( ( ) )
}
2020-12-12 08:00:42 +00:00
}
2023-01-21 21:55:34 +00:00
pub struct SanitizingFormatter < W > {
output : W ,
}
impl < W > SanitizingFormatter < W > {
pub fn new ( output : W ) -> SanitizingFormatter < W > {
Self { output }
}
}
impl < W : Write > Write for SanitizingFormatter < W > {
fn write ( & mut self , data : & [ u8 ] ) -> Result < usize , Error > {
write_sanitized ( & mut self . output , data ) ? ;
Ok ( data . len ( ) )
}
fn flush ( & mut self ) -> Result < ( ) , Error > {
self . output . flush ( )
}
}
impl < W : Write > Formatter for SanitizingFormatter < W > {
fn raw ( & mut self ) -> & mut dyn Write {
& mut self . output
}
fn push_label ( & mut self , _label : & str ) -> io ::Result < ( ) > {
Ok ( ( ) )
}
fn pop_label ( & mut self ) -> io ::Result < ( ) > {
Ok ( ( ) )
}
}
2023-01-07 16:02:47 +00:00
#[ derive(Clone, Debug, Default, PartialEq, Eq) ]
2023-01-03 08:07:03 +00:00
pub struct Style {
2023-01-07 16:02:47 +00:00
pub fg_color : Option < Color > ,
2023-01-07 17:34:15 +00:00
pub bg_color : Option < Color > ,
2023-01-07 18:03:08 +00:00
pub bold : Option < bool > ,
2023-01-10 02:59:21 +00:00
pub underlined : Option < bool > ,
2023-01-02 17:36:04 +00:00
}
2023-01-09 07:50:18 +00:00
impl Style {
fn merge ( & mut self , other : & Style ) {
self . fg_color = other . fg_color . or ( self . fg_color ) ;
2023-01-07 17:34:15 +00:00
self . bg_color = other . bg_color . or ( self . bg_color ) ;
2023-01-07 18:03:08 +00:00
self . bold = other . bold . or ( self . bold ) ;
2023-01-10 02:59:21 +00:00
self . underlined = other . underlined . or ( self . underlined ) ;
2023-01-09 07:50:18 +00:00
}
}
2022-10-07 11:37:51 +00:00
pub struct ColorFormatter < W > {
output : W ,
2023-01-10 16:36:24 +00:00
rules : Arc < Rules > ,
2023-01-12 17:22:39 +00:00
/// The stack of currently applied labels. These determine the desired
/// style.
2020-12-12 08:00:42 +00:00
labels : Vec < String > ,
2023-01-02 17:36:04 +00:00
cached_styles : HashMap < Vec < String > , Style > ,
2023-01-12 17:22:39 +00:00
/// The style we last wrote to the output.
2023-01-02 17:36:04 +00:00
current_style : Style ,
2020-12-12 08:00:42 +00:00
}
2023-01-07 17:36:19 +00:00
impl < W : Write > ColorFormatter < W > {
2023-01-10 16:36:24 +00:00
pub fn new ( output : W , rules : Arc < Rules > ) -> ColorFormatter < W > {
2021-06-02 22:50:08 +00:00
ColorFormatter {
2020-12-12 08:00:42 +00:00
output ,
2023-01-03 08:07:03 +00:00
rules ,
2020-12-12 08:00:42 +00:00
labels : vec ! [ ] ,
2023-01-02 17:36:04 +00:00
cached_styles : HashMap ::new ( ) ,
current_style : Style ::default ( ) ,
2020-12-12 08:00:42 +00:00
}
}
2023-01-18 01:20:27 +00:00
pub fn for_config ( output : W , config : & config ::Config ) -> Result < Self , config ::ConfigError > {
let rules = rules_from_config ( config ) ? ;
Ok ( Self ::new ( output , Arc ::new ( rules ) ) )
2023-01-03 08:07:03 +00:00
}
2023-01-12 17:22:39 +00:00
fn requested_style ( & mut self ) -> Style {
2023-01-02 17:36:04 +00:00
if let Some ( cached ) = self . cached_styles . get ( & self . labels ) {
cached . clone ( )
2020-12-12 08:00:42 +00:00
} else {
2023-01-09 07:50:18 +00:00
// We use the reverse list of matched indices as a measure of how well the rule
// matches the actual labels. For example, for rule "a d" and the actual labels
// "a b c d", we'll get [3,0]. We compare them by Rust's default Vec comparison.
// That means "a d" will trump both rule "d" (priority [3]) and rule
// "a b c" (priority [2,1,0]).
let mut matched_styles = vec! [ ] ;
2023-01-03 08:07:03 +00:00
for ( labels , style ) in self . rules . as_ref ( ) {
2023-01-09 07:50:18 +00:00
let mut labels_iter = self . labels . iter ( ) . enumerate ( ) ;
// The indexes in the current label stack that match the required label.
let mut matched_indices = vec! [ ] ;
2023-01-03 08:07:03 +00:00
for required_label in labels {
2023-01-09 07:50:18 +00:00
for ( label_index , label ) in & mut labels_iter {
if label = = required_label {
matched_indices . push ( label_index ) ;
break ;
2023-01-01 18:00:06 +00:00
}
2020-12-12 08:00:42 +00:00
}
}
2023-01-09 07:50:18 +00:00
if matched_indices . len ( ) = = labels . len ( ) {
matched_indices . reverse ( ) ;
matched_styles . push ( ( style , matched_indices ) ) ;
2020-12-12 08:00:42 +00:00
}
}
2023-01-09 07:50:18 +00:00
matched_styles . sort_by_key ( | ( _ , indices ) | indices . clone ( ) ) ;
2020-12-12 08:00:42 +00:00
2023-01-09 07:50:18 +00:00
let mut style = Style ::default ( ) ;
for ( matched_style , _ ) in matched_styles {
style . merge ( matched_style ) ;
}
2023-01-02 17:36:04 +00:00
self . cached_styles
. insert ( self . labels . clone ( ) , style . clone ( ) ) ;
style
2020-12-12 08:00:42 +00:00
}
}
2023-01-07 17:36:19 +00:00
2023-01-02 17:36:04 +00:00
fn write_new_style ( & mut self ) -> io ::Result < ( ) > {
2023-01-12 17:22:39 +00:00
let new_style = self . requested_style ( ) ;
2023-01-02 17:36:04 +00:00
if new_style ! = self . current_style {
2023-01-07 18:03:08 +00:00
if new_style . bold ! = self . current_style . bold {
if new_style . bold . unwrap_or_default ( ) {
queue! ( self . output , SetAttribute ( Attribute ::Bold ) ) ? ;
} else {
// NoBold results in double underlining on some terminals, so we use reset
// instead. However, that resets other attributes as well, so we reset
// our record of the current style so we re-apply the other attributes
// below.
queue! ( self . output , SetAttribute ( Attribute ::Reset ) ) ? ;
self . current_style = Style ::default ( ) ;
}
}
2023-01-10 02:59:21 +00:00
if new_style . underlined ! = self . current_style . underlined {
if new_style . underlined . unwrap_or_default ( ) {
queue! ( self . output , SetAttribute ( Attribute ::Underlined ) ) ? ;
} else {
queue! ( self . output , SetAttribute ( Attribute ::NoUnderline ) ) ? ;
}
}
2023-01-07 17:34:15 +00:00
if new_style . fg_color ! = self . current_style . fg_color {
queue! (
self . output ,
SetForegroundColor ( new_style . fg_color . unwrap_or ( Color ::Reset ) )
) ? ;
}
if new_style . bg_color ! = self . current_style . bg_color {
queue! (
self . output ,
SetBackgroundColor ( new_style . bg_color . unwrap_or ( Color ::Reset ) )
) ? ;
}
2023-01-02 17:36:04 +00:00
self . current_style = new_style ;
2023-01-07 17:36:19 +00:00
}
Ok ( ( ) )
}
2022-12-31 22:18:14 +00:00
}
2020-12-12 08:00:42 +00:00
2023-01-18 01:20:27 +00:00
fn rules_from_config ( config : & config ::Config ) -> Result < Rules , config ::ConfigError > {
2023-01-10 16:36:24 +00:00
let mut result = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let table = config . get_table ( " colors " ) ? ;
for ( key , value ) in table {
let labels = key
. split_whitespace ( )
. map ( ToString ::to_string )
. collect_vec ( ) ;
match value . kind {
config ::ValueKind ::String ( color_name ) = > {
let style = Style {
fg_color : color_for_name ( & color_name ) ,
bg_color : None ,
bold : None ,
underlined : None ,
} ;
result . push ( ( labels , style ) ) ;
}
config ::ValueKind ::Table ( style_table ) = > {
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 = color_for_name ( color_name ) ;
2023-01-06 05:58:52 +00:00
}
2023-01-18 01:20:27 +00:00
}
if let Some ( value ) = style_table . get ( " bg " ) {
if let config ::ValueKind ::String ( color_name ) = & value . kind {
style . bg_color = color_for_name ( color_name ) ;
2023-01-07 17:34:15 +00:00
}
2023-01-18 01:20:27 +00:00
}
if let Some ( value ) = style_table . get ( " bold " ) {
if let config ::ValueKind ::Boolean ( value ) = & value . kind {
style . bold = Some ( * value ) ;
2023-01-07 18:03:08 +00:00
}
2023-01-18 01:20:27 +00:00
}
2023-01-31 06:51:14 +00:00
if let Some ( value ) = style_table . get ( " underline " ) {
2023-01-18 01:20:27 +00:00
if let config ::ValueKind ::Boolean ( value ) = & value . kind {
style . underlined = Some ( * value ) ;
2023-01-10 02:59:21 +00:00
}
2023-01-06 05:58:52 +00:00
}
2023-01-18 01:20:27 +00:00
result . push ( ( labels , style ) ) ;
2023-01-06 05:58:52 +00:00
}
2023-01-18 01:20:27 +00:00
_ = > { }
2023-01-03 08:07:03 +00:00
}
}
2023-01-18 01:20:27 +00:00
Ok ( result )
2023-01-03 08:07:03 +00:00
}
2023-01-07 16:02:47 +00:00
fn color_for_name ( color_name : & str ) -> Option < Color > {
2022-12-31 22:18:14 +00:00
match color_name {
2023-01-07 16:02:47 +00:00
" black " = > Some ( Color ::Black ) ,
" red " = > Some ( Color ::DarkRed ) ,
" green " = > Some ( Color ::DarkGreen ) ,
" yellow " = > Some ( Color ::DarkYellow ) ,
" blue " = > Some ( Color ::DarkBlue ) ,
" magenta " = > Some ( Color ::DarkMagenta ) ,
" cyan " = > Some ( Color ::DarkCyan ) ,
" white " = > Some ( Color ::Grey ) ,
" bright black " = > Some ( Color ::DarkGrey ) ,
" bright red " = > Some ( Color ::Red ) ,
" bright green " = > Some ( Color ::Green ) ,
" bright yellow " = > Some ( Color ::Yellow ) ,
" bright blue " = > Some ( Color ::Blue ) ,
" bright magenta " = > Some ( Color ::Magenta ) ,
" bright cyan " = > Some ( Color ::Cyan ) ,
" bright white " = > Some ( Color ::White ) ,
_ = > None ,
2020-12-12 08:00:42 +00:00
}
}
2022-10-07 11:37:51 +00:00
impl < W : Write > Write for ColorFormatter < W > {
2020-12-12 08:00:42 +00:00
fn write ( & mut self , data : & [ u8 ] ) -> Result < usize , Error > {
2023-01-13 18:09:09 +00:00
/*
We clear the current style at the end of each line , and then we re - apply the style
after the newline . There are several reasons for this :
* We can more easily skip styling a trailing blank line , which other
internal code then can correctly detect as having a trailing
newline .
* Some tools ( like ` less - R ` ) add an extra newline if the final
character is not a newline ( e . g . if there ' s a color reset after
it ) , which led to an annoying blank line after the diff summary in
e . g . ` jj status ` .
* Since each line is styled independently , you get all the necessary
escapes even when grepping through the output .
* Some terminals extend background color to the end of the terminal
( i . e . past the newline character ) , which is probably not what the
user wanted .
* Some tools ( like ` less - R ` ) get confused and lose coloring of lines
after a newline .
* /
for line in data . split_inclusive ( | b | * b = = b '\n' ) {
if line . ends_with ( b " \n " ) {
self . write_new_style ( ) ? ;
2023-01-19 17:33:16 +00:00
write_sanitized ( & mut self . output , & line [ .. line . len ( ) - 1 ] ) ? ;
2023-01-13 18:09:09 +00:00
let labels = mem ::take ( & mut self . labels ) ;
self . write_new_style ( ) ? ;
self . output . write_all ( b " \n " ) ? ;
self . labels = labels ;
} else {
self . write_new_style ( ) ? ;
2023-01-19 17:33:16 +00:00
write_sanitized ( & mut self . output , line ) ? ;
2023-01-13 18:09:09 +00:00
}
}
Ok ( data . len ( ) )
2020-12-12 08:00:42 +00:00
}
fn flush ( & mut self ) -> Result < ( ) , Error > {
self . output . flush ( )
}
}
2022-10-07 11:37:51 +00:00
impl < W : Write > Formatter for ColorFormatter < W > {
2023-01-19 17:33:16 +00:00
fn raw ( & mut self ) -> & mut dyn Write {
& mut self . output
}
2023-01-12 08:00:12 +00:00
fn push_label ( & mut self , label : & str ) -> io ::Result < ( ) > {
2022-10-06 11:16:41 +00:00
self . labels . push ( label . to_owned ( ) ) ;
2023-01-13 08:39:22 +00:00
Ok ( ( ) )
2020-12-12 08:00:42 +00:00
}
2023-01-12 08:00:12 +00:00
fn pop_label ( & mut self ) -> io ::Result < ( ) > {
2020-12-12 08:00:42 +00:00
self . labels . pop ( ) ;
2023-01-13 08:39:22 +00:00
if self . labels . is_empty ( ) {
self . write_new_style ( ) ?
}
Ok ( ( ) )
2020-12-12 08:00:42 +00:00
}
}
2022-12-23 04:15:37 +00:00
2023-03-02 06:58:04 +00:00
/// Like buffered formatter, but records `push`/`pop_label()` calls.
///
/// This allows you to manipulate the recorded data without losing labels.
/// The recorded data and labels can be written to another formatter. If
/// the destination formatter has already been labeled, the recorded labels
/// will be stacked on top of the existing labels, and the subsequent data
/// may be colorized differently.
#[ derive(Debug, Default) ]
pub struct FormatRecorder {
data : Vec < u8 > ,
label_ops : Vec < ( usize , LabelOp ) > ,
}
#[ derive(Clone, Debug, Eq, PartialEq) ]
enum LabelOp {
PushLabel ( String ) ,
PopLabel ,
}
impl FormatRecorder {
pub fn new ( ) -> Self {
FormatRecorder ::default ( )
}
pub fn data ( & self ) -> & [ u8 ] {
& self . data
}
fn push_label_op ( & mut self , op : LabelOp ) {
self . label_ops . push ( ( self . data . len ( ) , op ) ) ;
}
pub fn replay ( & self , formatter : & mut dyn Formatter ) -> io ::Result < ( ) > {
2023-03-05 03:33:40 +00:00
self . replay_with ( formatter , | formatter , range | {
formatter . write_all ( & self . data [ range ] )
} )
2023-03-02 06:58:04 +00:00
}
pub fn replay_with (
& self ,
formatter : & mut dyn Formatter ,
2023-03-05 03:33:40 +00:00
mut write_data : impl FnMut ( & mut dyn Formatter , Range < usize > ) -> io ::Result < ( ) > ,
2023-03-02 06:58:04 +00:00
) -> io ::Result < ( ) > {
let mut last_pos = 0 ;
let mut flush_data = | formatter : & mut dyn Formatter , pos | -> io ::Result < ( ) > {
if last_pos ! = pos {
2023-03-05 03:33:40 +00:00
write_data ( formatter , last_pos .. pos ) ? ;
2023-03-02 06:58:04 +00:00
last_pos = pos ;
}
Ok ( ( ) )
} ;
for ( pos , op ) in & self . label_ops {
flush_data ( formatter , * pos ) ? ;
match op {
LabelOp ::PushLabel ( label ) = > formatter . push_label ( label ) ? ,
LabelOp ::PopLabel = > formatter . pop_label ( ) ? ,
}
}
flush_data ( formatter , self . data . len ( ) )
}
}
impl Write for FormatRecorder {
fn write ( & mut self , data : & [ u8 ] ) -> io ::Result < usize > {
self . data . extend_from_slice ( data ) ;
Ok ( data . len ( ) )
}
fn flush ( & mut self ) -> io ::Result < ( ) > {
Ok ( ( ) )
}
}
impl Formatter for FormatRecorder {
fn raw ( & mut self ) -> & mut dyn Write {
panic! ( " raw output isn't supported by FormatRecorder " )
}
fn push_label ( & mut self , label : & str ) -> io ::Result < ( ) > {
self . push_label_op ( LabelOp ::PushLabel ( label . to_owned ( ) ) ) ;
Ok ( ( ) )
}
fn pop_label ( & mut self ) -> io ::Result < ( ) > {
self . push_label_op ( LabelOp ::PopLabel ) ;
Ok ( ( ) )
}
}
2023-01-19 17:33:16 +00:00
fn write_sanitized ( output : & mut impl Write , buf : & [ u8 ] ) -> Result < ( ) , Error > {
if buf . contains ( & b '\x1b' ) {
let mut sanitized = Vec ::with_capacity ( buf . len ( ) ) ;
for b in buf {
if * b = = b '\x1b' {
sanitized . extend_from_slice ( " ␛ " . as_bytes ( ) ) ;
} else {
sanitized . push ( * b ) ;
}
}
output . write_all ( & sanitized )
} else {
output . write_all ( buf )
}
}
2022-12-23 04:15:37 +00:00
#[ cfg(test) ]
mod tests {
2023-03-02 06:58:04 +00:00
use std ::str ;
2022-12-23 04:15:37 +00:00
use super ::* ;
2023-01-03 08:07:03 +00:00
fn config_from_string ( text : & str ) -> config ::Config {
config ::Config ::builder ( )
. add_source ( config ::File ::from_str ( text , config ::FileFormat ::Toml ) )
. build ( )
. unwrap ( )
}
2022-12-23 04:15:37 +00:00
#[ test ]
fn test_plaintext_formatter ( ) {
// Test that PlainTextFormatter ignores labels.
let mut output : Vec < u8 > = vec! [ ] ;
let mut formatter = PlainTextFormatter ::new ( & mut output ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " warning " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " hello " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " hello " ) ;
}
2023-01-21 21:55:34 +00:00
#[ test ]
fn test_plaintext_formatter_ansi_codes_in_text ( ) {
// Test that ANSI codes in the input text are NOT escaped.
let mut output : Vec < u8 > = vec! [ ] ;
let mut formatter = PlainTextFormatter ::new ( & mut output ) ;
formatter . write_str ( " \x1b [1mactually bold \x1b [0m " ) . unwrap ( ) ;
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " [1mactually bold [0m" ) ;
}
#[ test ]
fn test_sanitizing_formatter_ansi_codes_in_text ( ) {
// Test that ANSI codes in the input text are escaped.
let mut output : Vec < u8 > = vec! [ ] ;
let mut formatter = SanitizingFormatter ::new ( & mut output ) ;
formatter
. write_str ( " \x1b [1mnot actually bold \x1b [0m " )
. unwrap ( ) ;
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " ␛[1mnot actually bold␛[0m " ) ;
}
2022-12-23 04:15:37 +00:00
#[ test ]
fn test_color_formatter_color_codes ( ) {
// Test the color code for each color.
let colors = [
" black " ,
" red " ,
" green " ,
" yellow " ,
" blue " ,
" magenta " ,
" cyan " ,
" white " ,
" bright black " ,
" bright red " ,
" bright green " ,
" bright yellow " ,
" bright blue " ,
" bright magenta " ,
" bright cyan " ,
" bright white " ,
] ;
2023-01-03 08:07:03 +00:00
let mut config_builder = config ::Config ::builder ( ) ;
for color in colors {
2022-12-23 04:15:37 +00:00
// Use the color name as the label.
2023-01-03 08:07:03 +00:00
config_builder = config_builder
. set_override ( format! ( " colors. {} " , color . replace ( ' ' , " - " ) ) , color )
. unwrap ( ) ;
2022-12-23 04:15:37 +00:00
}
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-03 08:07:03 +00:00
let mut formatter =
2023-01-18 01:20:27 +00:00
ColorFormatter ::for_config ( & mut output , & config_builder . build ( ) . unwrap ( ) ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
for color in colors {
2023-01-12 08:00:12 +00:00
formatter . push_label ( & color . replace ( ' ' , " - " ) ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( & format! ( " {color} " ) ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
}
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ r ###"
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.
2022-12-27 07:23:19 +00:00
[ 38 ; 5 ; 0 m black [ 39 m
[ 38 ; 5 ; 1 m red [ 39 m
[ 38 ; 5 ; 2 m green [ 39 m
[ 38 ; 5 ; 3 m yellow [ 39 m
[ 38 ; 5 ; 4 m blue [ 39 m
[ 38 ; 5 ; 5 m magenta [ 39 m
[ 38 ; 5 ; 6 m cyan [ 39 m
[ 38 ; 5 ; 7 m white [ 39 m
2023-01-07 18:16:57 +00:00
[ 38 ; 5 ; 8 m bright black [ 39 m
[ 38 ; 5 ; 9 m bright red [ 39 m
[ 38 ; 5 ; 10 m bright green [ 39 m
[ 38 ; 5 ; 11 m bright yellow [ 39 m
[ 38 ; 5 ; 12 m bright blue [ 39 m
[ 38 ; 5 ; 13 m bright magenta [ 39 m
[ 38 ; 5 ; 14 m bright cyan [ 39 m
[ 38 ; 5 ; 15 m bright white [ 39 m
2022-12-23 04:15:37 +00:00
" ###);
}
#[ test ]
fn test_color_formatter_single_label ( ) {
// Test that a single label can be colored and that the color is reset
// afterwards.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . inside = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " before " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " inside " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " inside " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " after " ) . unwrap ( ) ;
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.
2022-12-27 07:23:19 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " before [38;5;2m inside [39m after " ) ;
2022-12-23 04:15:37 +00:00
}
2023-01-06 05:58:52 +00:00
#[ test ]
fn test_color_formatter_attributes ( ) {
2023-01-07 17:34:15 +00:00
// Test that each attribute of the style can be set and that they can be
// combined in a single rule or by using multiple rules.
2023-01-06 05:58:52 +00:00
let config = config_from_string (
r #"
colors . red_fg = { fg = " red " }
2023-01-07 17:34:15 +00:00
colors . blue_bg = { bg = " blue " }
2023-01-07 18:03:08 +00:00
colors . bold_font = { bold = true }
2023-01-31 06:51:14 +00:00
colors . underlined_text = { underline = true }
colors . multiple = { fg = " green " , bg = " yellow " , bold = true , underline = true }
2023-01-06 05:58:52 +00:00
" #,
) ;
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " red_fg " ) . unwrap ( ) ;
2023-01-06 05:58:52 +00:00
formatter . write_str ( " fg only " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " blue_bg " ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " bg only " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " bold_font " ) . unwrap ( ) ;
2023-01-07 18:03:08 +00:00
formatter . write_str ( " bold only " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 18:03:08 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " underlined_text " ) . unwrap ( ) ;
2023-01-10 02:59:21 +00:00
formatter . write_str ( " underlined only " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-10 02:59:21 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " multiple " ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " single rule " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " red_fg " ) . unwrap ( ) ;
formatter . push_label ( " blue_bg " ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " two rules " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " \n " ) . unwrap ( ) ;
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ r ###"
[ 38 ; 5 ; 1 m fg only [ 39 m
[ 48 ; 5 ; 4 m bg only [ 49 m
2023-01-07 18:03:08 +00:00
[ 1 m bold only [ 0 m
2023-01-10 02:59:21 +00:00
[ 4 m underlined only [ 24 m
[ 1 m [ 4 m [ 38 ; 5 ; 2 m [ 48 ; 5 ; 3 m single rule [ 0 m
2023-01-13 08:39:22 +00:00
[ 38 ; 5 ; 1 m [ 48 ; 5 ; 4 m two rules [ 39 m [ 49 m
2023-01-07 17:34:15 +00:00
" ###);
}
#[ test ]
fn test_color_formatter_bold_reset ( ) {
// Test that we don't lose other attributes when we reset the bold attribute.
let config = config_from_string (
r #"
2023-01-31 06:51:14 +00:00
colors . not_bold = { fg = " red " , bg = " blue " , underline = true }
2023-01-07 18:03:08 +00:00
colors . bold_font = { bold = true }
2023-01-07 17:34:15 +00:00
" #,
) ;
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " not_bold " ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " not bold " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " bold_font " ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " bold " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-07 17:34:15 +00:00
formatter . write_str ( " not bold again " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-10 02:59:21 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " [4m [38;5;1m [48;5;4m not bold [1m bold [0m [4m [38;5;1m [48;5;4m not bold again [24m [39m [49m" ) ;
2023-01-06 05:58:52 +00:00
}
2022-12-23 04:15:37 +00:00
#[ test ]
fn test_color_formatter_no_space ( ) {
// Test that two different colors can touch.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . red = " red "
colors . green = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " before " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " red " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " first " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
formatter . push_label ( " green " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " second " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " after " ) . unwrap ( ) ;
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.
2022-12-27 07:23:19 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " before [38;5;1mfirst [39m [38;5;2msecond [39mafter " ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_ansi_codes_in_text ( ) {
// Test that ANSI codes in the input text are escaped.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . red = " red "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " red " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter
. write_str ( " \x1b [1mnot actually bold \x1b [0m " )
. unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-19 17:33:16 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " [38;5;1m␛[1mnot actually bold␛[0m [39m" ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_nested ( ) {
// A color can be associated with a combination of labels. A more specific match
// overrides a less specific match. After the inner label is removed, the outer
// color is used again (we don't reset).
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . outer = " blue "
colors . inner = " red "
colors . " outer inner " = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " before outer " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " outer " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " before inner " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " inner " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " inside inner " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " after inner " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " after outer " ) . unwrap ( ) ;
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) ,
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.
2022-12-27 07:23:19 +00:00
@ " before outer [38;5;4m before inner [38;5;2m inside inner [38;5;4m after inner [39m after outer " ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_partial_match ( ) {
// A partial match doesn't count
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . " outer inner " = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " outer " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " not colored " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " inner " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " colored " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " not colored " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) ,
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.
2022-12-27 07:23:19 +00:00
@ " not colored [38;5;2m colored [39m not colored " ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_unrecognized_color ( ) {
// An unrecognized color is ignored; it doesn't reset the color.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . " outer " = " red "
colors . " outer inner " = " bloo "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " outer " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " red before " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " inner " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " still red inside " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " also red afterwards " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) ,
2023-01-09 07:50:18 +00:00
@ " [38;5;1m red before still red inside also red afterwards [39m" ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_sibling ( ) {
// A partial match on one rule does not eliminate other rules.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . " outer1 inner1 " = " red "
colors . inner2 = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " outer1 " ) . unwrap ( ) ;
formatter . push_label ( " inner2 " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " hello " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) ,
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.
2022-12-27 07:23:19 +00:00
@ " [38;5;2m hello [39m" ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_reverse_order ( ) {
// Rules don't match labels out of order
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . " inner outer " = " green "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " outer " ) . unwrap ( ) ;
formatter . push_label ( " inner " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " hello " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
formatter . pop_label ( ) . unwrap ( ) ;
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.
2022-12-27 07:23:19 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) , @ " hello " ) ;
2022-12-23 04:15:37 +00:00
}
#[ test ]
fn test_color_formatter_innermost_wins ( ) {
// When two labels match, the innermost one wins.
2023-01-03 08:07:03 +00:00
let config = config_from_string (
r #"
colors . " a " = " red "
colors . " b " = " green "
colors . " a c " = " blue "
colors . " b c " = " yellow "
" #,
2022-12-23 04:15:37 +00:00
) ;
2023-01-03 08:07:03 +00:00
let mut output : Vec < u8 > = vec! [ ] ;
2023-01-18 01:20:27 +00:00
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " a " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " a1 " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " b " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " b1 " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . push_label ( " c " ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " c " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " b2 " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2022-12-23 04:15:37 +00:00
formatter . write_str ( " a2 " ) . unwrap ( ) ;
2023-01-12 08:00:12 +00:00
formatter . pop_label ( ) . unwrap ( ) ;
2023-01-09 07:50:18 +00:00
insta ::assert_snapshot! ( String ::from_utf8 ( output ) . unwrap ( ) ,
@ " [38;5;1m a1 [38;5;2m b1 [38;5;3m c [38;5;2m b2 [38;5;1m a2 [39m" ) ;
2022-12-23 04:15:37 +00:00
}
2023-03-02 06:58:04 +00:00
#[ test ]
fn test_format_recorder ( ) {
let mut recorder = FormatRecorder ::new ( ) ;
recorder . write_str ( " outer1 " ) . unwrap ( ) ;
recorder . push_label ( " inner " ) . unwrap ( ) ;
recorder . write_str ( " inner1 " ) . unwrap ( ) ;
recorder . write_str ( " inner2 " ) . unwrap ( ) ;
recorder . pop_label ( ) . unwrap ( ) ;
recorder . write_str ( " outer2 " ) . unwrap ( ) ;
insta ::assert_snapshot! (
str ::from_utf8 ( recorder . data ( ) ) . unwrap ( ) ,
@ " outer1 inner1 inner2 outer2 " ) ;
// Replayed output should be labeled.
let config = config_from_string ( r # " colors.inner = "red" "# ) ;
let mut output : Vec < u8 > = vec! [ ] ;
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
recorder . replay ( & mut formatter ) . unwrap ( ) ;
insta ::assert_snapshot! (
String ::from_utf8 ( output ) . unwrap ( ) ,
@ " outer1 [38;5;1m inner1 inner2 [39m outer2 " ) ;
// Replayed output should be split at push/pop_label() call.
let mut output : Vec < u8 > = vec! [ ] ;
let mut formatter = ColorFormatter ::for_config ( & mut output , & config ) . unwrap ( ) ;
recorder
2023-03-05 03:33:40 +00:00
. replay_with ( & mut formatter , | formatter , range | {
let data = & recorder . data ( ) [ range ] ;
2023-03-02 06:58:04 +00:00
write! ( formatter , " <<{}>> " , str ::from_utf8 ( data ) . unwrap ( ) )
} )
. unwrap ( ) ;
insta ::assert_snapshot! (
String ::from_utf8 ( output ) . unwrap ( ) ,
@ " << outer1 >> [38;5;1m<< inner1 inner2 >> [39m<< outer2 >> " ) ;
}
2022-12-23 04:15:37 +00:00
}