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>` - `<color>`
- `none` - `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. 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 /// 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 where
D: Deserializer<'de>, 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 /** Parse a style string which represents an ansi style. Valid tokens in the style
string include the following: string include the following:
- 'fg:<color>' (specifies that the color read should be a foreground color) - 'fg:<color>' (specifies that the color read should be a foreground color)
@ -279,15 +361,14 @@ where
- 'italic' - 'italic'
- 'inverted' - 'inverted'
- 'blink' - '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) - '<color>' (see the `parse_color_string` doc for valid color strings)
*/ */
pub fn parse_style_string( pub fn parse_style_string(style_string: &str, context: Option<&Context>) -> Option<Style> {
style_string: &str,
context: Option<&Context>,
) -> Option<nu_ansi_term::Style> {
style_string style_string
.split_whitespace() .split_whitespace()
.try_fold(nu_ansi_term::Style::new(), |style, token| { .try_fold(Style::default(), |style, token| {
let token = token.to_lowercase(); let token = token.to_lowercase();
// Check for FG/BG identifiers and strip them off if appropriate // Check for FG/BG identifiers and strip them off if appropriate
@ -301,14 +382,21 @@ pub fn parse_style_string(
}; };
match token.as_str() { match token.as_str() {
"underline" => Some(style.underline()), "underline" => Some(style.map_style(|s| s.underline())),
"bold" => Some(style.bold()), "bold" => Some(style.map_style(|s| s.bold())),
"italic" => Some(style.italic()), "italic" => Some(style.map_style(|s| s.italic())),
"dimmed" => Some(style.dimmed()), "dimmed" => Some(style.map_style(|s| s.dimmed())),
"inverted" => Some(style.reverse()), "inverted" => Some(style.map_style(|s| s.reverse())),
"blink" => Some(style.blink()), "blink" => Some(style.map_style(|s| s.blink())),
"hidden" => Some(style.hidden()), "hidden" => Some(style.map_style(|s| s.hidden())),
"strikethrough" => Some(style.strikethrough()), "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: // When the string is supposed to be a color:
// Decide if we yield none, reset background or set color. // Decide if we yield none, reset background or set color.
color_string => { color_string => {
@ -328,15 +416,15 @@ pub fn parse_style_string(
// bg + invalid color = reset the background to default. // bg + invalid color = reset the background to default.
if !col_fg && parsed.is_none() { if !col_fg && parsed.is_none() {
let mut new_style = style; let mut new_style = style;
new_style.background = Option::None; new_style.style.background = Option::None;
Some(new_style) Some(new_style)
} else { } else {
// Valid color, apply color to either bg or fg // Valid color, apply color to either bg or fg
parsed.map(|ansi_color| { parsed.map(|ansi_color| {
if col_fg { if col_fg {
style.fg(ansi_color) style.map_style(|s| s.fg(ansi_color))
} else { } else {
style.on(ansi_color) style.map_style(|s| s.on(ansi_color))
} }
}) })
} }
@ -441,7 +529,7 @@ fn get_palette<'a>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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=)] // Small wrapper to allow deserializing Style without a struct with #[serde(deserialize_with=)]
#[derive(Default, Clone, Debug, PartialEq)] #[derive(Default, Clone, Debug, PartialEq)]
@ -514,7 +602,7 @@ mod tests {
git_status_config.modified, git_status_config.modified,
SegmentDisplayConfig { SegmentDisplayConfig {
value: "", value: "",
style: Color::Red.normal(), style: Color::Red.normal().into(),
} }
); );
} }
@ -624,7 +712,7 @@ mod tests {
let config = Value::from("red bold"); let config = Value::from("red bold");
assert_eq!( assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0, <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() { fn table_get_styles_bold_italic_underline_green_dimmed_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD"); let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0; let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold); assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.is_italic); assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.is_underline); assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.is_dimmed); assert!(mystyle.to_ansi_style(None).is_dimmed);
assert_eq!( assert_eq!(
mystyle, mystyle.to_ansi_style(None),
nu_ansi_term::Style::new() AnsiStyle::new()
.bold() .bold()
.italic() .italic()
.underline() .underline()
@ -681,14 +769,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_inverted_silly_caps() { fn table_get_styles_bold_italic_underline_green_dimmed_inverted_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD InVeRTed"); let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD InVeRTed");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0; let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold); assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.is_italic); assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.is_underline); assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.is_dimmed); assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.is_reverse); assert!(mystyle.to_ansi_style(None).is_reverse);
assert_eq!( assert_eq!(
mystyle, mystyle.to_ansi_style(None),
nu_ansi_term::Style::new() AnsiStyle::new()
.bold() .bold()
.italic() .italic()
.underline() .underline()
@ -702,14 +790,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_blink_silly_caps() { fn table_get_styles_bold_italic_underline_green_dimmed_blink_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD bLiNk"); let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD bLiNk");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0; let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold); assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.is_italic); assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.is_underline); assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.is_dimmed); assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.is_blink); assert!(mystyle.to_ansi_style(None).is_blink);
assert_eq!( assert_eq!(
mystyle, mystyle.to_ansi_style(None),
nu_ansi_term::Style::new() AnsiStyle::new()
.bold() .bold()
.italic() .italic()
.underline() .underline()
@ -723,14 +811,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_hidden_silly_caps() { fn table_get_styles_bold_italic_underline_green_dimmed_hidden_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD hIDDen"); let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD hIDDen");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0; let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold); assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.is_italic); assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.is_underline); assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.is_dimmed); assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.is_hidden); assert!(mystyle.to_ansi_style(None).is_hidden);
assert_eq!( assert_eq!(
mystyle, mystyle.to_ansi_style(None),
nu_ansi_term::Style::new() AnsiStyle::new()
.bold() .bold()
.italic() .italic()
.underline() .underline()
@ -744,14 +832,14 @@ mod tests {
fn table_get_styles_bold_italic_underline_green_dimmed_strikethrough_silly_caps() { fn table_get_styles_bold_italic_underline_green_dimmed_strikethrough_silly_caps() {
let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD StRiKEthROUgh"); let config = Value::from("bOlD ItAlIc uNdErLiNe GrEeN diMMeD StRiKEthROUgh");
let mystyle = <StyleWrapper>::from_config(&config).unwrap().0; let mystyle = <StyleWrapper>::from_config(&config).unwrap().0;
assert!(mystyle.is_bold); assert!(mystyle.to_ansi_style(None).is_bold);
assert!(mystyle.is_italic); assert!(mystyle.to_ansi_style(None).is_italic);
assert!(mystyle.is_underline); assert!(mystyle.to_ansi_style(None).is_underline);
assert!(mystyle.is_dimmed); assert!(mystyle.to_ansi_style(None).is_dimmed);
assert!(mystyle.is_strikethrough); assert!(mystyle.to_ansi_style(None).is_strikethrough);
assert_eq!( assert_eq!(
mystyle, mystyle.to_ansi_style(None),
nu_ansi_term::Style::new() AnsiStyle::new()
.bold() .bold()
.italic() .italic()
.underline() .underline()
@ -766,7 +854,7 @@ mod tests {
// Test a "plain" style with no formatting // Test a "plain" style with no formatting
let config = Value::from(""); let config = Value::from("");
let plain_style = <StyleWrapper>::from_config(&config).unwrap().0; 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 // Test a string that's clearly broken
let config = Value::from("djklgfhjkldhlhk;j"); let config = Value::from("djklgfhjkldhlhk;j");
@ -803,21 +891,84 @@ mod tests {
let config = Value::from("fg:red bg:none"); let config = Value::from("fg:red bg:none");
assert_eq!( assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0, <StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.normal() Color::Red.normal().into()
); );
// Test that bg:none will yield a style // Test that bg:none will yield a style
let config = Value::from("fg:red bg:none bold"); let config = Value::from("fg:red bg:none bold");
assert_eq!( assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0, <StyleWrapper>::from_config(&config).unwrap().0,
Color::Red.bold() Color::Red.bold().into()
); );
// Test that bg:none will overwrite the previous background colour // Test that bg:none will overwrite the previous background colour
let config = Value::from("fg:red bg:green bold bg:none"); let config = Value::from("fg:red bg:green bold bg:none");
assert_eq!( assert_eq!(
<StyleWrapper>::from_config(&config).unwrap().0, <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 config = Value::from("bg:#050505 underline fg:120");
let flipped_style = <StyleWrapper>::from_config(&config).unwrap().0; let flipped_style = <StyleWrapper>::from_config(&config).unwrap().0;
assert_eq!( assert_eq!(
flipped_style, flipped_style.to_ansi_style(None),
Style::new() AnsiStyle::new()
.underline() .underline()
.fg(Color::Fixed(120)) .fg(Color::Fixed(120))
.on(Color::Rgb(5, 5, 5)) .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 config = Value::from("bg:120 bg:125 bg:127 fg:127 122 125");
let multi_style = <StyleWrapper>::from_config(&config).unwrap().0; let multi_style = <StyleWrapper>::from_config(&config).unwrap().0;
assert_eq!( assert_eq!(
multi_style, multi_style.to_ansi_style(None),
Style::new().fg(Color::Fixed(125)).on(Color::Fixed(127)) 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 pest::error::Error as PestError;
use rayon::prelude::*; use rayon::prelude::*;
use std::borrow::Cow; use std::borrow::Cow;
@ -6,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use crate::config::parse_style_string; use crate::config::{parse_style_string, Style};
use crate::context::{Context, Shell}; use crate::context::{Context, Shell};
use crate::segment::Segment; use crate::segment::Segment;
@ -487,7 +486,7 @@ mod tests {
let style = Some(Color::Red.bold()); let style = Some(Color::Red.bold());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, "text", style); match_next!(result_iter, "text", style);
} }
@ -562,7 +561,9 @@ mod tests {
let inner_style = Some(Color::Blue.normal()); let inner_style = Some(Color::Blue.normal());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, "outer ", outer_style); match_next!(result_iter, "outer ", outer_style);
match_next!(result_iter, "middle ", middle_style); match_next!(result_iter, "middle ", middle_style);
@ -618,9 +619,9 @@ mod tests {
let mut segments: Vec<Segment> = Vec::new(); let mut segments: Vec<Segment> = Vec::new();
segments.extend(Segment::from_text(None, "styless")); 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( segments.extend(Segment::from_text(
styled_no_modifier_style, styled_no_modifier_style.map(|s| s.into()),
"styled_no_modifier", "styled_no_modifier",
)); ));

View File

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

View File

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