use rich::cells::cell_len;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
use crate::render::{MicroMeta, PAD_CELL};
#[derive(Clone, Debug)]
pub(crate) struct Run {
pub meta: MicroMeta,
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
pub width: usize,
}
pub(crate) fn runs(segments: &[Segment]) -> Vec<Run> {
let mut out = Vec::new();
let (mut line, mut column) = (0, 0);
let mut index = 0;
while index < segments.len() {
let segment = &segments[index];
let Some(meta) = MicroMeta::from_segment(segment) else {
if !segment.control {
match segment.text.rfind('\n') {
Some(last) => {
line += segment.text.matches('\n').count();
column = cell_len(&segment.text[last + 1..]);
}
None => column += segment.cell_length(),
}
}
index += 1;
continue;
};
let start = index;
let mut width = 0;
while index < segments.len()
&& MicroMeta::from_segment(&segments[index])
.is_some_and(|m| m.id == meta.id && m.row == meta.row)
{
width += segments[index].cell_length();
index += 1;
}
out.push(Run {
meta,
start,
end: index,
line,
column,
width,
});
column += width;
}
out
}
fn split(segments: Vec<Segment>) -> (Vec<Vec<Segment>>, Vec<Segment>) {
let mut lines = vec![Vec::new()];
let mut breaks = Vec::new();
for segment in segments {
if segment.control || !segment.text.contains('\n') {
lines.last_mut().expect("a line").push(segment);
continue;
}
for (index, part) in segment.text.split('\n').enumerate() {
if index > 0 {
breaks.push(Segment::new("\n", segment.style.clone()));
lines.push(Vec::new());
}
if !part.is_empty() {
lines
.last_mut()
.expect("a line")
.push(Segment::new(part, segment.style.clone()));
}
}
}
(lines, breaks)
}
fn background(style: Option<&Style>) -> Option<Style> {
let color = style?.bgcolor()?.clone();
Some(Style::new().with_bgcolor(color))
}
fn blank(out: &mut Vec<Segment>, width: usize, style: Option<Style>) {
if width == 0 {
return;
}
if let Some(last) = out.last_mut() {
let is_blank = !last.control
&& last.style == style
&& MicroMeta::from_segment(last).is_none()
&& last.text.bytes().all(|b| b == b' ');
if is_blank {
last.text.push_str(&" ".repeat(width));
return;
}
}
out.push(Segment::new(" ".repeat(width), style));
}
fn reserved(line: &[Segment], pending: &[&Run], row: usize) -> Vec<Segment> {
let mut out: Vec<Segment> = Vec::new();
let mut index = 0;
while index < line.len() {
if let Some(run) = pending.iter().find(|run| run.start == index) {
let under = background(line[run.start].style.as_ref());
if run.meta.rows > row {
let tag = run.meta.with_row(row).to_style();
let style = under.map_or(tag.clone(), |under| under.combine(&tag));
out.push(Segment::new(
PAD_CELL.to_string().repeat(run.meta.cols),
Some(style),
));
} else {
blank(&mut out, run.width, under);
}
index = run.end;
continue;
}
let segment = &line[index];
if !segment.control {
blank(
&mut out,
segment.cell_length(),
background(segment.style.as_ref()),
);
}
index += 1;
}
if out
.last()
.is_some_and(|last| last.style.is_none() && last.text.bytes().all(|b| b == b' '))
{
out.pop();
}
out
}
pub fn reserve_rows(segments: Vec<Segment>) -> Vec<Segment> {
let multi_row = segments.iter().any(|segment| {
MicroMeta::from_segment(segment).is_some_and(|meta| meta.row == 0 && meta.rows > 1)
});
if !multi_row {
return segments;
}
let (lines, breaks) = split(segments);
let found: Vec<Vec<Run>> = lines.iter().map(|line| runs(line)).collect();
let mut out = Vec::new();
for (index, line) in lines.iter().enumerate() {
let below = found.get(index + 1);
let pending: Vec<&Run> = found[index]
.iter()
.filter(|run| {
run.meta.row == 0
&& run.meta.rows > 1
&& run.width == run.meta.cols
&& !below.is_some_and(|below| {
below.iter().any(|other| {
other.meta.id == run.meta.id
&& other.meta.row == 1
&& other.column == run.column
})
})
})
.collect();
out.extend(line.iter().cloned());
let extra = pending.iter().map(|run| run.meta.rows - 1).max();
for row in 1..=extra.unwrap_or(0) {
out.push(Segment::line());
out.extend(reserved(line, &pending, row));
}
if let Some(line_break) = breaks.get(index) {
out.push(line_break.clone());
}
}
out
}
#[derive(Clone, Debug)]
pub struct MicroRows<R> {
inner: R,
}
impl<R> MicroRows<R> {
pub fn new(inner: R) -> Self {
MicroRows { inner }
}
pub fn inner(&self) -> &R {
&self.inner
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R: Renderable> Renderable for MicroRows<R> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
reserve_rows(self.inner.rich_render(console, options))
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.inner.measure(console, options)
}
fn fit_to_measurement(&self) -> bool {
self.inner.fit_to_measurement()
}
fn vertical(&self) -> Option<rich::align::VerticalAlign> {
self.inner.vertical()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tagged(text: &str, meta: &MicroMeta) -> Segment {
Segment::new(text, Some(meta.to_style()))
}
fn meta(rows: usize, id: u64) -> MicroMeta {
MicroMeta {
name: "a".into(),
cols: 2,
rows,
id,
row: 0,
}
}
fn plain(segments: &[Segment]) -> String {
segments.iter().map(|s| s.text.as_str()).collect()
}
#[test]
fn runs_know_their_line_and_column() {
let one = meta(1, 1);
let two = meta(2, 2);
let segments = vec![
Segment::new("ab ", None),
tagged("x", &one),
tagged("y", &one),
Segment::new(" c\nd", None),
tagged("zz", &two),
Segment::control("\x1b[0m"),
tagged("zz", &two),
];
let found = runs(&segments);
let shape: Vec<(u64, usize, usize, usize, usize)> = found
.iter()
.map(|r| (r.meta.id, r.line, r.column, r.width, r.end - r.start))
.collect();
assert_eq!(
shape,
[(1, 0, 3, 2, 2), (2, 1, 1, 2, 1), (2, 1, 3, 2, 1)],
"{found:#?}"
);
}
#[test]
fn a_line_with_a_two_row_asset_gets_a_row_below_it() {
let two = meta(2, 7);
let red = Style::parse("red on blue").unwrap();
let segments = vec![
Segment::new("go ", None),
tagged("[]", &two),
Segment::new(" now", Some(red)),
Segment::line(),
Segment::new("next", None),
];
let out = reserve_rows(segments);
assert_eq!(plain(&out), "go [] now\n \u{2800}\u{2800} \nnext");
let under = out.iter().rev().nth(2).unwrap();
assert_eq!(under.text, " ");
assert_eq!(
under.style.as_ref().and_then(|s| s.bgcolor()).cloned(),
Style::parse("on blue").unwrap().bgcolor().cloned()
);
assert!(under.style.as_ref().unwrap().color().is_none());
let second: Vec<MicroMeta> = out.iter().filter_map(MicroMeta::from_segment).collect();
assert_eq!(second.iter().map(|m| m.row).collect::<Vec<_>>(), [0, 1]);
assert!(second.iter().all(|m| m.id == 7));
assert_eq!(reserve_rows(out.clone()), out);
}
#[test]
fn one_row_output_is_untouched() {
let one = meta(1, 3);
let segments = vec![Segment::new("a\n", None), tagged("xx", &one)];
assert_eq!(reserve_rows(segments.clone()), segments);
}
#[test]
fn a_cut_asset_reserves_nothing() {
let two = meta(2, 4);
let segments = vec![Segment::new("ab ", None), tagged("x", &two)];
assert_eq!(plain(&reserve_rows(segments)), "ab x");
}
}