diff options
author | Matthew (Matt) Jeffryes <github@mjeffryes.net> | 2021-09-12 16:59:15 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-12 19:59:15 -0400 |
commit | 5d0a38aca3fdc8133314bf8fc24830c8e4b9eabd (patch) | |
tree | 4c68f189d0537a613d6aefa62c30fcd96414e40f /src/segment.rs | |
parent | 5ac7ad741fdcb199671c63ae215a06f216fa78b8 (diff) |
feat: Add a fill module to pad out the line (#3029)
Diffstat (limited to 'src/segment.rs')
-rw-r--r-- | src/segment.rs | 169 |
1 files changed, 157 insertions, 12 deletions
diff --git a/src/segment.rs b/src/segment.rs index 200a93b9e..643298c2d 100644 --- a/src/segment.rs +++ b/src/segment.rs @@ -1,39 +1,184 @@ +use crate::print::{Grapheme, UnicodeWidthGraphemes}; use ansi_term::{ANSIString, Style}; use std::fmt; +use unicode_segmentation::UnicodeSegmentation; -/// A segment is a single configurable element in a module. This will usually -/// contain a data point to provide context for the prompt's user -/// (e.g. The version that software is running). +/// Type that holds text with an associated style #[derive(Clone)] -pub struct Segment { +pub struct TextSegment { /// The segment's style. If None, will inherit the style of the module containing it. - pub style: Option<Style>, + style: Option<Style>, /// The string value of the current segment. - pub value: String, + value: String, +} + +impl TextSegment { + // Returns the ANSIString of the segment value + fn ansi_string(&self) -> ANSIString { + match self.style { + Some(style) => style.paint(&self.value), + None => ANSIString::from(&self.value), + } + } +} + +/// Type that holds fill text with an associated style +#[derive(Clone)] +pub struct FillSegment { + /// The segment's style. If None, will inherit the style of the module containing it. + style: Option<Style>, + + /// The string value of the current segment. + value: String, +} + +impl FillSegment { + // Returns the ANSIString of the segment value, not including its prefix and suffix + pub fn ansi_string(&self, width: Option<usize>) -> ANSIString { + let s = match width { + Some(w) => self + .value + .graphemes(true) + .cycle() + .scan(0usize, |len, g| { + *len += Grapheme(g).width(); + if *len <= w { + Some(g) + } else { + None + } + }) + .collect::<String>(), + None => String::from(&self.value), + }; + match self.style { + Some(style) => style.paint(s), + None => ANSIString::from(s), + } + } +} + +#[cfg(test)] +mod fill_seg_tests { + use super::FillSegment; + use ansi_term::Color; + + #[test] + fn ansi_string_width() { + let width: usize = 10; + let style = Color::Blue.bold(); + + let inputs = vec![ + (".", ".........."), + (".:", ".:.:.:.:.:"), + ("-:-", "-:--:--:--"), + ("🟦", "🟦🟦🟦🟦🟦"), + ("🟢🔵🟡", "🟢🔵🟡🟢🔵"), + ]; + + for (text, expected) in inputs.iter() { + let f = FillSegment { + value: String::from(*text), + style: Some(style), + }; + let actual = f.ansi_string(Some(width)); + assert_eq!(style.paint(*expected), actual); + } + } +} + +/// A segment is a styled text chunk ready for printing. +#[derive(Clone)] +pub enum Segment { + Text(TextSegment), + Fill(FillSegment), + LineTerm, } impl Segment { - /// Creates a new segment. - pub fn new<T>(style: Option<Style>, value: T) -> Self + /// Creates new segments from a text with a style; breaking out LineTerminators. + pub fn from_text<T>(style: Option<Style>, value: T) -> Vec<Segment> + where + T: Into<String>, + { + let mut segs: Vec<Segment> = Vec::new(); + value.into().split(LINE_TERMINATOR).for_each(|s| { + if !segs.is_empty() { + segs.push(Segment::LineTerm) + } + segs.push(Segment::Text(TextSegment { + value: String::from(s), + style, + })) + }); + segs + } + + /// Creates a new fill segment + pub fn fill<T>(style: Option<Style>, value: T) -> Self where T: Into<String>, { - Self { + Segment::Fill(FillSegment { style, value: value.into(), + }) + } + + pub fn style(&self) -> Option<Style> { + match self { + Segment::Fill(fs) => fs.style, + Segment::Text(ts) => ts.style, + Segment::LineTerm => None, + } + } + + pub fn set_style_if_empty(&mut self, style: Option<Style>) { + match self { + Segment::Fill(fs) => { + if fs.style.is_none() { + fs.style = style + } + } + Segment::Text(ts) => { + if ts.style.is_none() { + ts.style = style + } + } + Segment::LineTerm => {} + } + } + + pub fn value(&self) -> &str { + match self { + Segment::Fill(fs) => &fs.value, + Segment::Text(ts) => &ts.value, + Segment::LineTerm => LINE_TERMINATOR_STRING, } } // Returns the ANSIString of the segment value, not including its prefix and suffix pub fn ansi_string(&self) -> ANSIString { - match self.style { - Some(style) => style.paint(&self.value), - None => ANSIString::from(&self.value), + match self { + Segment::Fill(fs) => fs.ansi_string(None), + Segment::Text(ts) => ts.ansi_string(), + Segment::LineTerm => ANSIString::from(LINE_TERMINATOR_STRING), + } + } + + pub fn width_graphemes(&self) -> usize { + match self { + Segment::Fill(fs) => fs.value.width_graphemes(), + Segment::Text(ts) => ts.value.width_graphemes(), + Segment::LineTerm => 0, } } } +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()) |