use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::Frame;
use crate::config::Background;
use crate::view::fitted::{columns, indent, Fitted};
use crate::view::palette;
use crate::view::phrase;
use crate::view::sgr;
use crate::view::tail::Tail;
use super::sentence;
const RULE: char = '─';
pub struct Band<'a> {
pub tail: &'a Tail,
pub background: Background,
}
pub fn draw_tail(frame: &mut Frame, area: Rect, band: Band<'_>) {
let Band { tail, background } = band;
if area.width == 0 || area.height == 0 {
return;
}
let row = |n: usize| Rect {
y: area.y + n as u16,
height: 1,
..area
};
let room = area.height as usize - 1;
match tail {
Tail::Pane { pane, lines } => {
frame.render_widget(rule(Some(&pane.id), area.width as usize), row(0));
let styled = sgr::lines(lines);
for (n, said) in styled
.into_iter()
.skip(lines.len().saturating_sub(room))
.enumerate()
{
frame.render_widget(as_the_pane_drew_it(said), row(n + 1));
}
}
Tail::Reading { pane } => {
frame.render_widget(rule(Some(&pane.id), area.width as usize), row(0));
if room > 0 {
frame.render_widget(
sentence(
&indent(),
phrase::pane_being_read().to_string(),
palette::voice(background),
),
row(1),
);
}
}
Tail::Silent(why) => {
frame.render_widget(rule(None, area.width as usize), row(0));
if room > 0 {
frame.render_widget(
sentence(&indent(), (*why).to_string(), palette::voice(background)),
row(1),
);
}
}
}
}
fn as_the_pane_drew_it(said: Line<'static>) -> Fitted {
let mut spans = vec![Span::raw(indent())];
spans.extend(said.spans);
Fitted::new(spans, Vec::new(), Vec::new())
}
fn rule(pane: Option<&str>, width: usize) -> Line<'static> {
let drawn = |n: usize| Span::styled(RULE.to_string().repeat(n), palette::QUIET);
let named = match pane {
Some(pane) => format!(" {pane} "),
None => return Line::from(drawn(width)),
};
let taken = columns(&[Span::raw(named.clone())]);
if taken >= width {
return Line::from(drawn(width));
}
let left = (width - taken) / 2;
Line::from(vec![
drawn(left),
Span::raw(named),
drawn(width - taken - left),
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::types::testing::key;
use pretty_assertions::assert_eq;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use crate::view::painted::{Painted, Run};
use crate::view::phrase;
fn tail_frame(tail: &Tail, width: u16, height: u16, at: u16) -> Painted {
tail_frame_on(Background::Dark, tail, width, height, at)
}
fn tail_frame_on(
background: Background,
tail: &Tail,
width: u16,
height: u16,
at: u16,
) -> Painted {
Painted::drawn_by(width, height, |frame| {
draw_tail(
frame,
Rect::new(0, at, width, height - at),
Band { tail, background },
);
})
}
fn tailing(pane: &str, lines: &[&str]) -> Tail {
Tail::Pane {
pane: key(pane),
lines: lines.iter().map(|line| (*line).to_string()).collect(),
}
}
#[test]
fn the_tail_fills_the_band_it_is_given_and_no_row_above_it() {
let tail = tailing("wCM:p9", &["rebuilt .#larkspur, generation 541"]);
assert_eq!(
tail_frame(&tail, 44, 5, 2).rows(),
vec![
" ",
" ",
"────────────────── wCM:p9 ──────────────────",
" rebuilt .#larkspur, generation 541 ",
" ",
]
);
}
#[test]
fn a_pane_with_more_to_say_than_there_is_room_shows_the_newest_of_it() {
let tail = tailing("w:p1", &["one", "two", "three", "four"]);
assert_eq!(
tail_frame(&tail, 20, 3, 0).rows(),
vec![
"─────── w:p1 ───────",
" three ",
" four "
]
);
}
#[test]
fn a_tail_with_no_pane_says_why_rather_than_leaving_the_band_blank() {
assert_eq!(
tail_frame(&Tail::Silent(phrase::no_agent_to_tail()), 40, 3, 0).rows(),
vec![
"────────────────────────────────────────",
" no pane · nobody is working this bead ",
" ",
]
);
}
#[test]
fn a_pane_not_yet_read_says_it_is_being_read() {
assert_eq!(
tail_frame(
&Tail::Reading {
pane: key("wCM:p9")
},
40,
3,
0
)
.rows(),
vec![
"──────────────── wCM:p9 ────────────────",
" reading that pane ",
" ",
]
);
}
const BAND: u16 = 60;
fn as_a_reader_with_no_colour_sees_it(style: Style) -> Style {
Style::new()
.add_modifier(style.add_modifier)
.remove_modifier(style.sub_modifier)
}
fn drawn_style(row: &[Run], words: &str) -> Style {
row.iter()
.find(|run| run.said.contains(words))
.unwrap_or_else(|| panic!("no run saying {words:?} in {row:?}"))
.style
}
fn a_plain_pane_line(background: Background, said: &str) -> Style {
drawn_style(
&tail_frame_on(background, &tailing("w:p1", &[said]), BAND, 2, 0).row(1),
said,
)
}
fn every_row_bdi_says_itself() -> [(Tail, &'static str); 2] {
[
(
Tail::Reading { pane: key("w:p1") },
phrase::pane_being_read(),
),
(
Tail::Silent(phrase::no_bead_to_tail()),
phrase::no_bead_to_tail(),
),
]
}
#[test]
fn what_bdi_says_on_a_dark_background_is_drawn_dimmer_than_the_pane() {
let waiting = tail_frame(&Tail::Reading { pane: key("w:p1") }, 40, 2, 0).row(1);
assert!(
waiting.iter().any(|run| {
run.said.contains(phrase::pane_being_read())
&& run.style.fg == Some(Color::Reset)
&& run.style.add_modifier.contains(Modifier::DIM)
}),
"the row saying the pane is being read: {waiting:?}"
);
let said = tail_frame(&tailing("w:p1", &["rebuilt .#larkspur"]), 40, 2, 0).row(1);
assert!(
said.iter().any(|run| {
run.said.contains("rebuilt .#larkspur") && run.style.fg == Some(Color::Reset)
}),
"the pane's own line: {said:?}"
);
}
#[test]
fn what_tells_bdi_from_the_pane_on_a_dark_background_is_not_colour() {
let pane = a_plain_pane_line(Background::Dark, "rebuilt .#larkspur");
for (voice, said) in every_row_bdi_says_itself() {
let spoken = drawn_style(&tail_frame(&voice, BAND, 2, 0).row(1), said);
assert_ne!(
as_a_reader_with_no_colour_sees_it(spoken),
as_a_reader_with_no_colour_sees_it(pane),
"with no colour, {said:?} is drawn as the pane's own line is"
);
}
}
#[test]
fn what_bdi_says_is_told_from_the_pane_on_either_background() {
for background in [Background::Dark, Background::Light] {
let pane = a_plain_pane_line(background, "rebuilt .#larkspur");
for (voice, said) in every_row_bdi_says_itself() {
let spoken =
drawn_style(&tail_frame_on(background, &voice, BAND, 2, 0).row(1), said);
assert_ne!(spoken, pane, "on a {background:?} background, {said:?}");
}
}
}
#[test]
fn the_declared_background_reaches_the_band() {
for (voice, said) in every_row_bdi_says_itself() {
let undeclared = drawn_style(&tail_frame(&voice, BAND, 2, 0).row(1), said);
let light = drawn_style(
&tail_frame_on(Background::Light, &voice, BAND, 2, 0).row(1),
said,
);
assert_ne!(light, undeclared, "{said:?} is drawn on neither background");
}
}
fn a_captured_pane() -> Tail {
use crate::collect::agents::Agents;
use crate::collect::herdr::Herdr;
use crate::collect::run::testing::FakeRunner;
use crate::view::tail;
const ARGV: &str =
"herdr --session default agent read wQ:p1 --source visible --lines 6 --format ansi";
let runner = FakeRunner::default().with(
ARGV,
include_str!("../../../tests/fixtures/herdr_agent_read_ansi.txt"),
);
tail::read(key("wQ:p1"), Herdr::new(&runner).read(&key("wQ:p1"), 6))
}
#[test]
fn the_tail_draws_the_panes_own_colour_and_attributes() {
let painted = tail_frame(&a_captured_pane(), 120, 7, 0);
let host = painted.row(4);
assert!(
host.iter().any(|run| {
run.said == "larkspur"
&& run.style.fg == Some(Color::Rgb(255, 121, 198))
&& run.style.add_modifier.contains(Modifier::BOLD)
}),
"the host, bold and pink as the pane drew it: {host:?}"
);
assert!(
host.iter()
.any(|run| run.said == "main" && run.style.fg == Some(Color::Indexed(6))),
"the branch, in the pane's 256-colour cyan: {host:?}"
);
assert!(
painted.rows().iter().all(|row| !row.contains('\x1b')),
"no escape reaches the screen as text: {:?}",
painted.rows()
);
}
#[test]
fn a_pane_line_too_wide_for_the_screen_is_cut_rather_than_wrapped() {
let tail = tailing("w:p1", &["a line with a great deal more to say than this"]);
assert_eq!(
tail_frame(&tail, 20, 2, 0).rows(),
vec!["─────── w:p1 ───────", " a line with a gre…"]
);
}
#[test]
fn a_tail_with_barely_any_room_draws_what_it_can() {
let tail = tailing("a-very-long-pane-identifier", &["never seen"]);
assert_eq!(tail_frame(&tail, 10, 1, 0).rows(), vec!["──────────"]);
assert_eq!(
tail_frame(&Tail::Silent(phrase::no_bead_to_tail()), 10, 1, 0).rows(),
vec!["──────────"]
);
}
#[test]
fn a_band_one_row_high_writes_nothing_under_its_rule() {
for tail in [
Tail::Reading { pane: key("w:p1") },
Tail::Silent(phrase::no_bead_to_tail()),
] {
let screen = Painted::drawn_by(20, 2, |frame| {
draw_tail(
frame,
Rect::new(0, 0, 20, 1),
Band {
tail: &tail,
background: Background::Dark,
},
);
});
assert_eq!(
screen.rows()[1],
" ".repeat(20),
"the row under the band, for {tail:?}"
);
}
}
#[test]
fn a_band_with_no_rows_draws_nothing() {
let screen = Painted::drawn_by(20, 1, |frame| {
draw_tail(
frame,
Rect::new(0, 0, 20, 0),
Band {
tail: &tailing("w:p1", &["x"]),
background: Background::Dark,
},
);
});
assert_eq!(screen.rows()[0], " ".repeat(20));
}
}