use crate::{
config::Style,
print::{Grapheme, UnicodeWidthGraphemes},
};
use nu_ansi_term::{AnsiString, Style as AnsiStyle};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)]
pub struct TextSegment {
style: Option<Style>,
value: String,
}
impl TextSegment {
fn ansi_string(&self, prev: Option<&AnsiStyle>) -> AnsiString<'_> {
match self.style {
Some(style) => style.to_ansi_style(prev).paint(&self.value),
None => AnsiString::from(&self.value),
}
}
}
#[derive(Clone)]
pub struct FillSegment {
style: Option<Style>,
value: String,
}
impl FillSegment {
pub fn ansi_string(&self, width: Option<usize>, prev: Option<&AnsiStyle>) -> 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.to_ansi_style(prev).paint(s),
None => AnsiString::from(s),
}
}
}
#[cfg(test)]
mod fill_seg_tests {
use super::FillSegment;
use nu_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 {
let f = FillSegment {
value: String::from(*text),
style: Some(style.into()),
};
let actual = f.ansi_string(Some(width), None);
assert_eq!(style.paint(*expected), actual);
}
}
}
#[derive(Clone)]
pub enum Segment {
Text(TextSegment),
Fill(FillSegment),
LineTerm,
}
impl Segment {
pub fn from_text<T>(style: Option<Style>, value: T) -> Vec<Self>
where
T: Into<String>,
{
let mut segs: Vec<Self> = Vec::new();
value.into().split(LINE_TERMINATOR).for_each(|s| {
if !segs.is_empty() {
segs.push(Self::LineTerm);
}
segs.push(Self::Text(TextSegment {
value: String::from(s),
style,
}));
});
segs
}
pub fn fill<T>(style: Option<Style>, value: T) -> Self
where
T: Into<String>,
{
Self::Fill(FillSegment {
style,
value: value.into(),
})
}
pub fn style(&self) -> Option<AnsiStyle> {
match self {
Self::Fill(fs) => fs.style.map(|cs| cs.to_ansi_style(None)),
Self::Text(ts) => ts.style.map(|cs| cs.to_ansi_style(None)),
Self::LineTerm => None,
}
}
pub fn set_style_if_empty(&mut self, style: Option<Style>) {
match self {
Self::Fill(fs) => {
if fs.style.is_none() {
fs.style = style;
}
}
Self::Text(ts) => {
if ts.style.is_none() {
ts.style = style;
}
}
Self::LineTerm => {}
}
}
pub fn value(&self) -> &str {
match self {
Self::Fill(fs) => &fs.value,
Self::Text(ts) => &ts.value,
Self::LineTerm => LINE_TERMINATOR_STRING,
}
}
pub fn ansi_string(&self, prev: Option<&AnsiStyle>) -> AnsiString<'_> {
match self {
Self::Fill(fs) => fs.ansi_string(None, prev),
Self::Text(ts) => ts.ansi_string(prev),
Self::LineTerm => AnsiString::from(LINE_TERMINATOR_STRING),
}
}
pub fn width_graphemes(&self) -> usize {
match self {
Self::Fill(fs) => fs.value.width_graphemes(),
Self::Text(ts) => ts.value.width_graphemes(),
Self::LineTerm => 0,
}
}
}
const LINE_TERMINATOR: char = '\n';
const LINE_TERMINATOR_STRING: &str = "\n";