use ratatui::layout::Rect;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Gauge {
Cpu,
Temp,
Power,
Memory,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Tier {
Full,
Grid,
Compact,
}
impl Tier {
pub fn header_rows(self) -> u16 {
match self {
Tier::Full | Tier::Grid => 9,
Tier::Compact => 5,
}
}
pub fn gauge_rows(self) -> u16 {
match self {
Tier::Grid => 24, Tier::Full => 12,
Tier::Compact => 8,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Body {
ThreeCards,
ProcessesOverPorts,
ProcessesOnly,
}
#[derive(Clone, Debug)]
pub struct Screen {
pub tier: Tier,
pub gauges: Vec<Gauge>,
pub body: Body,
pub network: bool,
}
const TEMP_COLS: u16 = 50;
const POWER_COLS: u16 = 70;
const THREE_CARDS_COLS: u16 = 120;
impl Screen {
pub fn resolve(area: Rect) -> Self {
let mut gauges = vec![Gauge::Cpu];
if area.width >= TEMP_COLS {
gauges.push(Gauge::Temp);
}
if area.width >= POWER_COLS {
gauges.push(Gauge::Power);
}
gauges.push(Gauge::Memory);
let tier = if area.height >= 30 && area.width >= THREE_CARDS_COLS {
Tier::Full
} else if area.height >= 40 && gauges.len() == 4 {
Tier::Grid
} else {
Tier::Compact
};
let body = if area.width >= THREE_CARDS_COLS {
Body::ThreeCards
} else if area.height >= 20 {
Body::ProcessesOverPorts
} else {
Body::ProcessesOnly
};
Screen { tier, gauges, body, network: area.height >= 16 }
}
}
#[cfg(test)]
mod tests {
use super::{Body, Gauge, Screen, Tier};
use ratatui::layout::Rect;
fn at(w: u16, h: u16) -> Screen {
Screen::resolve(Rect::new(0, 0, w, h))
}
#[test]
fn the_grid_tier_always_has_four_cards_to_place() {
for w in 20u16..=250 {
for h in [40u16, 45, 60, 100] {
let s = at(w, h);
if s.tier == Tier::Grid {
assert_eq!(s.gauges.len(), 4, "{w}x{h}: grid tier with a gap in the 2x2");
}
}
}
}
#[test]
fn gauges_drop_out_in_priority_order_and_never_lose_cpu_or_memory() {
for w in 1u16..=250 {
let g = at(w, 40).gauges;
assert_eq!(g.first(), Some(&Gauge::Cpu), "{w}: CPU must always survive");
assert_eq!(g.last(), Some(&Gauge::Memory), "{w}: Memory must always survive");
if g.contains(&Gauge::Power) {
assert!(g.contains(&Gauge::Temp), "{w}: Power without Temp");
}
}
assert_eq!(at(49, 40).gauges, vec![Gauge::Cpu, Gauge::Memory]);
assert_eq!(at(50, 40).gauges, vec![Gauge::Cpu, Gauge::Temp, Gauge::Memory]);
assert_eq!(
at(70, 40).gauges,
vec![Gauge::Cpu, Gauge::Temp, Gauge::Power, Gauge::Memory]
);
}
#[test]
fn tier_boundaries() {
assert_eq!(at(120, 30).tier, Tier::Full);
assert_eq!(at(119, 30).tier, Tier::Compact, "too narrow for full, too short for grid");
assert_eq!(at(120, 29).tier, Tier::Compact);
assert_eq!(at(103, 45).tier, Tier::Grid, "narrow but tall keeps the hero design");
assert_eq!(at(103, 39).tier, Tier::Compact, "one row below the grid floor");
assert_eq!(at(69, 45).tier, Tier::Compact, "one column below the grid floor");
}
#[test]
fn the_three_card_body_and_the_full_tier_are_decided_independently() {
let s = at(120, 24);
assert_eq!(s.body, Body::ThreeCards);
assert_eq!(s.tier, Tier::Compact);
}
#[test]
fn the_body_sheds_ports_then_network_as_height_gets_tight() {
assert_eq!(at(80, 24).body, Body::ProcessesOverPorts);
assert!(at(80, 24).network);
assert_eq!(at(80, 19).body, Body::ProcessesOnly, "ports goes first");
assert!(at(80, 19).network, "network outlives ports");
assert!(!at(80, 15).network, "network goes next");
}
#[test]
fn every_size_resolves_without_panicking_or_producing_an_empty_gauge_band() {
for w in 0u16..=200 {
for h in 0u16..=60 {
let s = at(w, h);
assert!(!s.gauges.is_empty(), "{w}x{h}: no gauges at all");
assert!(s.tier.header_rows() > 0 && s.tier.gauge_rows() > 0);
}
}
}
}