use ratatui::text::{Line, Span};
use super::super::app::App;
use super::super::tail::Stream;
use super::flock::fit;
use crate::output::human_bytes;
#[must_use]
pub fn feed_lines(app: &App, width: u16, rows: usize) -> Vec<Line<'static>> {
let palette = app.palette();
let mut out = Vec::with_capacity(rows);
let feed = app.feed();
let body = rows.saturating_sub(1);
let lost_lines = feed.missed_lines + feed.lines.len().saturating_sub(body);
out.push(match app.selected_row() {
None => Line::from(Span::styled(
fit("bleats no sheep is selected", width),
palette.muted(),
)),
Some(row) => match gap_notice(lost_lines, feed.missed_bytes) {
Some(notice) => Line::from(Span::styled(
fit(&format!("bleats {} {notice}", row.info.name), width),
palette.attention(),
)),
None => Line::from(Span::styled(
fit(
&format!(
"bleats {} out then err from the log files, re-read with each listing",
row.info.name
),
width,
),
palette.muted(),
)),
},
});
if feed.lines.is_empty() {
if let Some(note) = feed.note.as_deref() {
out.push(Line::from(Span::styled(fit(note, width), palette.muted())));
}
return out;
}
let skip = feed.lines.len().saturating_sub(body);
for line in feed.lines.iter().skip(skip) {
let tag = match line.stream {
Stream::Out => "out",
Stream::Err => "err",
};
out.push(Line::from(vec![
Span::styled(format!("{tag} "), palette.muted()),
Span::raw(fit(&line.text, width.saturating_sub(5))),
]));
}
out
}
fn gap_notice(lines: usize, bytes: u64) -> Option<String> {
match (lines, bytes) {
(0, 0) => None,
(0, bytes) => Some(format!(
"… {} written before these lines was never read",
human_bytes(bytes)
)),
(lines, 0) => Some(format!("… {}", earlier_lines(lines))),
(lines, bytes) => Some(format!(
"… {}, and {} before them never read",
earlier_lines(lines),
human_bytes(bytes)
)),
}
}
fn earlier_lines(count: usize) -> String {
if count == 1 {
"1 earlier line not shown".to_string()
} else {
format!("{count} earlier lines not shown")
}
}
#[cfg(test)]
mod tests {
use super::super::super::tail::{Stream, Tail};
use super::super::fixtures::{
coloured, line, render_all, with_feed, with_feed_and_palette, with_feed_and_selection,
with_no_selection,
};
use super::feed_lines;
#[test]
fn a_byte_gap_replaces_the_header_and_says_how_much_was_never_read() {
let app = with_feed(Tail {
lines: vec![line(Stream::Out, "still here")],
missed_lines: 0,
missed_bytes: 4_000_000,
read_bytes: 65_536,
note: None,
});
let rendered = render_all(&feed_lines(&app, 120, 6));
assert!(
rendered.contains("3.8M written before these lines was never read"),
"got {rendered:?}"
);
assert!(!rendered.contains("re-read with each listing"));
}
#[test]
fn a_pane_that_cannot_show_every_line_it_holds_says_how_many() {
let app = with_feed(Tail {
lines: (0..30)
.map(|n| line(Stream::Out, &format!("line-{n}")))
.collect(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 4_096,
note: None,
});
let rendered = render_all(&feed_lines(&app, 120, 6));
assert!(
rendered.contains("… 25 earlier lines not shown"),
"got {rendered:?}"
);
assert!(
!rendered.contains("never read"),
"no bytes were skipped, so nothing may claim any were: {rendered:?}"
);
}
#[test]
fn both_kinds_of_gap_are_named_separately_in_one_line() {
let app = with_feed(Tail {
lines: (0..30)
.map(|n| line(Stream::Out, &format!("line-{n}")))
.collect(),
missed_lines: 500,
missed_bytes: 4_000_000,
read_bytes: 131_072,
note: None,
});
let rendered = render_all(&feed_lines(&app, 200, 6));
assert!(
rendered.contains("… 525 earlier lines not shown, and 3.8M before them never read"),
"500 the reader dropped plus 25 the pane has no room for: {rendered:?}"
);
}
#[test]
fn the_header_says_which_sheep_and_that_it_is_a_re_read() {
let app = with_feed_and_selection(
Tail {
lines: vec![line(Stream::Out, "hello")],
missed_lines: 0,
missed_bytes: 0,
read_bytes: 6,
note: None,
},
1,
);
let rendered = render_all(&feed_lines(&app, 120, 6));
assert!(rendered.contains("bleats sheep-1"), "got {rendered:?}");
assert!(rendered.contains("out then err"), "got {rendered:?}");
assert!(
!rendered.contains("out+err"),
"`+` reads as a merge: {rendered:?}"
);
assert!(
rendered.contains("re-read with each listing"),
"got {rendered:?}"
);
}
#[test]
fn the_pane_shows_the_last_lines_that_fit_and_says_so() {
let app = with_feed(Tail {
lines: (0..40)
.map(|n| line(Stream::Out, &format!("line-{n}")))
.collect(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 4_096,
note: None,
});
let rendered = render_all(&feed_lines(&app, 120, 6));
for n in 35..40 {
assert!(
rendered.contains(&format!("out line-{n}")),
"line-{n} is on screen"
);
}
assert!(
!rendered.contains("out line-34"),
"and line-34 is not: {rendered:?}"
);
assert!(
rendered.contains("… 35 earlier lines not shown"),
"and the pane says the other thirty-five went: {rendered:?}"
);
}
#[test]
fn the_stream_tag_is_a_word_and_stderr_is_not_bark() {
let palette = coloured();
let app = with_feed_and_palette(
Tail {
lines: vec![line(Stream::Out, "fine"), line(Stream::Err, "warning")],
missed_lines: 0,
missed_bytes: 0,
read_bytes: 32,
note: None,
},
palette,
);
let lines = feed_lines(&app, 120, 6);
let rendered = render_all(&lines);
assert!(rendered.contains("out fine"));
assert!(rendered.contains("err warning"));
let bark = palette.alarm().fg;
for line in &lines {
for span in &line.spans {
assert_ne!(
span.style.fg, bark,
"nothing in this pane is bark: {span:?}"
);
}
}
}
#[test]
fn an_empty_feed_prints_the_reason_rather_than_nothing() {
let app = with_feed(Tail {
lines: Vec::new(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 0,
note: Some("this sheep has written nothing yet".to_string()),
});
let rendered = render_all(&feed_lines(&app, 120, 6));
assert!(rendered.contains("this sheep has written nothing yet"));
let empty = with_no_selection();
let rendered = render_all(&feed_lines(&empty, 120, 6));
assert!(
rendered.contains("no sheep is selected"),
"got {rendered:?}"
);
}
}