feat(color): add prev_fg and prev_bg as color specifiers based on the previous foreground/background colors respectively (#6017)

feat(color): add prevfg,prevbg as color specifiers based on the previous foreground/background colors respectively

Co-authored-by: Vladimir Lushnikov <vladimir@solidninja.is>
This commit is contained in:
Jovan Gerodetti 2024-06-28 23:40:35 +02:00 committed by GitHub
parent e0281868c9
commit 9a3e87f2cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 250 additions and 94 deletions

View File

@ -358,7 +358,9 @@ Style strings are a list of words, separated by whitespace. The words are not ca
- `<color>`
- `none`
where `<color>` is a color specifier (discussed below). `fg:<color>` and `<color>` currently do the same thing, though this may change in the future. `inverted` swaps the background and foreground colors. The order of words in the string does not matter.
where `<color>` is a color specifier (discussed below). `fg:<color>` and `<color>` currently do the same thing, though this may change in the future.
`<color>` can also be set to `prev_fg` or `prev_bg` which evaluates to the previous item's foreground or background color respectively if available or `none` otherwise.
`inverted` swaps the background and foreground colors. The order of words in the string does not matter.
The `none` token overrides all other tokens in a string if it is not part of a `bg:` specifier, so that e.g. `fg:red none fg:blue` will still create a string with no styling. `bg:none` sets the background to the default color so `fg:red bg:none` is equivalent to `red` or `fg:red` and `bg:green fg:red bg:none` is also equivalent to `fg:red` or `red`. It may become an error to use `none` in conjunction with other tokens in the future.

View File

@ -261,7 +261,7 @@ impl StarshipConfig {
}
/// Deserialize a style string in the starship format with serde
pub fn deserialize_style<'de, D>(de: D) -> Result<nu_ansi_term::Style, D::Error>
pub fn deserialize_style<'de, D>(de: D) -> Result<Style, D::Error>
where
D: Deserializer<'de>,
{
@ -270,6 +270,88 @@ where
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PrevColor {
Fg,
Bg,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
/// Wrapper for `nu_ansi_term::Style` that supports referencing the previous style's foreground/background color.
pub struct Style {
style: nu_ansi_term::Style,
bg: Option<PrevColor>,
fg: Option<PrevColor>,
}
impl Style {
pub fn to_ansi_style(&self, prev: Option<&nu_ansi_term::Style>) -> nu_ansi_term::Style {
let Some(prev_style) = prev else {
return self.style;
};
let mut current = self.style;
if let Some(prev_color) = self.bg {
match prev_color {
PrevColor::Fg => current.background = prev_style.foreground,
PrevColor::Bg => current.background = prev_style.background,
}
}
if let Some(prev_color) = self.fg {
match prev_color {
PrevColor::Fg => current.foreground = prev_style.foreground,
PrevColor::Bg => current.foreground = prev_style.background,
}
}
current
}
fn map_style<F>(&self, f: F) -> Self
where
F: FnOnce(&nu_ansi_term::Style) -> nu_ansi_term::Style,
{
Style {
style: f(&self.style),
..*self
}
}
fn fg(&self, prev_color: PrevColor) -> Self {
Self {
fg: Some(prev_color),
..*self
}
}
fn bg(&self, prev_color: PrevColor) -> Self {
Self {
bg: Some(prev_color),
..*self
}
}
}
impl From<nu_ansi_term::Style> for Style {
fn from(value: nu_ansi_term::Style) -> Self {
Style {
style: value,
..Default::default()
}
}
}
impl From<nu_ansi_term::Color> for Style {
fn from(value: nu_ansi_term::Color) -> Self {
Style {
style: value.into(),
..Default::default()
}
}
}
/** Parse a style string which represents an ansi style. Valid tokens in the style
string include the following:
- 'fg:<color>' (specifies that the color read should be a foreground color)
@ -279,15 +361,14 @@ where
- 'italic'
- 'inverted'
- 'blink'
- 'prev_fg' (specifies the color should be the previous foreground color)
- 'prev_bg' (specifies the color should be the previous background color)
- '<color>' (see the `parse_color_string` doc for valid color strings)
*/
pub fn parse_style_string(
style_string: &str,
context: Option<&Context>,
) -> Option<nu_ansi_term::Style> {
pub fn parse_style_string(style_string: &str, context: Option<&Context>) -> Option<Style> {
style_string
.split_whitespace()
.try_fold(nu_ansi_term::Style::new(), |style, token| {
.try_fold(Style::default(), |style, token| {
let token = token.to_lowercase();
// Check for FG/BG identifiers and strip them off if appropriate
@ -301,14 +382,21 @@ pub fn parse_style_string(
};
match token.as_str() {
"underline" => Some(style.underline()),
"bold" => Some(style.bold()),
"italic" => Some(style.italic()),
"dimmed" => Some(style.dimmed()),
"inverted" => Some(style.reverse()),
"blink" => Some(style.blink()),
"hidden" => Some(style.hidden()),
"strikethrough" => Some(style.strikethrough()),
"underline" => Some(style.map_style(|s| s.underline())),
"bold" => Some(style.map_style(|s| s.bold())),
"italic" => Some(style.map_style(|s| s.italic())),
"dimmed" => Some(style.map_style(|s| s.dimmed())),
"inverted" => Some(style.map_style(|s| s.reverse())),
"blink" => Some(style.map_style(|s| s.blink())),
"hidden" => Some(style.map_style(|s| s.hidden())),
"strikethrough" => Some(style.map_style(|s| s.strikethrough())),
"prev_fg" if col_fg => Some(style.fg(PrevColor::Fg)),
"prev_fg" => Some(style.bg(PrevColor::Fg)),
"prev_bg" if col_fg => Some(style.fg(PrevColor::Bg)),
"prev_bg" => Some(style.bg(PrevColor::Bg)),
// When the string is supposed to be a color:
// Decide if we yield none, reset background or set color.
color_string => {
@ -328,15 +416,15 @@ pub fn parse_style_string(
// bg + invalid color = reset the background to default.
if !col_fg && parsed.is_none() {
let mut new_style = style;
new_style.background = Option::None;
new_style.style.background = Option::None;
Some(new_style)
} else {
// Valid color, apply color to either bg or fg
parsed.map(|ansi_color| {
if col_fg {
style.fg(ansi_color)
style.map_style(|s| s.fg(ansi_color))
} else {
style.on(ansi_color)
style.map_style(|s| s.on(ansi_color))
}
})
}
@ -441,7 +529,7 @@ fn get_palette<'a>(
#[cfg(test)]
mod tests {
use super::*;
use nu_ansi_term::Style;
use nu_ansi_term::Style as AnsiStyle;
// Small wrapper to allow deserializing Style without a struct with #[serde(deserialize_with=)]
#[derive(Default, Clone, Debug, PartialEq)]
@ -514,7 +602,7 @@ mod tests {
git_status_config.modified,
SegmentDisplayConfig {
value: "",
style: Color::Red.normal(),
style: Color::Red.normal().into(),
}
);
}
@ -624,7 +712,7 @@ mod tests {
let config = Value::from("red bold");
assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.bold()
Color::Red.bold().into()
);
}
@ -662,13 +750,13 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.to_ansi_style(None).is_dimmed);
assert_eq!(
mystyle,
nu_ansi_term::Style::new()
mystyle.to_ansi_style(None),
AnsiStyle::new()
.bold()
.italic()
.underline()
@ -681,14 +769,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_inverted_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD InVeRTed");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert!(mystyle.is_reverse);
assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.to_ansi_style(None).is_reverse);
assert_eq!(
mystyle,
nu_ansi_term::Style::new()
mystyle.to_ansi_style(None),
AnsiStyle::new()
.bold()
.italic()
.underline()
@ -702,14 +790,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_blink_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD bLiNk");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert!(mystyle.is_blink);
assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.to_ansi_style(None).is_blink);
assert_eq!(
mystyle,
nu_ansi_term::Style::new()
mystyle.to_ansi_style(None),
AnsiStyle::new()
.bold()
.italic()
.underline()
@ -723,14 +811,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_hidden_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD hIDDen");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert!(mystyle.is_hidden);
assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.to_ansi_style(None).is_hidden);
assert_eq!(
mystyle,
nu_ansi_term::Style::new()
mystyle.to_ansi_style(None),
AnsiStyle::new()
.bold()
.italic()
.underline()
@ -744,14 +832,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_strikethrough_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD StRiKEthROUgh");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold);
assert!(mystyle.is_italic);
assert!(mystyle.is_underline);
assert!(mystyle.is_dimmed);
assert!(mystyle.is_strikethrough);
assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.to_ansi_style(None).is_strikethrough);
assert_eq!(
mystyle,
nu_ansi_term::Style::new()
mystyle.to_ansi_style(None),
AnsiStyle::new()
.bold()
.italic()
.underline()
@ -766,7 +854,7 @@ mod tests {
// Test a "plain" style with no formatting
let config = Value::from("");
let plain_style = <StyleWrapper>::from_config(&config).unwrap().0;
assert_eq!(plain_style, nu_ansi_term::Style::new());
assert_eq!(plain_style.to_ansi_style(None), AnsiStyle::new());
// Test a string that's clearly broken
let config = Value::from("djklgfhjkldhlhk;j");
@ -803,21 +891,84 @@ mod tests {
let config = Value::from("fg:red bg:none");
assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.normal()
Color::Red.normal().into()
);
// Test that bg:none will yield a style
let config = Value::from("fg:red bg:none bold");
assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.bold()
Color::Red.bold().into()
);
// Test that bg:none will overwrite the previous background colour
let config = Value::from("fg:red bg:green bold bg:none");
assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.bold()
Color::Red.bold().into()
);
}
#[test]
fn table_get_styles_previous() {
// Test that previous has no effect when there is no previous style
let both_prevfg = <StyleWrapper>::from_config(&Value::from(
"bold fg:black fg:prev_bg bg:prev_fg underline",
))
.unwrap()
.0;
assert_eq!(
both_prevfg.to_ansi_style(None),
AnsiStyle::default().fg(Color::Black).bold().underline()
);
// But if there is a style on the previous string, then use that
let prev_style = AnsiStyle::new()
.underline()
.fg(Color::Yellow)
.on(Color::Red);
assert_eq!(
both_prevfg.to_ansi_style(Some(&prev_style)),
AnsiStyle::new()
.fg(Color::Red)
.on(Color::Yellow)
.bold()
.underline()
);
// Test that all the combinations of previous colors work
let fg_prev_fg = <StyleWrapper>::from_config(&Value::from("fg:prev_fg"))
.unwrap()
.0;
assert_eq!(
fg_prev_fg.to_ansi_style(Some(&prev_style)),
AnsiStyle::new().fg(Color::Yellow)
);
let fg_prev_bg = <StyleWrapper>::from_config(&Value::from("fg:prev_bg"))
.unwrap()
.0;
assert_eq!(
fg_prev_bg.to_ansi_style(Some(&prev_style)),
AnsiStyle::new().fg(Color::Red)
);
let bg_prev_fg = <StyleWrapper>::from_config(&Value::from("bg:prev_fg"))
.unwrap()
.0;
assert_eq!(
bg_prev_fg.to_ansi_style(Some(&prev_style)),
AnsiStyle::new().on(Color::Yellow)
);
let bg_prev_bg = <StyleWrapper>::from_config(&Value::from("bg:prev_bg"))
.unwrap()
.0;
assert_eq!(
bg_prev_bg.to_ansi_style(Some(&prev_style)),
AnsiStyle::new().on(Color::Red)
);
}
@ -827,8 +978,8 @@ mod tests {
let config = Value::from("bg:#050505 underline fg:120");
let flipped_style = <StyleWrapper>::from_config(&config).unwrap().0;
assert_eq!(
flipped_style,
Style::new()
flipped_style.to_ansi_style(None),
AnsiStyle::new()
.underline()
.fg(Color::Fixed(120))
.on(Color::Rgb(5, 5, 5))
@ -838,8 +989,8 @@ mod tests {
let config = Value::from("bg:120 bg:125 bg:127 fg:127 122 125");
let multi_style = <StyleWrapper>::from_config(&config).unwrap().0;
assert_eq!(
multi_style,
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
multi_style.to_ansi_style(None),
AnsiStyle::new().fg(Color::Fixed(125)).on(Color::Fixed(127))
);
}

View File

@ -1,4 +1,3 @@
use nu_ansi_term::Style;
use pest::error::Error as PestError;
use rayon::prelude::*;
use std::borrow::Cow;
@ -6,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt;
use crate::config::parse_style_string;
use crate::config::{parse_style_string, Style};
use crate::context::{Context, Shell};
use crate::segment::Segment;
@ -487,7 +486,7 @@ mod tests {
let style = Some(Color::Red.bold());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper);
let result = formatter.parse(style, None).unwrap();
let result = formatter.parse(style.map(|s| s.into()), None).unwrap();
let mut result_iter = result.iter();
match_next!(result_iter, "text", style);
}
@ -562,7 +561,9 @@ mod tests {
let inner_style = Some(Color::Blue.normal());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper);
let result = formatter.parse(outer_style, None).unwrap();
let result = formatter
.parse(outer_style.map(|s| s.into()), None)
.unwrap();
let mut result_iter = result.iter();
match_next!(result_iter, "outer ", outer_style);
match_next!(result_iter, "middle ", middle_style);
@ -618,9 +619,9 @@ mod tests {
let mut segments: Vec<Segment> = Vec::new();
segments.extend(Segment::from_text(None, "styless"));
segments.extend(Segment::from_text(styled_style, "styled"));
segments.extend(Segment::from_text(styled_style.map(|s| s.into()), "styled"));
segments.extend(Segment::from_text(
styled_no_modifier_style,
styled_no_modifier_style.map(|s| s.into()),
"styled_no_modifier",
));

View File

@ -1,6 +1,6 @@
use crate::segment;
use crate::segment::{FillSegment, Segment};
use nu_ansi_term::{AnsiString, AnsiStrings};
use nu_ansi_term::{AnsiString, AnsiStrings, Style as AnsiStyle};
use std::fmt;
use std::time::Duration;
@ -190,16 +190,21 @@ where
let mut used = 0usize;
let mut current: Vec<AnsiString> = Vec::new();
let mut chunks: Vec<(Vec<AnsiString>, &FillSegment)> = Vec::new();
let mut prev_style: Option<AnsiStyle> = None;
for segment in segments {
match segment {
Segment::Fill(fs) => {
chunks.push((current, fs));
current = Vec::new();
prev_style = None;
}
_ => {
used += segment.width_graphemes();
current.push(segment.ansi_string());
let current_segment_string = segment.ansi_string(prev_style.as_ref());
prev_style = Some(*current_segment_string.style_ref());
current.push(current_segment_string);
}
}
@ -217,8 +222,9 @@ where
chunks
.into_iter()
.flat_map(|(strs, fill)| {
strs.into_iter()
.chain(std::iter::once(fill.ansi_string(fill_size)))
let fill_string =
fill.ansi_string(fill_size, strs.last().map(|segment| segment.style_ref()));
strs.into_iter().chain(std::iter::once(fill_string))
})
.chain(current)
.collect::<Vec<AnsiString>>()

View File

@ -1,6 +1,8 @@
use crate::print::{Grapheme, UnicodeWidthGraphemes};
use nu_ansi_term::{AnsiString, Style};
use std::fmt;
use crate::{
config::Style,
print::{Grapheme, UnicodeWidthGraphemes},
};
use nu_ansi_term::{AnsiString, Style as AnsiStyle};
use unicode_segmentation::UnicodeSegmentation;
/// Type that holds text with an associated style
@ -15,9 +17,9 @@ pub struct TextSegment {
impl TextSegment {
// Returns the AnsiString of the segment value
fn ansi_string(&self) -> AnsiString {
fn ansi_string(&self, prev: Option<&AnsiStyle>) -> AnsiString {
match self.style {
Some(style) => style.paint(&self.value),
Some(style) => style.to_ansi_style(prev).paint(&self.value),
None => AnsiString::from(&self.value),
}
}
@ -35,7 +37,7 @@ pub struct FillSegment {
impl FillSegment {
// Returns the AnsiString of the segment value, not including its prefix and suffix
pub fn ansi_string(&self, width: Option<usize>) -> AnsiString {
pub fn ansi_string(&self, width: Option<usize>, prev: Option<&AnsiStyle>) -> AnsiString {
let s = match width {
Some(w) => self
.value
@ -53,7 +55,7 @@ impl FillSegment {
None => String::from(&self.value),
};
match self.style {
Some(style) => style.paint(s),
Some(style) => style.to_ansi_style(prev).paint(s),
None => AnsiString::from(s),
}
}
@ -80,9 +82,9 @@ mod fill_seg_tests {
for (text, expected) in &inputs {
let f = FillSegment {
value: String::from(*text),
style: Some(style),
style: Some(style.into()),
};
let actual = f.ansi_string(Some(width));
let actual = f.ansi_string(Some(width), None);
assert_eq!(style.paint(*expected), actual);
}
}
@ -126,10 +128,10 @@ impl Segment {
})
}
pub fn style(&self) -> Option<Style> {
pub fn style(&self) -> Option<AnsiStyle> {
match self {
Self::Fill(fs) => fs.style,
Self::Text(ts) => ts.style,
Self::Fill(fs) => fs.style.map(|cs| cs.to_ansi_style(None).to_owned()),
Self::Text(ts) => ts.style.map(|cs| cs.to_ansi_style(None).to_owned()),
Self::LineTerm => None,
}
}
@ -159,10 +161,10 @@ impl Segment {
}
// Returns the AnsiString of the segment value, not including its prefix and suffix
pub fn ansi_string(&self) -> AnsiString {
pub fn ansi_string(&self, prev: Option<&AnsiStyle>) -> AnsiString {
match self {
Self::Fill(fs) => fs.ansi_string(None),
Self::Text(ts) => ts.ansi_string(),
Self::Fill(fs) => fs.ansi_string(None, prev),
Self::Text(ts) => ts.ansi_string(prev),
Self::LineTerm => AnsiString::from(LINE_TERMINATOR_STRING),
}
}
@ -178,9 +180,3 @@ impl Segment {
const LINE_TERMINATOR: char = '\n';
const LINE_TERMINATOR_STRING: &str = "\n";
impl fmt::Display for Segment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.ansi_string())
}
}