use itertools::Itertools;
use lazy_static;
use regex::Regex;
use std::{borrow::Cow, cell::RefCell, fmt, iter};
use unicode_width::UnicodeWidthChar;
use unicode_linebreak::{linebreaks, BreakOpportunity};
use unicode_width::UnicodeWidthStr;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Alignment {
Left,
Right,
Center,
}
#[derive(Debug, Clone)]
pub struct Cell<'txt> {
pub(crate) content: Cow<'txt, str>,
pub(crate) col_span: usize,
pub(crate) alignment: Alignment,
pub(crate) pad_content: bool,
layout_newlines: RefCell<Option<Vec<usize>>>,
content_without_ansi_esc: Option<String>,
}
impl<'txt> Default for Cell<'txt> {
fn default() -> Self {
Self {
content: Cow::Borrowed(""),
col_span: 1,
alignment: Alignment::Left,
pad_content: true,
layout_newlines: RefCell::new(None),
content_without_ansi_esc: None,
}
}
}
impl<'txt> Cell<'txt> {
fn owned(content: String) -> Cell<'txt> {
let mut this = Self {
content: Cow::Owned(content),
..Default::default()
};
this.update_without_ansi_esc();
this
}
fn borrowed(content: &'txt str) -> Self {
let mut this = Self {
content: Cow::Borrowed(content.as_ref()),
..Default::default()
};
this.update_without_ansi_esc();
this
}
pub fn with_content(mut self, content: impl Into<Cow<'txt, str>>) -> Self {
self.set_content(content);
self
}
pub fn set_content(&mut self, content: impl Into<Cow<'txt, str>>) -> &mut Self {
self.content = content.into();
self.update_without_ansi_esc();
self
}
fn content_for_layout(&self) -> &str {
self.content_without_ansi_esc
.as_ref()
.map(|s| s.as_str())
.unwrap_or(&self.content)
}
fn update_without_ansi_esc(&mut self) {
self.content_without_ansi_esc = if ANSI_ESC_RE.is_match(&self.content) {
Some(ANSI_ESC_RE.split(&self.content).collect())
} else {
None
};
}
pub fn with_col_span(mut self, col_span: usize) -> Self {
self.set_col_span(col_span);
self
}
pub fn set_col_span(&mut self, col_span: usize) -> &mut Self {
assert!(col_span > 0, "cannot have a col_span of 0");
self.col_span = col_span;
*self.layout_newlines.borrow_mut() = None;
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.set_alignment(alignment);
self
}
pub fn set_alignment(&mut self, alignment: Alignment) -> &mut Self {
self.alignment = alignment;
*self.layout_newlines.borrow_mut() = None;
self
}
pub fn with_padding(mut self, padding: bool) -> Self {
self.set_padding(padding);
self
}
pub fn set_padding(&mut self, padding: bool) -> &mut Self {
self.pad_content = padding;
*self.layout_newlines.borrow_mut() = None;
self
}
pub(crate) fn layout(&self, width: Option<usize>) -> usize {
let width = width.unwrap_or(usize::MAX);
if width < 1 || (self.pad_content && width < 3) {
panic!("cell too small to show anything");
}
let content_width = if self.pad_content {
width.saturating_sub(2)
} else {
width
};
let mut ln = self.layout_newlines.borrow_mut();
let ln = ln.get_or_insert(vec![]);
ln.clear();
ln.push(0);
let mut s = self.content_for_layout();
let mut acc = 0;
while let Some(idx) = next_linebreak(s, content_width) {
s = &s[idx..];
ln.push(idx + acc);
acc += idx;
}
ln.pop();
ln.len()
}
pub(crate) fn min_width(&self, only_mandatory: bool) -> usize {
let content = self.content_for_layout();
let max_newline_gap = linebreaks(content).filter_map(|(idx, ty)| {
if only_mandatory && !matches!(ty, BreakOpportunity::Mandatory) {
None
} else {
Some(idx)
}
});
let max_newline_gap = iter::once(0)
.chain(max_newline_gap)
.chain(iter::once(content.len()))
.tuple_windows()
.map(|(start, end)| content[start..end].width())
.max()
.unwrap_or(0);
max_newline_gap + if self.pad_content { 2 } else { 0 }
}
pub(crate) fn width<'s>(
&self,
border_width: usize,
cell_widths: &'s [usize],
) -> (usize, &'s [usize]) {
(
cell_widths[..self.col_span].iter().copied().sum::<usize>()
+ border_width * self.col_span.saturating_sub(1),
&cell_widths[self.col_span..],
)
}
pub(crate) fn render_line(
&self,
line_idx: usize,
width: usize,
f: &mut fmt::Formatter,
) -> fmt::Result {
let newlines = self.layout_newlines.borrow();
let newlines = newlines.as_ref().expect("missed call to `layout`");
let line = match newlines.get(line_idx) {
Some(&start_idx) => match newlines.get(line_idx + 1) {
Some(&end_idx) => &self.content[start_idx..end_idx],
None => &self.content[start_idx..],
},
None => "",
};
let (front_pad, back_pad) = self.get_padding(width, line.width());
let edge = self.edge_char();
f.write_str(edge)?;
for _ in 0..front_pad {
f.write_str(" ")?;
}
f.write_str(line)?;
for _ in 0..back_pad {
f.write_str(" ")?;
}
f.write_str(edge)
}
fn get_padding(&self, width: usize, line_width: usize) -> (usize, usize) {
let padding = if self.pad_content { 2 } else { 0 };
let gap = (width - line_width).saturating_sub(padding);
match self.alignment {
Alignment::Left => (0, gap),
Alignment::Center => (gap / 2, gap - gap / 2),
Alignment::Right => (gap, 0),
}
}
fn edge_char(&self) -> &'static str {
if self.pad_content {
" "
} else {
"\0"
}
}
}
impl<'txt> From<String> for Cell<'txt> {
fn from(other: String) -> Self {
Cell::owned(other)
}
}
impl<'txt> From<&'txt String> for Cell<'txt> {
fn from(other: &'txt String) -> Self {
Cell::borrowed(other)
}
}
impl<'txt> From<&'txt str> for Cell<'txt> {
fn from(other: &'txt str) -> Self {
Cell::borrowed(other)
}
}
lazy_static! {
static ref ANSI_ESC_RE: Regex =
Regex::new(r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]")
.unwrap();
}
fn next_linebreak(text: &str, max_width: usize) -> Option<usize> {
let mut prev = None;
for (idx, ty) in linebreaks(text) {
if text[..idx].width() > max_width {
if let Some(prev) = prev {
return Some(prev);
};
if let Some(linebreak) = next_linebreak_midword(text, max_width) {
return Some(linebreak);
}
return text.chars().next().map(|ch| ch.width()).flatten();
} else if matches!(ty, BreakOpportunity::Mandatory) {
return Some(idx);
} else {
prev = Some(idx);
}
}
None
}
fn next_linebreak_midword(text: &str, max_width: usize) -> Option<usize> {
let mut prev = None;
for (idx, _) in text.char_indices() {
if text[..idx].width() > max_width {
return prev;
} else {
prev = Some(idx);
}
}
unreachable!()
}