use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::layout::{MIN_WIDTH, fit};
pub const NAME_MIN: u16 = 8;
pub const NAME_MAX: u16 = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Column {
Id,
Name,
Status,
Pid,
Restarts,
Exit,
Cfg,
CpuSpark,
Cpu,
MemCeil,
Mem,
Uptime,
Fold,
Smit,
}
impl Column {
#[must_use]
pub const fn header(self) -> &'static str {
match self {
Self::Id => "ID",
Self::Name => "NAME",
Self::Status => "STATUS",
Self::Pid => "PID",
Self::Restarts => "RESTARTS",
Self::Exit => "EXIT",
Self::Cfg => "CFG",
Self::CpuSpark => "CPU 20s",
Self::Cpu => "CPU",
Self::MemCeil => "MEM/CEIL",
Self::Mem => "MEM",
Self::Uptime => "UPTIME",
Self::Fold => "FOLD",
Self::Smit => "SMIT",
}
}
#[must_use]
pub const fn frozen_header(self) -> &'static str {
match self {
Self::Uptime => "FROZEN",
_ => self.header(),
}
}
#[must_use]
pub const fn width(self) -> u16 {
match self {
Self::Id => 4,
Self::Name => 0,
Self::Status => 15,
Self::Pid => 7,
Self::Restarts => 8,
Self::Exit => 9,
Self::Cfg => 4,
Self::CpuSpark => 10,
Self::Cpu => 6,
Self::MemCeil => 10,
Self::Mem => 8,
Self::Uptime => 8,
Self::Fold => 10,
Self::Smit => 13,
}
}
}
pub(super) const ALL: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cfg,
Column::CpuSpark,
Column::Cpu,
Column::MemCeil,
Column::Mem,
Column::Uptime,
Column::Fold,
Column::Smit,
];
const NO_CEIL: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cfg,
Column::CpuSpark,
Column::Cpu,
Column::Mem,
Column::Uptime,
Column::Fold,
Column::Smit,
];
const NO_SPARK: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cfg,
Column::Cpu,
Column::Mem,
Column::Uptime,
Column::Fold,
Column::Smit,
];
const NO_CFG: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cpu,
Column::Mem,
Column::Uptime,
Column::Fold,
Column::Smit,
];
const NO_SMIT: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cpu,
Column::Mem,
Column::Uptime,
Column::Fold,
];
const NO_FOLD: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Exit,
Column::Cpu,
Column::Mem,
Column::Uptime,
];
const NO_EXIT: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Restarts,
Column::Cpu,
Column::Mem,
Column::Uptime,
];
const NO_RESTARTS: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Pid,
Column::Cpu,
Column::Mem,
Column::Uptime,
];
const NO_PID: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Cpu,
Column::Mem,
Column::Uptime,
];
const NO_MEM: &[Column] = &[
Column::Id,
Column::Name,
Column::Status,
Column::Cpu,
Column::Uptime,
];
const NO_CPU: &[Column] = &[Column::Id, Column::Name, Column::Status, Column::Uptime];
const FLOOR: &[Column] = &[Column::Id, Column::Name, Column::Status];
#[cfg(test)]
pub(in crate::lookout::view) fn cfg_tier_width() -> u16 {
TIERS
.iter()
.filter(|(_, columns)| columns.contains(&Column::Cfg))
.map(|(threshold, _)| *threshold)
.min()
.expect("a tier draws CFG")
}
const TIERS: &[(u16, &[Column])] = &[
(146, ALL),
(134, NO_CEIL),
(122, NO_SPARK),
(116, NO_CFG),
(101, NO_SMIT),
(89, NO_FOLD),
(78, NO_EXIT),
(68, NO_RESTARTS),
(59, NO_PID),
(49, NO_MEM),
(41, NO_CPU),
(MIN_WIDTH, FLOOR),
];
#[must_use]
pub fn columns_for(width: u16) -> &'static [Column] {
TIERS
.iter()
.find(|(threshold, _)| width >= *threshold)
.map_or(FLOOR, |(_, columns)| *columns)
}
#[must_use]
pub fn name_width(width: u16, columns: &[Column]) -> u16 {
let fixed: u16 = columns.iter().map(|column| column.width()).sum();
name_width_from(width, fixed, columns.len())
}
fn name_width_from(width: u16, fixed: u16, columns_len: usize) -> u16 {
let gaps = u16::try_from(columns_len.saturating_sub(1)).unwrap_or(0) * 2;
width
.saturating_sub(fixed)
.saturating_sub(gaps)
.clamp(NAME_MIN, NAME_MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FoldColumn {
Name,
Status,
Share,
Mem,
Cpu,
Uptime,
Restarts,
Notes,
}
impl FoldColumn {
#[must_use]
pub const fn width(self) -> u16 {
match self {
Self::Name => 0,
Self::Status => 12,
Self::Share => 22,
Self::Mem => 10,
Self::Cpu => 9,
Self::Uptime => 10,
Self::Restarts => 8,
Self::Notes => 63,
}
}
#[must_use]
pub const fn header(self) -> &'static str {
match self {
Self::Name => "NAME",
Self::Status => "STATUS",
Self::Share => "SHARE",
Self::Mem => "MEM",
Self::Cpu => "CPU",
Self::Uptime => "UPTIME",
Self::Restarts => "RESTARTS",
Self::Notes => "NOTES",
}
}
}
const FOLD_ALL: &[FoldColumn] = &[
FoldColumn::Name,
FoldColumn::Status,
FoldColumn::Share,
FoldColumn::Mem,
FoldColumn::Cpu,
FoldColumn::Uptime,
FoldColumn::Restarts,
FoldColumn::Notes,
];
const FOLD_NO_SHARE: &[FoldColumn] = &[
FoldColumn::Name,
FoldColumn::Status,
FoldColumn::Mem,
FoldColumn::Cpu,
FoldColumn::Uptime,
FoldColumn::Restarts,
FoldColumn::Notes,
];
const FOLD_NO_NOTES: &[FoldColumn] = &[
FoldColumn::Name,
FoldColumn::Status,
FoldColumn::Mem,
FoldColumn::Cpu,
FoldColumn::Uptime,
FoldColumn::Restarts,
];
const FOLD_NO_RESTARTS: &[FoldColumn] = &[
FoldColumn::Name,
FoldColumn::Status,
FoldColumn::Mem,
FoldColumn::Cpu,
FoldColumn::Uptime,
];
const FOLD_NO_CPU: &[FoldColumn] = &[
FoldColumn::Name,
FoldColumn::Status,
FoldColumn::Mem,
FoldColumn::Uptime,
];
const FOLD_NO_UPTIME: &[FoldColumn] = &[FoldColumn::Name, FoldColumn::Status, FoldColumn::Mem];
const FOLD_FLOOR: &[FoldColumn] = &[FoldColumn::Name, FoldColumn::Status];
const FOLD_TIERS: &[(u16, &[FoldColumn])] = &[
(158, FOLD_ALL),
(134, FOLD_NO_SHARE),
(69, FOLD_NO_NOTES),
(59, FOLD_NO_RESTARTS),
(48, FOLD_NO_CPU),
(36, FOLD_NO_UPTIME),
(MIN_WIDTH, FOLD_FLOOR),
];
#[must_use]
pub fn fold_columns_for(width: u16) -> &'static [FoldColumn] {
FOLD_TIERS
.iter()
.find(|(threshold, _)| width >= *threshold)
.map_or(FOLD_FLOOR, |(_, columns)| *columns)
}
pub(super) fn fold_name_width(width: u16, columns: &[FoldColumn]) -> u16 {
let fixed: u16 = columns.iter().map(|column| column.width()).sum();
name_width_from(width, fixed, columns.len()).max(FOLD_NAME_MIN)
}
const FOLD_NAME_MIN: u16 = NAME_MIN + 2;
#[must_use]
pub fn header_line(columns: &[Column], width: u16, style: Style, frozen: bool) -> Line<'static> {
let name = name_width(width, columns);
let mut text = String::new();
for (index, column) in columns.iter().enumerate() {
if index > 0 {
text.push_str(" ");
}
let cell_width = if *column == Column::Name {
name
} else {
column.width()
};
let header = if frozen {
column.frozen_header()
} else {
column.header()
};
text.push_str(&fit(header, cell_width));
}
Line::from(Span::styled(text, style))
}
#[must_use]
pub fn fold_columns_header_line(columns: &[FoldColumn], width: u16, style: Style) -> Line<'static> {
let name = fold_name_width(width, columns);
let mut text = String::new();
for (index, column) in columns.iter().enumerate() {
if index > 0 {
text.push_str(" ");
}
let cell_width = if *column == FoldColumn::Name {
name
} else {
column.width()
};
text.push_str(&fit(column.header(), cell_width));
}
Line::from(Span::styled(text, style))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_frozen_header_fits_the_column_it_renames() {
use crate::output::width::visible_width;
assert_eq!(Column::Uptime.frozen_header(), "FROZEN");
assert!(
visible_width(Column::Uptime.frozen_header()) <= usize::from(Column::Uptime.width()),
"FROZEN does not fit UPTIME's own 8 cells"
);
assert!(
visible_width("FROZEN AT") > usize::from(Column::Uptime.width()),
"the design's own header now fits, so this rename has no reason to exist"
);
}
#[test]
fn only_the_uptime_column_is_renamed_by_a_freeze() {
let renamed: Vec<&str> = ALL
.iter()
.filter(|column| column.frozen_header() != column.header())
.map(|column| column.header())
.collect();
assert_eq!(renamed, vec!["UPTIME"]);
}
#[test]
fn columns_drop_in_a_fixed_order_as_the_terminal_narrows() {
assert_eq!(columns_for(300).len(), 14);
assert_eq!(columns_for(122).len(), 12);
assert!(!columns_for(121).contains(&Column::Cfg));
assert!(columns_for(121).contains(&Column::Smit));
assert_eq!(columns_for(116).len(), 11);
assert!(!columns_for(115).contains(&Column::Smit));
assert!(columns_for(115).contains(&Column::Fold));
assert_eq!(columns_for(101).len(), 10);
assert!(!columns_for(100).contains(&Column::Fold));
assert!(columns_for(100).contains(&Column::Exit));
assert!(!columns_for(88).contains(&Column::Exit));
assert!(columns_for(88).contains(&Column::Restarts));
assert!(!columns_for(77).contains(&Column::Restarts));
assert!(!columns_for(67).contains(&Column::Pid));
assert!(!columns_for(58).contains(&Column::Mem));
assert!(!columns_for(48).contains(&Column::Cpu));
assert_eq!(columns_for(31), &[Column::Id, Column::Name, Column::Status]);
for width in [31u16, 40, 48, 58, 67, 77, 88, 100, 300] {
let cols = columns_for(width);
for required in [Column::Id, Column::Name, Column::Status] {
assert!(
cols.contains(&required),
"width {width} dropped {required:?}"
);
}
}
}
#[test]
fn smit_survives_the_gallerys_own_wide_fixtures() {
assert!(columns_for(120).contains(&Column::Smit));
}
#[test]
fn the_two_new_rungs_restore_todays_table_before_shedding_anything_old() {
assert_eq!(columns_for(146), ALL);
assert!(!columns_for(134).contains(&Column::MemCeil));
assert!(columns_for(134).contains(&Column::CpuSpark));
assert!(!columns_for(122).contains(&Column::CpuSpark));
assert_eq!(
columns_for(122),
NO_SPARK,
"122 is today's full set, unchanged"
);
}
#[test]
fn every_shared_header_still_matches_flock_rows_exactly() {
use crate::output::Render;
let shared: Vec<&str> = ALL
.iter()
.map(|column| column.header())
.filter(|header| crate::output::FlockRows::headers().contains(header))
.collect();
assert_eq!(shared, crate::output::FlockRows::headers());
}
#[test]
fn the_only_headers_lookout_adds_are_the_two_shep_flock_cannot_draw() {
use crate::output::Render;
let extra: Vec<&str> = ALL
.iter()
.map(|column| column.header())
.filter(|header| !crate::output::FlockRows::headers().contains(header))
.collect();
assert_eq!(extra, vec!["CPU 20s", "MEM/CEIL"]);
}
#[test]
fn every_tier_fits_the_width_it_claims() {
for width in MIN_WIDTH..=200 {
let cols = columns_for(width);
let fixed: u16 = cols.iter().map(|c| c.width()).sum();
let gaps = u16::try_from(cols.len() - 1).unwrap() * 2;
assert!(
fixed + gaps + NAME_MIN <= width,
"width {width} chose {} columns needing {}",
cols.len(),
fixed + gaps + NAME_MIN
);
}
}
#[test]
fn every_fold_tier_fits_the_width_it_claims() {
for width in MIN_WIDTH..=200 {
let cols = fold_columns_for(width);
let fixed: u16 = cols.iter().map(|c| c.width()).sum();
let gaps = u16::try_from(cols.len() - 1).unwrap() * 2;
assert!(
fixed + gaps + FOLD_NAME_MIN <= width,
"width {width} chose {} columns needing {}",
cols.len(),
fixed + gaps + FOLD_NAME_MIN
);
}
}
#[test]
fn the_share_bar_is_the_first_column_to_go() {
let wide = fold_columns_for(200);
assert!(wide.contains(&FoldColumn::Share));
let narrow = fold_columns_for(100);
assert!(!narrow.contains(&FoldColumn::Share), "got {narrow:?}");
}
}