use std::cmp;
use std::cmp::Ordering;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::iter;
use std::mem;
use crossterm::style::Attribute;
use crossterm::style::Attributes;
use crossterm::style::Color;
use itertools::Itertools;
use termwiz::cell::Intensity;
use termwiz::color::ColorSpec;
use termwiz::color::RgbColor;
use termwiz::escape::csi::Sgr;
use termwiz::escape::csi::CSI;
use termwiz::escape::Action;
use crate::style::ContentStyle;
use crate::style::StyledContent;
use crate::Dimensions;
use crate::Line;
use crate::Span;
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct Lines(pub Vec<Line>);
#[derive(Default)]
struct ColoredStringParser {
foreground_color: Option<Color>,
background_color: Option<Color>,
attributes: Attributes,
line_buffer: String,
spans: Vec<Span>,
}
impl ColoredStringParser {
fn push_current(&mut self) {
let sc = StyledContent::new(
ContentStyle {
foreground_color: self.foreground_color,
background_color: self.background_color,
attributes: self.attributes,
},
std::mem::take(&mut self.line_buffer),
);
self.spans.push(Span::new_styled_lossy(sc));
}
fn spec_to_color(spec: ColorSpec) -> Option<Color> {
match spec {
ColorSpec::Default => None,
ColorSpec::PaletteIndex(idx) => Some(Color::AnsiValue(idx)),
ColorSpec::TrueColor(srgba) => Some(RgbColor::from(srgba).to_tuple_rgb8().into()),
}
}
fn parse_line(&mut self, parser: &mut termwiz::escape::parser::Parser, s: &str) -> Line {
parser.parse(s.as_bytes(), |a| match a {
Action::Print(c) => {
self.line_buffer.push(c);
}
Action::CSI(CSI::Sgr(Sgr::Reset)) => {
self.push_current();
self.foreground_color = None;
self.background_color = None;
self.attributes = Attributes::default();
}
Action::CSI(CSI::Sgr(Sgr::Intensity(intensity))) => {
self.push_current();
self.attributes = match intensity {
Intensity::Normal => Attributes::default(),
Intensity::Bold => Attributes::from(Attribute::Bold),
Intensity::Half => Attributes::from(Attribute::Dim),
};
}
Action::CSI(CSI::Sgr(Sgr::Foreground(spec))) => {
self.push_current();
self.foreground_color = Self::spec_to_color(spec);
}
Action::CSI(CSI::Sgr(Sgr::Background(spec))) => {
self.push_current();
self.background_color = Self::spec_to_color(spec);
}
_ => {}
});
self.push_current();
Line::from_iter(mem::take(&mut self.spans))
}
}
impl Lines {
pub fn new() -> Lines {
Lines(Vec::new())
}
pub fn from_multiline_string(multiline_string: &str, style: ContentStyle) -> Lines {
multiline_string
.lines()
.map(|line| {
let styled = StyledContent::new(style, line.to_owned());
Line::from_iter([Span::new_styled_lossy(styled)])
})
.collect()
}
pub fn from_colored_multiline_string(multiline_string: &str) -> Lines {
let mut parser = termwiz::escape::parser::Parser::new();
let mut color_parser = ColoredStringParser::default();
multiline_string
.lines()
.map(|s| color_parser.parse_line(&mut parser, s))
.collect()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn push(&mut self, line: Line) {
self.0.push(line);
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = &Line> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> impl ExactSizeIterator<Item = &mut Line> {
self.0.iter_mut()
}
pub fn truncate_lines(&mut self, max_width: usize) {
self.iter_mut()
.for_each(|line| line.truncate_line(max_width));
}
pub fn max_line_length(&self) -> usize {
self.iter()
.map(|line| line.len())
.max()
.unwrap_or_default()
}
pub fn pad_lines_right(&mut self, amount: usize) {
if amount == 0 {
return;
}
let longest_len = self.max_line_length();
for line in self.iter_mut() {
let len = line.len();
line.pad_right(amount + (longest_len - len));
}
}
pub fn pad_lines_left(&mut self, amount: usize) {
if amount == 0 {
return;
}
self.iter_mut().for_each(|line| {
line.pad_left(amount);
});
}
pub fn justify(&mut self) {
let longest_len = self.max_line_length();
for line in self.iter_mut() {
let len = line.len();
line.pad_right(longest_len - len);
}
}
pub fn set_lines_to_exact_width(&mut self, exact_width: usize) {
self.iter_mut()
.for_each(|line| line.to_exact_width(exact_width));
}
pub fn pad_lines_bottom(&mut self, amount: usize) {
let mut extender = iter::repeat(Line::default()).take(amount);
self.0.extend(&mut extender);
}
pub fn pad_lines_top(&mut self, amount: usize) {
let extender = iter::repeat(Line::default()).take(amount);
self.0.splice(0..0, extender);
}
pub fn truncate_lines_bottom(&mut self, desired_length: usize) {
self.0.truncate(desired_length);
}
pub fn set_lines_to_exact_length(&mut self, desired_length: usize) {
match self.len().cmp(&desired_length) {
Ordering::Less => {
self.pad_lines_bottom(desired_length - self.len());
}
Ordering::Equal => {}
Ordering::Greater => {
self.truncate_lines_bottom(desired_length);
}
}
}
pub fn shrink_lines_to_dimensions(&mut self, dimensions: Dimensions) {
self.iter_mut()
.for_each(|line| line.truncate_line(dimensions.width));
self.truncate_lines_bottom(dimensions.height);
}
pub(crate) fn render(
&mut self,
writer: &mut Vec<u8>,
limit: Option<usize>,
) -> anyhow::Result<()> {
let limit = limit.unwrap_or(self.len());
let amt = cmp::min(limit, self.len());
for line in self.0.drain(..amt) {
line.render_with_clear_and_nl(writer)?;
}
Ok(())
}
pub fn dimensions(&self) -> anyhow::Result<Dimensions> {
let x = self.max_line_length();
let y = self.len();
Ok((x, y).into())
}
pub fn set_lines_to_exact_dimensions(&mut self, Dimensions { width, height }: Dimensions) {
self.set_lines_to_exact_length(height);
self.set_lines_to_exact_width(width);
}
pub fn join_horizontally(blocks: Vec<Lines>) -> Lines {
if blocks.is_empty() {
return Lines::new();
}
let longest = blocks.iter().map(|output| output.len()).max().unwrap();
let padded = blocks.into_iter().update(|output| {
output.set_lines_to_exact_length(longest);
output.justify();
});
padded
.reduce(|mut all, output| {
for (all_line, output_line) in all.iter_mut().zip(output.into_iter()) {
all_line.extend(output_line);
}
all
})
.unwrap()
}
pub fn fmt_for_test(&self) -> impl Display + '_ {
struct Impl<'a>(&'a Lines);
impl<'a> Display for Impl<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for line in self.0.iter() {
writeln!(f, "{}", line.fmt_for_test())?;
}
Ok(())
}
}
Impl(self)
}
}
impl FromIterator<Line> for Lines {
fn from_iter<I: IntoIterator<Item = Line>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl IntoIterator for Lines {
type Item = Line;
type IntoIter = <Vec<Line> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use crossterm::style::Attribute;
use crossterm::style::Color;
use super::*;
#[test]
fn truncate_lines() -> anyhow::Result<()> {
let mut test: Lines = Lines(vec![
vec!["test", "line"].try_into()?,
vec!["another one"].try_into()?,
]);
let mut new_test = test.clone();
new_test.truncate_lines(5);
test.0[0] = vec!["test", "l"].try_into()?;
test.0[1] = vec!["anoth"].try_into()?;
assert_eq!(test, new_test);
let mut empty = Lines::new();
empty.truncate_lines(5);
assert_eq!(empty, Lines::new());
Ok(())
}
#[test]
fn test_max_line_length() -> anyhow::Result<()> {
let test = Lines(vec![
Line::default(),
vec!["test", "line"].try_into()?,
vec!["another one"].try_into()?,
]);
assert_eq!(test.max_line_length(), 11);
assert_eq!(Lines::new().max_line_length(), 0);
Ok(())
}
#[test]
fn test_pad_lines_right() -> anyhow::Result<()> {
let mut test = Lines(vec![
vec!["test", "line"].try_into()?, vec!["another one"].try_into()?, Line::default(), ]);
let result = Lines(vec![
vec!["test", "line", &" ".repeat(11 + 3)].try_into()?,
vec!["another one", &" ".repeat(11)].try_into()?,
vec![" ".repeat(11 + 11)].try_into()?,
]);
test.pad_lines_right(11);
assert_eq!(test, result);
Ok(())
}
#[test]
fn test_pad_lines_left() -> anyhow::Result<()> {
let mut test = Lines(vec![
vec!["test", "line"].try_into()?, vec!["another one"].try_into()?, Line::default(), ]);
let result = Lines(vec![
vec![" ".repeat(11).as_ref(), "test", "line"].try_into()?,
vec![" ".repeat(11).as_ref(), "another one"].try_into()?,
vec![" ".repeat(11)].try_into()?,
]);
test.pad_lines_left(11);
assert_eq!(test, result);
Ok(())
}
#[test]
fn test_pad_lines_bottom() -> anyhow::Result<()> {
let mut test = Lines(vec![vec!["test"].try_into()?, vec!["another"].try_into()?]);
test.pad_lines_bottom(3);
let result = Lines(vec![
vec!["test"].try_into()?,
vec!["another"].try_into()?,
Line::default(),
Line::default(),
Line::default(),
]);
assert_eq!(test, result);
Ok(())
}
#[test]
fn test_pad_lines_top() -> anyhow::Result<()> {
let mut test = Lines(vec![vec!["test"].try_into()?, vec!["another"].try_into()?]);
test.pad_lines_top(3);
let result = Lines(vec![
Line::default(),
Line::default(),
Line::default(),
vec!["test"].try_into()?,
vec!["another"].try_into()?,
]);
assert_eq!(test, result);
Ok(())
}
#[test]
fn test_truncate_lines_bottom() -> anyhow::Result<()> {
let mut test = Lines(vec![
vec!["test"].try_into()?,
vec!["another"].try_into()?,
vec!["one more"].try_into()?,
]);
test.truncate_lines_bottom(1);
let output = Lines(vec![vec!["test"].try_into()?]);
assert_eq!(test, output);
Ok(())
}
#[test]
fn test_justify() -> anyhow::Result<()> {
let mut test = Lines(vec![
vec!["test"].try_into()?,
Line::default(),
vec!["ok"].try_into()?,
]);
test.justify();
let expected = Lines(vec![
vec!["test"].try_into()?,
vec![" ".repeat(4)].try_into()?,
vec!["ok", " "].try_into()?,
]);
assert_eq!(test, expected);
Ok(())
}
#[test]
fn test_from_multiline_string() {
let content = "foo bar\n\nbaz\nsome other line";
let style = ContentStyle {
foreground_color: Some(Color::Red),
background_color: None,
attributes: Default::default(),
};
let test = Lines::from_multiline_string(content, style);
let expected = Lines(vec![
Line::from_iter([Span::new_styled_lossy(StyledContent::new(
style,
"foo bar".to_owned(),
))]),
Line::from_iter([Span::new_styled_lossy(StyledContent::new(
style,
"".to_owned(),
))]),
Line::from_iter([Span::new_styled_lossy(StyledContent::new(
style,
"baz".to_owned(),
))]),
Line::from_iter([Span::new_styled_lossy(StyledContent::new(
style,
"some other line".to_owned(),
))]),
]);
assert_eq!(test, expected);
}
#[allow(clippy::from_iter_instead_of_collect)] #[test]
fn test_colored_from_multiline_string() {
let test_string = format!(
"This is a string
That has both {blue}8 bit blue {blue2}(in both formats)
in it,{reset} as well as {high_blue}256 color blue,
{reset}and {rgb_blue}RGB blue as well.{reset} It resets to the
console default at the end.
It can do {rgb_blue}{bg_blue}background colors, {blue}foreground colors,
{bold}colored, and {reset}normal {bold}bold,{remove_bold} and it
strips out {bs}invalid control sequences",
blue = "\x1b[34m",
blue2 = "\x1b[38;5;4m",
high_blue = "\x1b[38;5;20m",
rgb_blue = "\x1b[38;2;0;0;238m",
bg_blue = "\x1b[44m",
reset = "\x1b[0m",
bold = "\x1b[1m",
remove_bold = "\x1b[22m",
bs = "\x1b[D"
);
let default = ContentStyle::default();
let blue = ContentStyle {
foreground_color: Some(Color::AnsiValue(4)),
..Default::default()
};
let high_blue = ContentStyle {
foreground_color: Some(Color::AnsiValue(20)),
..Default::default()
};
let rgb_blue = ContentStyle {
foreground_color: Some(Color::from((0, 0, 238))),
..Default::default()
};
let bg_blue = ContentStyle {
background_color: Some(Color::AnsiValue(4)),
..Default::default()
};
let rgb_blue_and_bg_blue = ContentStyle {
foreground_color: rgb_blue.foreground_color,
background_color: bg_blue.background_color,
..Default::default()
};
let blue_and_bg_blue = ContentStyle {
foreground_color: blue.foreground_color,
background_color: bg_blue.background_color,
..Default::default()
};
let blue_and_bg_blue_bold = ContentStyle {
foreground_color: blue.foreground_color,
background_color: bg_blue.background_color,
attributes: Attributes::from(Attribute::Bold),
};
let bold = ContentStyle {
attributes: Attributes::from(Attribute::Bold),
..Default::default()
};
let expected = vec![
vec![StyledContent::new(default, "This is a string")],
vec![
StyledContent::new(default, "That has both "),
StyledContent::new(blue, "8 bit blue "),
StyledContent::new(blue, "(in both formats)"),
],
vec![
StyledContent::new(blue, "in it,"),
StyledContent::new(default, " as well as "),
StyledContent::new(high_blue, "256 color blue,"),
],
vec![
StyledContent::new(default, "and "),
StyledContent::new(rgb_blue, "RGB blue as well."),
StyledContent::new(default, " It resets to the"),
],
vec![StyledContent::new(default, "console default at the end.")],
vec![
StyledContent::new(default, "It can do "),
StyledContent::new(rgb_blue_and_bg_blue, "background colors, "),
StyledContent::new(blue_and_bg_blue, "foreground colors,"),
],
vec![
StyledContent::new(blue_and_bg_blue_bold, "colored, and "),
StyledContent::new(default, "normal "),
StyledContent::new(bold, "bold,"),
StyledContent::new(default, " and it"),
],
vec![StyledContent::new(
default,
"strips out invalid control sequences",
)],
];
let expected: Lines = expected
.into_iter()
.map(|spans| {
Line::from_iter(spans.iter().map(|sc| {
Span::new_styled_lossy(StyledContent::new(
*sc.style(),
(*sc.content()).to_owned(),
))
}))
})
.collect();
let lines = Lines::from_colored_multiline_string(&test_string);
assert_eq!(expected, lines);
}
#[test]
fn test_fmt_for_test() {
let lines = Lines::from_iter([
Line::unstyled("orange").unwrap(),
Line::from_iter([Span::new_colored("pineapple", Color::Yellow).unwrap()]),
]);
assert_eq!(
"orange\n<span fg=yellow>pineapple</span>\n",
format!("{}", lines.fmt_for_test())
);
}
}