use crate::output::width;
pub(super) fn fit_rows(frame: &str, columns: u16, rows: u16) -> String {
let (columns, budget) = (usize::from(columns), usize::from(rows).saturating_sub(1));
if columns == 0 || budget == 0 {
return frame.to_owned();
}
let lines: Vec<&str> = frame.lines().collect();
let reserve = line_rows(¬ice(lines.len()), columns);
let fill = budget.saturating_sub(reserve);
let mut used = 0;
let mut kept = 0;
for line in &lines {
let height = line_rows(line, columns);
if used + height > fill {
break;
}
used += height;
kept += 1;
}
if kept == lines.len() {
return frame.to_owned();
}
let mut fitted = lines[..kept].join("\n");
if kept > 0 {
fitted.push('\n');
}
fitted.push_str(&clip_rows(
¬ice(lines.len() - kept),
columns,
budget.saturating_sub(used),
));
fitted.push('\n');
fitted
}
fn clip_rows(text: &str, columns: usize, rows: usize) -> String {
let ceiling = rows.saturating_mul(columns);
if width::visible_width(text) <= ceiling {
return text.to_owned();
}
text.chars().take(ceiling).collect()
}
pub(super) fn notice(dropped: usize) -> String {
let lines = if dropped == 1 { "line" } else { "lines" };
format!("{dropped} more {lines} than this terminal shows")
}
fn line_rows(line: &str, columns: usize) -> usize {
width::visible_width(line).div_ceil(columns).max(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trimmed_frame_never_overruns_the_rows_it_was_given() {
let frame: String = (0..40)
.map(|n| format!("sheep-{n:02} online 1234 0.5% 12.3 MB 0d 0h 1m\n"))
.collect();
for columns in [12u16, 16, 20, 24, 30, 38, 40, 60, 80, 120] {
for rows in [3u16, 4, 5, 8, 12, 24, 40] {
let out = fit_rows(&frame, columns, rows);
let used: usize = out
.lines()
.map(|line| line_rows(line, usize::from(columns)))
.sum();
let budget = usize::from(rows).saturating_sub(1);
assert!(
used <= budget,
"{columns}x{rows}: used {used} rows against a budget of {budget}"
);
}
}
}
#[test]
fn the_notice_counts_one_dropped_line_in_the_singular() {
let frame = "one\ntwo\nthree\n";
assert_eq!(
fit_rows(frame, 80, 4),
"one\ntwo\n1 more line than this terminal shows\n"
);
assert_eq!(
fit_rows(frame, 80, 3),
"one\n2 more lines than this terminal shows\n"
);
}
#[test]
fn a_frame_that_fits_is_left_alone() {
let frame = "one\ntwo\nthree\n";
assert_eq!(fit_rows(frame, 80, 24), frame);
}
#[test]
fn a_wrapped_line_costs_more_than_one_row() {
let wide = "0123456789012345678901234";
let frame = format!("{wide}\n{wide}\n{wide}\n{wide}\n");
let fitted = fit_rows(&frame, 10, 8);
assert_eq!(
fitted, "0123456789012345678901234\n3 more lines than this terminal shows\n",
"the notice reserves its own four rows at ten columns, leaving three of the seven \
for content, which is one wrapped line"
);
}
#[test]
fn a_frame_too_tall_says_how_much_it_dropped() {
let frame = (0..10)
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\n");
let fitted = fit_rows(&frame, 80, 6);
assert_eq!(
fitted,
"0\n1\n2\n3\n6 more lines than this terminal shows\n"
);
}
#[test]
fn a_terminal_reporting_no_size_gets_the_frame_whole() {
let frame = "a\nb\nc\n";
assert_eq!(fit_rows(frame, 0, 0), frame, "no size at all");
assert_eq!(fit_rows(frame, 80, 0), frame, "no rows");
assert_eq!(fit_rows(frame, 0, 24), frame, "no columns");
assert_eq!(
fit_rows(frame, 80, 1),
frame,
"one row leaves nothing to trim to once the cursor has its own"
);
}
}