use super::*;
pub(crate) const GAP_W: usize = 4;
pub(crate) const HIGHLIGHT_W: usize = 1;
pub(crate) const MARKER_W: usize = 1;
pub(crate) const STATUS_DOT_W: usize = 2;
pub(crate) const HOST_MIN: usize = 8;
pub(crate) const NAME_MIN: usize = 8;
pub(crate) const IMAGE_MIN: usize = 12;
pub(crate) const UPTIME_W: usize = 8;
pub(crate) struct Columns {
pub(crate) host: usize,
pub(crate) name: usize,
pub(crate) image: usize,
pub(crate) show_uptime: bool,
pub(crate) show_host: bool,
}
pub(crate) fn compute_columns<'a, I>(rows: I, content_w: usize, show_host: bool) -> Columns
where
I: IntoIterator<Item = &'a ContainerRow> + Clone,
{
let host_content = rows
.clone()
.into_iter()
.map(|r| r.alias.width())
.max()
.unwrap_or(0);
let host = if show_host {
host_content.max(HOST_MIN)
} else {
0
};
let name_content = rows
.clone()
.into_iter()
.map(|r| r.name.width())
.max()
.unwrap_or(0);
let name = name_content.max(NAME_MIN);
let image_content = rows.into_iter().map(|r| r.image.width()).max().unwrap_or(0);
let image_max = image_content.max(IMAGE_MIN);
let host_segment = if show_host { host + GAP_W } else { 0 };
let always_on_with_image =
|image: usize| HIGHLIGHT_W + MARKER_W + STATUS_DOT_W + host_segment + name + GAP_W + image;
let with_uptime_min = always_on_with_image(IMAGE_MIN) + GAP_W + UPTIME_W;
let show_uptime = content_w >= with_uptime_min;
let total_max =
always_on_with_image(image_max) + if show_uptime { GAP_W + UPTIME_W } else { 0 };
let image = if total_max > content_w {
let excess = total_max - content_w;
image_max.saturating_sub(excess).max(IMAGE_MIN)
} else if show_uptime {
let consumed = always_on_with_image(0) + GAP_W + UPTIME_W;
content_w.saturating_sub(consumed).max(image_max)
} else {
image_max
};
Columns {
host,
name,
image,
show_uptime,
show_host,
}
}
pub(crate) fn pad_or_truncate(s: &str, w: usize) -> String {
let cur = s.width();
match cur.cmp(&w) {
std::cmp::Ordering::Equal => s.to_string(),
std::cmp::Ordering::Less => format!("{}{}", s, " ".repeat(w - cur)),
std::cmp::Ordering::Greater => {
let truncated = crate::ui::truncate(s, w);
let tw = truncated.width();
if tw < w {
format!("{}{}", truncated, " ".repeat(w - tw))
} else {
truncated
}
}
}
}
pub(crate) fn first_visible_index(items: &[ContainerListItem]) -> Option<usize> {
if items.is_empty() { None } else { Some(0) }
}
pub(crate) fn position_of_container(app: &App, alias: &str, container_id: &str) -> Option<usize> {
visible_items(app).iter().position(|item| match item {
ContainerListItem::Container(row) => row.alias == alias && row.id == container_id,
_ => false,
})
}
pub(crate) fn build_stats_title(container_count: usize) -> Line<'static> {
Line::from(vec![Span::styled(
format!(" {} ", container_count),
theme::bold(),
)])
}
pub(crate) fn render_host_header_row<'a>(alias: &'a str, content_w: usize) -> ListItem<'a> {
let prefix = format!("── {} ", alias);
let available = content_w.saturating_sub(1);
let fill_width = available.saturating_sub(prefix.width());
ListItem::new(Line::from(vec![
Span::styled(prefix, theme::bold()),
Span::styled("─".repeat(fill_width), theme::muted()),
]))
}
pub(crate) fn render_header(
frame: &mut Frame,
area: Rect,
cols: &Columns,
sort_mode: ContainersSortMode,
) {
let style = theme::bold();
let gap = " ".repeat(GAP_W);
let host_arrow = matches!(sort_mode, ContainersSortMode::AlphaHost);
let name_arrow = matches!(sort_mode, ContainersSortMode::AlphaContainer);
let host_label = if host_arrow { "HOST \u{25BE}" } else { "HOST" };
let name_label = if name_arrow { "NAME \u{25BE}" } else { "NAME" };
let leading_pad = " ".repeat(HIGHLIGHT_W + MARKER_W + STATUS_DOT_W);
let mut spans = vec![Span::styled(leading_pad, style)];
if cols.show_host {
spans.push(Span::styled(
format!("{:<width$}", host_label, width = cols.host),
style,
));
spans.push(Span::raw(gap.clone()));
}
spans.push(Span::styled(
format!("{:<width$}", name_label, width = cols.name),
style,
));
spans.push(Span::raw(gap.clone()));
spans.push(Span::styled(
format!("{:<width$}", "IMAGE", width = cols.image),
style,
));
if cols.show_uptime {
spans.push(Span::raw(gap));
spans.push(Span::styled(
format!("{:>width$}", "UPTIME", width = UPTIME_W),
style,
));
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
pub(crate) fn render_row<'a>(
row: &'a ContainerRow,
cols: &Columns,
health: Option<&str>,
inspect_exit_code: Option<i32>,
spinner_tick: u64,
) -> ListItem<'a> {
let (state_glyph, state_style) = state_glyph(
&row.state,
health,
&row.status,
inspect_exit_code,
spinner_tick,
);
let image = crate::ui::truncate(&row.image, cols.image);
let mut spans: Vec<Span<'static>> = vec![
Span::raw(" ".repeat(MARKER_W)),
Span::styled(format!("{} ", state_glyph), state_style),
];
if cols.show_host {
spans.push(Span::styled(
pad_or_truncate(&row.alias, cols.host),
theme::bold(),
));
spans.push(Span::raw(GAP));
}
spans.push(Span::styled(
pad_or_truncate(&row.name, cols.name),
theme::bold(),
));
spans.push(Span::raw(GAP));
spans.push(Span::styled(
pad_or_truncate(&image, cols.image),
theme::muted(),
));
if cols.show_uptime {
spans.push(Span::raw(GAP));
match &row.uptime {
Some(uptime) => spans.push(Span::styled(
format!("{:>width$}", uptime, width = UPTIME_W),
theme::muted(),
)),
None => spans.push(Span::styled(
format!("{:>width$}", "-", width = UPTIME_W),
theme::muted(),
)),
}
}
ListItem::new(Line::from(spans))
}
pub(crate) fn state_glyph(
state: &str,
health: Option<&str>,
status: &str,
inspect_exit_code: Option<i32>,
spinner_tick: u64,
) -> (&'static str, ratatui::style::Style) {
design::container_state_style(state, health, status, inspect_exit_code, spinner_tick)
}