use crate::ui::fit::{cols, pick_pair, truncate_cols};
use ratatui::{
layout::Rect,
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
const GAP: u16 = 2;
const HOSTNAME_FLOOR: u16 = 8;
#[derive(Clone, Copy)]
pub struct HeaderState<'a> {
pub hostname: Option<&'a str>,
pub is_tls: bool,
pub has_token: bool,
pub metrics_ms: u128,
pub procs_ms: u128,
}
pub fn build_header(state: HeaderState<'_>, width: u16) -> (String, String) {
let host = state.hostname.unwrap_or("connecting...");
let tls = if state.is_tls {
"🔒 TLS"
} else {
"🔒✗ TLS"
};
let badges = if state.has_token {
format!("{tls} | 🔑 token")
} else {
tls.to_string()
};
let named = format!("socktop — host: {host}");
let with_badges = format!("{named} | {badges}");
let with_keys = format!("{with_badges} | (a: about, h: help, q: quit)");
let intervals = format!(
"⏱ {}ms metrics | {}ms procs",
state.metrics_ms, state.procs_ms
);
let intervals_short = format!("⏱ {}ms | {}ms", state.metrics_ms, state.procs_ms);
let ladder = [
(with_keys.as_str(), intervals.as_str()),
(with_badges.as_str(), intervals.as_str()),
(named.as_str(), intervals.as_str()),
(host, intervals.as_str()),
(host, intervals_short.as_str()),
];
let (left, right) = pick_pair(width, GAP, &ladder);
if cols(left) + cols(right) + GAP <= width {
return (left.to_string(), right.to_string());
}
let room = width
.saturating_sub(cols(&intervals_short))
.saturating_sub(GAP);
if room >= HOSTNAME_FLOOR {
return (truncate_cols(host, room), intervals_short);
}
(truncate_cols(host, width), String::new())
}
pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, title: &str, intervals: &str) {
f.render_widget(Block::default().title(title).borders(Borders::BOTTOM), area);
if intervals.is_empty() {
return;
}
let intervals_width = cols(intervals);
if area.width >= intervals_width {
let right_area = Rect {
x: area.x + area.width - intervals_width,
y: area.y,
width: intervals_width,
height: 1,
};
f.render_widget(Paragraph::new(Line::from(Span::raw(intervals))), right_area);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state(hostname: Option<&str>) -> HeaderState<'_> {
HeaderState {
hostname,
is_tls: false,
has_token: false,
metrics_ms: 500,
procs_ms: 2000,
}
}
#[test]
fn halves_never_overlap_at_any_width() {
for width in 0..=200u16 {
let (left, right) = build_header(state(Some("cachyos-gaming")), width);
let used = cols(&left) + cols(&right);
if right.is_empty() {
assert!(cols(&left) <= width, "width {width}: {left:?} overflows");
} else {
assert!(
used + GAP <= width,
"width {width}: {left:?} + {right:?} = {used} cols, no room for both"
);
}
}
}
#[test]
fn hostname_and_intervals_survive_longest() {
for width in 34..=200u16 {
let (left, right) = build_header(state(Some("cachyos-gaming")), width);
assert!(
left.contains("cachyos-gaming"),
"width {width}: lost the hostname ({left:?})"
);
assert!(
right.contains("500ms") && right.contains("2000ms"),
"width {width}: lost the intervals ({right:?})"
);
}
}
#[test]
fn detail_is_dropped_in_priority_order() {
let s = state(Some("cachyos-gaming"));
let (left, right) = build_header(s, 120);
assert_eq!(
left,
"socktop — host: cachyos-gaming | 🔒✗ TLS | (a: about, h: help, q: quit)"
);
assert_eq!(right, "⏱ 500ms metrics | 2000ms procs");
let (left, _) = build_header(s, 80);
assert_eq!(left, "socktop — host: cachyos-gaming | 🔒✗ TLS");
let (left, _) = build_header(s, 70);
assert_eq!(left, "socktop — host: cachyos-gaming");
let (left, right) = build_header(s, 50);
assert_eq!(left, "cachyos-gaming");
assert_eq!(right, "⏱ 500ms metrics | 2000ms procs");
let (left, right) = build_header(s, 34);
assert_eq!(left, "cachyos-gaming");
assert_eq!(right, "⏱ 500ms | 2000ms");
let (left, right) = build_header(s, 30);
assert_eq!(left, "cachyos-gam…");
assert_eq!(right, "⏱ 500ms | 2000ms");
}
#[test]
fn a_long_hostname_is_shortened_rather_than_winning_the_row() {
let long = "a-very-long-hostname-that-will-not-fit-anywhere";
for width in 30..=100u16 {
let (left, right) = build_header(state(Some(long)), width);
assert!(!right.is_empty(), "width {width}: intervals were dropped");
assert!(cols(&left) + cols(&right) + GAP <= width, "width {width}");
}
}
#[test]
fn hostname_is_the_final_survivor() {
let (left, right) = build_header(state(Some("cachyos-gaming")), 20);
assert!(right.is_empty(), "intervals should have been dropped");
assert!(!left.is_empty());
assert!(cols(&left) <= 20);
}
#[test]
fn tls_and_token_badges_appear_when_there_is_room() {
let s = HeaderState {
hostname: Some("host"),
is_tls: true,
has_token: true,
metrics_ms: 500,
procs_ms: 2000,
};
let (left, _) = build_header(s, 200);
assert!(left.contains("🔒 TLS"), "{left}");
assert!(left.contains("🔑 token"), "{left}");
}
#[test]
fn a_missing_hostname_reads_as_connecting() {
let (left, _) = build_header(state(None), 120);
assert!(left.contains("connecting"), "{left}");
}
}