use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use super::super::app::{App, RowKey};
use super::super::pane_sheep::SheepPane;
use super::detail;
mod charts;
mod column;
mod feed;
mod layout;
use self::charts::{
ChartTier, chart_tier, draw_charts, draw_cpu_only, draw_hairline, draw_sparkline_row,
};
use self::column::{column_top_row, draw_column, draw_divider};
use self::feed::draw_feed;
use self::layout::{FEED_WIDTH, FEED_X};
pub(crate) use self::column::column_len;
pub(crate) use self::layout::COLUMN_BODY_ROWS;
pub fn draw(app: &App, pane: &SheepPane, area: Rect, buffer: &mut Buffer) {
if area.height == 0 {
return;
}
let palette = app.palette();
let sheep_row = app.sheep_pane_row();
let mut spans = match sheep_row {
Some(row) => detail::identity_line(app, row, area.width, palette).spans,
None => {
let RowKey::Sheep(id) = pane.sheep() else {
unreachable!("a sheep pane is only ever opened on `RowKey::Sheep`")
};
vec![Span::styled(
format!("sheep {id}: it is no longer in the flock"),
palette.muted(),
)]
}
};
if let Some(pending) = pane
.config()
.map(|view| view.pending.len())
.filter(|count| *count > 0)
{
spans.push(Span::styled(
format!(" !{pending} pending"),
app.palette().attention(),
));
}
let line = Line::from(spans);
buffer.set_line(area.x, area.y, &line, area.width);
if let Some(row) = sheep_row {
let tier = chart_tier(area.width, area.height);
match tier {
ChartTier::Full => {
draw_charts(app, row.info.id, row.info.max_memory, area, buffer, palette);
}
ChartTier::CpuOnly => {
draw_cpu_only(app, row.info.id, row.info.max_memory, area, buffer, palette);
}
ChartTier::Sparkline => {
draw_sparkline_row(app, &row.info, area, buffer, palette);
}
ChartTier::None => {}
}
if matches!(tier, ChartTier::Full | ChartTier::CpuOnly) {
draw_hairline(area, buffer, palette);
}
}
let top = column_top_row(area.height);
if area.height > top {
draw_column(pane, top, area, buffer, palette);
}
if area.height > top && usize::from(area.width) >= usize::from(FEED_X + FEED_WIDTH) {
draw_divider(top, area, buffer, palette);
draw_feed(app, pane, top, area, buffer, palette);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use shep_core::protocol::ProcessInfo;
use shep_core::status::ProcStatus;
use super::super::super::app::{Body, Control, KeyPress, Msg};
use super::super::super::frames::render_text;
use super::super::fixtures;
use super::layout::{FULL_TIER_MIN_HEIGHT, MIN_HEIGHT_FOR_CHARTS, TERMINAL_OVERHEAD};
use super::*;
#[test]
fn the_band_does_not_draw_the_sheep_that_replaced_the_pinned_one() {
let mut app = fixtures::app_with(
vec![
ProcessInfo::builder(1, "alpha", ProcStatus::Online).build(),
ProcessInfo::builder(2, "bravo", ProcStatus::Online).build(),
],
fixtures::plain(),
);
let _ = app.update(Msg::Key(KeyPress::Confirm));
assert!(
matches!(app.body(), Body::Sheep(_)),
"setup: the pane opened on alpha"
);
let _ = app.update(Msg::Snapshot {
rows: vec![ProcessInfo::builder(2, "bravo", ProcStatus::Online).build()],
at: std::time::Instant::now(),
});
assert_eq!(
app.selected(),
Some(RowKey::Sheep(2)),
"setup: the reseat moved the selection to bravo"
);
let Body::Sheep(pane) = app.body() else {
panic!("the pane is still open");
};
let area = Rect::new(0, 0, 80, 3);
let mut buffer = Buffer::empty(area);
draw(&app, pane, area, &mut buffer);
let text = render_text(&buffer);
assert!(
!text.contains("bravo"),
"must not draw the sheep that replaced the pinned one: {text:?}"
);
assert!(
text.contains("sheep 1: it is no longer in the flock"),
"got {text:?}"
);
}
fn render_at(width: u16, height: u16) -> String {
let t0 = std::time::Instant::now();
let mut app = App::new(
fixtures::plain(),
Control::ReadOnly,
"/home/ada/.shep".to_string(),
t0,
);
app.update(Msg::Snapshot {
rows: vec![
ProcessInfo::builder(7, "web", ProcStatus::Online)
.cpu_ms(Some(0))
.memory_bytes(Some(10 << 20))
.max_memory(Some(64 << 20))
.build(),
],
at: t0,
});
let _ = app.update(Msg::Key(KeyPress::Confirm));
for i in 1..6u64 {
let at = t0 + Duration::from_secs(2 * i);
app.update(Msg::Snapshot {
rows: vec![
ProcessInfo::builder(7, "web", ProcStatus::Online)
.cpu_ms(Some(i * 400))
.memory_bytes(Some((10 + i * 4) << 20))
.max_memory(Some(64 << 20))
.build(),
],
at,
});
}
let Body::Sheep(pane) = app.body() else {
panic!("setup: the pane opened on web")
};
let area = Rect::new(0, 0, width, height);
let mut buffer = Buffer::empty(area);
draw(&app, pane, area, &mut buffer);
render_text(&buffer)
}
#[test]
fn below_a_hundred_and_forty_columns_only_the_cpu_chart_draws() {
let rendered = render_at(139, 48);
assert!(
rendered.contains("\u{2588}\u{2588} CPU"),
"got {rendered:?}"
);
assert!(rendered.contains("rss "), "got {rendered:?}");
assert!(
!rendered.contains("\u{2588}\u{2588} MEM"),
"got {rendered:?}"
);
}
#[test]
fn below_a_hundred_columns_both_charts_become_the_sparkline_pair() {
let rendered = render_at(99, 48);
assert!(
!rendered.contains("\u{2588}\u{2588} CPU"),
"got {rendered:?}"
);
assert!(
!rendered.contains("\u{2588}\u{2588} MEM"),
"got {rendered:?}"
);
assert!(rendered.contains("CPU 20s"), "got {rendered:?}");
}
#[test]
fn the_hairline_and_column_headers_sit_at_the_spec_s_own_rows() {
let rendered = render_at(160, 48);
let lines: Vec<&str> = rendered.lines().collect();
assert!(
lines[16].contains("now"),
"row 16 (spec 17): {:?}",
lines.get(16)
);
let hairline = lines[17].trim_end();
assert!(
!hairline.is_empty() && hairline.chars().all(|c| c == '\u{2500}'),
"row 17 (spec 18) is not a hairline rule: {:?}",
lines.get(17)
);
assert!(
lines[18].contains("\u{2588}\u{2588} CONFIG & ENV"),
"row 18 (spec 19): {:?}",
lines.get(18)
);
}
#[test]
fn a_short_terminal_drops_the_charts_before_the_columns() {
assert!(!render_at(160, 20).contains("\u{2588}\u{2588} MEM"));
assert!(render_at(160, 20).contains("\u{2588}\u{2588} CONFIG & ENV"));
assert!(!render_at(160, 18).contains("\u{2588}\u{2588} CPU"));
assert!(render_at(160, 18).contains("\u{2588}\u{2588} CONFIG & ENV"));
}
#[test]
fn at_a_hundred_and_forty_columns_both_charts_still_draw() {
let rendered = render_at(140, 48);
assert!(
rendered.contains("\u{2588}\u{2588} CPU"),
"got {rendered:?}"
);
assert!(
rendered.contains("\u{2588}\u{2588} MEM"),
"got {rendered:?}"
);
}
#[test]
fn at_a_hundred_columns_the_cpu_chart_still_draws() {
let rendered = render_at(100, 48);
assert!(
rendered.contains("\u{2588}\u{2588} CPU"),
"got {rendered:?}"
);
assert!(!rendered.contains("CPU 20s"), "got {rendered:?}");
}
#[test]
fn at_the_chart_height_floor_a_chart_still_draws() {
let rendered = render_at(160, MIN_HEIGHT_FOR_CHARTS - TERMINAL_OVERHEAD);
assert!(
rendered.contains("\u{2588}\u{2588} CPU"),
"got {rendered:?}"
);
}
#[test]
fn at_the_full_tier_height_floor_the_memory_chart_still_draws() {
let rendered = render_at(160, FULL_TIER_MIN_HEIGHT - TERMINAL_OVERHEAD);
assert!(
rendered.contains("\u{2588}\u{2588} MEM"),
"got {rendered:?}"
);
}
}