use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::super::super::app::{App, GroupTotals, Row, RowKey};
use super::super::super::theme::Palette;
use super::super::cell;
use super::columns::{FoldColumn, fold_name_width};
use super::facts::FrameFacts;
use super::layout::{fit_owned, pad_ground};
use super::row::section_line;
use crate::output::{human_bytes, human_duration};
#[must_use]
pub fn fold_key_line(
app: &App,
facts: &FrameFacts<'_>,
key: &RowKey,
columns: &[FoldColumn],
width: u16,
selected: bool,
) -> Line<'static> {
match key {
RowKey::Fold(name) => fold_header_line(app, facts, name, columns, width, selected),
RowKey::Group(name) => fold_group_line(app, name, columns, width, selected),
RowKey::Sheep(id) => app.row(*id).map_or_else(
|| Line::from(Span::raw(" ".repeat(usize::from(width)))),
|row| fold_member_line(app, row, columns, width, selected),
),
RowKey::Section(label) => section_line(label, width, app.data_palette().muted()),
}
}
fn fold_header_line(
app: &App,
facts: &FrameFacts<'_>,
name: &str,
columns: &[FoldColumn],
width: u16,
selected: bool,
) -> Line<'static> {
let palette = app.data_palette();
let totals = app.fold_totals(name);
let total_memory = facts.flock_memory;
let share_percent = fold_share_percent(totals.memory, total_memory);
let share_fill = cell::gauge_fill(totals.memory.unwrap_or(0), total_memory, 20);
let status = app.fold_uniform_status(name);
let status_style = status.map_or(Style::default(), |status| palette.status(status));
let name_width = fold_name_width(width, columns);
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let mut spans: Vec<Span<'static>> = Vec::with_capacity(columns.len() * 2 + 1);
let mut used: u16 = 0;
for (index, column) in columns.iter().enumerate() {
if index > 0 {
spans.push(Span::styled(" ", ground));
used += 2;
}
let cell_width = if *column == FoldColumn::Name {
name_width
} else {
column.width()
};
let text = fit_owned(
fold_header_cell(app, name, *column, &totals, total_memory, share_percent),
cell_width,
);
match column {
FoldColumn::Share => {
push_fold_share_cell(&mut spans, palette, text, share_fill, ground)
}
FoldColumn::Status => spans.push(Span::styled(text, status_style.patch(ground))),
_ => spans.push(Span::styled(text, Style::default().patch(ground))),
}
used += cell_width;
}
pad_ground(&mut spans, used, width, ground);
Line::from(spans)
}
fn fold_header_cell(
app: &App,
name: &str,
column: FoldColumn,
totals: &GroupTotals,
total_memory: Option<u64>,
share_percent: Option<u32>,
) -> String {
match column {
FoldColumn::Name => {
let marker = if app.is_fold_collapsed(name) {
'\u{25b8}'
} else {
'\u{25be}'
};
format!("{marker} {name} \u{d7}{}", totals.count)
}
FoldColumn::Status => app.fold_status_text(name),
FoldColumn::Share => cell::gauge(totals.memory.unwrap_or(0), total_memory, 20),
FoldColumn::Mem => totals.memory.map_or_else(|| "-".to_string(), human_bytes),
FoldColumn::Cpu => totals
.cpu
.map_or_else(|| "-".to_string(), |cpu| format!("{cpu:.1}%")),
FoldColumn::Uptime => totals
.uptime_ms
.map_or_else(|| "-".to_string(), human_duration),
FoldColumn::Restarts => totals.restarts.to_string(),
FoldColumn::Notes => {
share_percent.map_or_else(String::new, |percent| format!("{percent}% of flock memory"))
}
}
}
fn push_fold_share_cell(
spans: &mut Vec<Span<'static>>,
palette: Palette,
text: String,
fill: usize,
ground: Style,
) {
let fill = fill.min(text.chars().count());
let mut chars = text.chars();
let filled: String = chars.by_ref().take(fill).collect();
let rest: String = chars.collect();
spans.push(Span::styled(filled, palette.sky().patch(ground)));
spans.push(Span::styled(rest, palette.gauge_rest().patch(ground)));
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] fn fold_share_percent(memory: Option<u64>, total_memory: Option<u64>) -> Option<u32> {
let memory = memory?;
let total_memory = total_memory?;
if total_memory == 0 {
return None;
}
Some((memory as f64 / total_memory as f64 * 100.0).round() as u32)
}
fn fold_group_line(
app: &App,
name: &str,
columns: &[FoldColumn],
width: u16,
selected: bool,
) -> Line<'static> {
let palette = app.data_palette();
let members = app.group_members(name);
let totals = app.totals_for(&members);
let status = App::uniform_status_for(&members);
let status_style = status.map_or(Style::default(), |status| palette.status(status));
let status_text = App::status_text_for(&members);
let name_width = fold_name_width(width, columns);
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let base = palette.muted();
let mut spans: Vec<Span<'static>> = Vec::with_capacity(columns.len() * 2 + 1);
let mut used: u16 = 0;
for (index, column) in columns.iter().enumerate() {
if index > 0 {
spans.push(Span::styled(" ", ground));
used += 2;
}
let cell_width = if *column == FoldColumn::Name {
name_width
} else {
column.width()
};
let text = fit_owned(
fold_group_cell(name, *column, &totals, &status_text),
cell_width,
);
let style = if *column == FoldColumn::Status {
status_style
} else {
base
};
spans.push(Span::styled(text, style.patch(ground)));
used += cell_width;
}
pad_ground(&mut spans, used, width, ground);
Line::from(spans)
}
fn fold_group_cell(
name: &str,
column: FoldColumn,
totals: &GroupTotals,
status_text: &str,
) -> String {
match column {
FoldColumn::Name => format!("{name} \u{d7}{}", totals.count),
FoldColumn::Status => status_text.to_string(),
FoldColumn::Share | FoldColumn::Notes => String::new(),
FoldColumn::Mem => totals.memory.map_or_else(|| "-".to_string(), human_bytes),
FoldColumn::Cpu => totals
.cpu
.map_or_else(|| "-".to_string(), |cpu| format!("{cpu:.1}%")),
FoldColumn::Uptime => totals
.uptime_ms
.map_or_else(|| "-".to_string(), human_duration),
FoldColumn::Restarts => totals.restarts.to_string(),
}
}
fn fold_member_line(
app: &App,
row: &Row,
columns: &[FoldColumn],
width: u16,
selected: bool,
) -> Line<'static> {
let palette = app.data_palette();
let status_style = palette.reported(row.reported());
let name_width = fold_name_width(width, columns);
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let base = palette.muted();
let mut spans: Vec<Span<'static>> = Vec::with_capacity(columns.len() * 2 + 1);
let mut used: u16 = 0;
for (index, column) in columns.iter().enumerate() {
if index > 0 {
spans.push(Span::styled(" ", ground));
used += 2;
}
let cell_width = if *column == FoldColumn::Name {
name_width
} else {
column.width()
};
let text = fit_owned(fold_member_cell(app, row, *column), cell_width);
let style = if *column == FoldColumn::Status {
status_style
} else {
base
};
spans.push(Span::styled(text, style.patch(ground)));
used += cell_width;
}
pad_ground(&mut spans, used, width, ground);
Line::from(spans)
}
fn fold_member_cell(app: &App, row: &Row, column: FoldColumn) -> String {
let info = &row.info;
match column {
FoldColumn::Name => info.name.clone(),
FoldColumn::Status => row.reported().word(),
FoldColumn::Share | FoldColumn::Notes => String::new(),
FoldColumn::Mem => info
.memory_bytes
.map_or_else(|| "-".to_string(), human_bytes),
FoldColumn::Cpu => app
.cpu_now(info.id)
.map_or_else(|| "-".to_string(), |cpu| format!("{cpu:.1}%")),
FoldColumn::Uptime => app
.uptime_ms(info.id)
.map_or_else(|| "-".to_string(), human_duration),
FoldColumn::Restarts => info.restarts.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::super::super::fixtures;
use super::super::columns::fold_columns_for;
use super::*;
#[test]
fn a_fold_header_shows_its_rollup_and_its_share() {
let app = fixtures::app_with(
vec![
fixtures::sheep_with(1, "api", Some("edge"), 120_000, Some(100 << 20), 2),
fixtures::sheep_with(2, "cdn", Some("edge"), 30_000, Some(150 << 20), 5),
fixtures::sheep_with(3, "batch", None, 60_000, Some(50 << 20), 0),
],
fixtures::plain(),
);
let line = fold_key_line(
&app,
&FrameFacts::new(&app),
&RowKey::Fold("edge".into()),
fold_columns_for(160),
160,
false,
);
let text = fixtures::rendered(&line);
assert!(text.contains("edge ×2"), "got {text}");
assert!(text.contains("250.0M"), "memory sums: {text}");
assert!(text.contains("30s"), "uptime is the minimum: {text}");
assert!(
text.contains("83%"),
"250 of 300 MiB of flock memory: {text}"
);
}
}