use ratatui::buffer::Cell;
use ratatui::style::Color;
pub(crate) const DECSACE_RECT: &str = "\x1b[2*x";
pub(crate) const DECSACE_DEFAULT: &str = "\x1b[*x";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BgFill {
pub content_end: u16,
pub bg: Color,
}
fn bg_sgr(color: Color) -> Option<String> {
match color {
Color::Reset => None,
Color::Black => Some("40".into()),
Color::Red => Some("41".into()),
Color::Green => Some("42".into()),
Color::Yellow => Some("43".into()),
Color::Blue => Some("44".into()),
Color::Magenta => Some("45".into()),
Color::Cyan => Some("46".into()),
Color::Gray => Some("47".into()),
Color::DarkGray => Some("100".into()),
Color::LightRed => Some("101".into()),
Color::LightGreen => Some("102".into()),
Color::LightYellow => Some("103".into()),
Color::LightBlue => Some("104".into()),
Color::LightMagenta => Some("105".into()),
Color::LightCyan => Some("106".into()),
Color::White => Some("107".into()),
Color::Indexed(i) => Some(format!("48;5;{i}")),
Color::Rgb(r, g, b) => Some(format!("48;2;{r};{g};{b}")),
}
}
pub(crate) fn encode_rect(top: u16, left: u16, bottom: u16, right: u16, sgr: &str) -> String {
format!("\x1b[{top};{left};{bottom};{right};{sgr}$r")
}
pub(crate) fn analyze_row(cells: &[(u16, &Cell)], width: u16) -> Option<BgFill> {
if width == 0 || cells.is_empty() {
return None;
}
let mut by_col: Vec<Option<&Cell>> = vec![None; width as usize];
for &(x, cell) in cells {
if x >= width {
return None; }
if by_col[x as usize].is_some() {
return None; }
by_col[x as usize] = Some(cell);
}
if by_col.iter().any(|c| c.is_none()) {
return None;
}
let mut content_end = 0u16;
for col in 0..width {
let cell = by_col[col as usize].expect("checked present");
if cell.symbol() != " " {
content_end = col + 1;
}
}
analyze_trailing(&by_col, content_end, width)
}
fn analyze_trailing(by_col: &[Option<&Cell>], content_end: u16, width: u16) -> Option<BgFill> {
if content_end >= width {
return None; }
let first = by_col[content_end as usize].expect("full-width");
let bg = first.bg;
bg_sgr(bg)?; for c in &by_col[content_end as usize + 1..width as usize] {
let cell = c.expect("full-width");
if cell.symbol() != " " || cell.bg != bg {
return None; }
}
Some(BgFill { content_end, bg })
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct FillCandidate {
top: u16,
bottom: u16,
content_end: u16,
bg: Color,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct DeccaraPlan {
pub cutoffs: Vec<Option<u16>>,
pub sequence: String,
}
pub(crate) fn plan_fills(fills: &[Option<BgFill>], width: u16, first_row: u16) -> DeccaraPlan {
let n = fills.len();
let mut cutoffs: Vec<Option<u16>> = vec![None; n];
let mut candidates: Vec<FillCandidate> = Vec::new();
let mut k = 0;
while k < n {
let Some(head) = &fills[k] else {
cutoffs[k] = None;
k += 1;
continue;
};
let mut end = k;
while end + 1 < n {
match &fills[end + 1] {
Some(next) if next.content_end == head.content_end && next.bg == head.bg => {
end += 1
}
_ => break,
}
}
candidates.push(FillCandidate {
top: first_row + k as u16,
bottom: first_row + end as u16 + 1,
content_end: head.content_end,
bg: head.bg,
});
cutoffs[k..=end].fill(Some(head.content_end));
k = end + 1;
}
if candidates.is_empty() {
return DeccaraPlan {
cutoffs,
sequence: String::new(),
};
}
const CLEAR_COST: usize = "\x1b[K".len();
let wrapper_bytes = DECSACE_RECT.len() + DECSACE_DEFAULT.len();
let mut removed_total: usize = 0;
let mut added_total: usize = 0;
let mut kept: Vec<bool> = vec![false; candidates.len()];
for (i, c) in candidates.iter().enumerate() {
let rows = (c.bottom - c.top) as usize;
let dropped_cells = rows * (width as usize - c.content_end as usize);
let Some(sgr) = bg_sgr(c.bg) else {
continue; };
let rect = encode_rect(c.top + 1, c.content_end + 1, c.bottom, width, &sgr);
let added = rect.len() + CLEAR_COST * rows;
if dropped_cells > added {
kept[i] = true;
removed_total += dropped_cells;
added_total += added;
}
}
if removed_total > wrapper_bytes + added_total {
let mut sequence = String::from(DECSACE_RECT);
for (i, c) in candidates.iter().enumerate() {
if kept[i] {
let sgr = bg_sgr(c.bg).unwrap_or_default();
sequence.push_str(&encode_rect(
c.top + 1,
c.content_end + 1,
c.bottom,
width,
&sgr,
));
}
}
sequence.push_str(DECSACE_DEFAULT);
for (i, c) in candidates.iter().enumerate() {
if !kept[i] {
cutoffs[(c.top - first_row) as usize..=(c.bottom - first_row - 1) as usize]
.fill(None);
}
}
DeccaraPlan { cutoffs, sequence }
} else {
for c in &candidates {
cutoffs[(c.top - first_row) as usize..=(c.bottom - first_row - 1) as usize].fill(None);
}
DeccaraPlan {
cutoffs,
sequence: String::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
fn cell(sym: &str, bg: Color) -> Cell {
let mut c = Cell::new(" ");
c.set_symbol(sym);
c.bg = bg;
c
}
fn row(spec: &[(char, Color)], width: u16) -> Vec<(u16, Cell)> {
let pad_bg = spec.last().map(|(_, bg)| *bg).unwrap_or(Color::Reset);
let mut v: Vec<(u16, Cell)> = spec
.iter()
.enumerate()
.map(|(i, (sym, bg))| (i as u16, cell(&sym.to_string(), *bg)))
.collect();
while (v.len() as u16) < width {
let i = v.len() as u16;
v.push((i, cell(" ", pad_bg)));
}
v
}
fn refs(cells: &[(u16, Cell)]) -> Vec<(u16, &Cell)> {
cells.iter().map(|(x, c)| (*x, c)).collect()
}
#[test]
fn bg_sgr_rgb_and_default() {
assert_eq!(bg_sgr(Color::Reset), None);
assert_eq!(bg_sgr(Color::Rgb(10, 20, 30)), Some("48;2;10;20;30".into()));
assert_eq!(bg_sgr(Color::Indexed(4)), Some("48;5;4".into()));
assert_eq!(bg_sgr(Color::Blue), Some("44".into()));
}
#[test]
fn full_bg_row_is_fillable() {
let spec: Vec<(char, Color)> = vec![(' ', Color::Rgb(1, 2, 3)); 10];
let cells = row(&spec, 10);
let fill = analyze_row(&refs(&cells), 10).expect("fillable");
assert_eq!(fill.content_end, 0);
assert_eq!(fill.bg, Color::Rgb(1, 2, 3));
}
#[test]
fn content_then_uniform_bg_trailing_is_fillable() {
let mut spec = vec![('H', Color::Rgb(1, 2, 3)), ('i', Color::Rgb(1, 2, 3))];
for _ in 0..8 {
spec.push((' ', Color::Rgb(1, 2, 3)));
}
let cells = row(&spec, 10);
let fill = analyze_row(&refs(&cells), 10).expect("fillable");
assert_eq!(fill.content_end, 2);
}
#[test]
fn default_bg_trailing_is_not_fillable() {
let spec = vec![('H', Color::Reset), ('i', Color::Reset)];
let cells = row(&spec, 10);
assert!(analyze_row(&refs(&cells), 10).is_none());
}
#[test]
fn mixed_bg_trailing_is_not_fillable() {
let mut spec = vec![('H', Color::Rgb(1, 2, 3))];
spec.push((' ', Color::Rgb(1, 2, 3)));
spec.push((' ', Color::Rgb(9, 9, 9))); let cells = row(&spec, 10);
assert!(analyze_row(&refs(&cells), 10).is_none());
}
#[test]
fn non_full_width_row_is_not_fillable() {
let cells: Vec<(u16, Cell)> = (0..3u16)
.map(|i| (i, cell(" ", Color::Rgb(1, 2, 3))))
.collect();
assert!(analyze_row(&refs(&cells), 10).is_none());
}
#[test]
fn encode_rect_is_well_formed() {
let s = encode_rect(1, 4, 3, 10, "48;2;1;2;3");
assert_eq!(s, "\x1b[1;4;3;10;48;2;1;2;3$r");
}
#[test]
fn plan_coalesces_adjacent_identical_rows() {
let fill = BgFill {
content_end: 0,
bg: Color::Rgb(1, 2, 3),
};
let fills = vec![Some(fill.clone()), Some(fill)];
let plan = plan_fills(&fills, 40, 5);
assert!(!plan.sequence.is_empty());
assert!(plan.sequence.contains(DECSACE_RECT));
assert!(plan.sequence.contains(DECSACE_DEFAULT));
assert!(plan.sequence.contains("6;1;7;40;"));
assert_eq!(plan.cutoffs, vec![Some(0), Some(0)]);
}
#[test]
fn plan_rejects_when_not_worth_it() {
let mut spec = vec![('H', Color::Rgb(1, 2, 3))];
spec.push((' ', Color::Rgb(1, 2, 3)));
let cells = row(&spec, 2);
let fill = analyze_row(&refs(&cells), 2).expect("fillable");
let plan = plan_fills(&[Some(fill)], 2, 0);
assert!(plan.sequence.is_empty());
assert_eq!(plan.cutoffs, vec![None]);
}
#[test]
fn plan_keeps_rows_without_fills_full() {
let fills = vec![None, None];
let plan = plan_fills(&fills, 40, 0);
assert!(plan.sequence.is_empty());
assert_eq!(plan.cutoffs, vec![None, None]);
}
}