use super::*;
#[derive(Clone, Debug, PartialEq)]
pub struct ContainerRow {
pub id: String,
pub alias: String,
pub name: String,
pub image: String,
pub state: String,
pub status: String,
pub ports: String,
pub uptime: Option<String>,
pub cache_timestamp: u64,
}
pub(crate) fn clean_name(raw: &str) -> String {
raw.strip_prefix('/').unwrap_or(raw).to_string()
}
pub(crate) fn is_running(state: &str) -> bool {
design::is_container_running(state)
}
pub(crate) fn current_unix_secs() -> u64 {
if crate::demo_flag::is_demo() {
crate::demo_flag::now_secs()
} else {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
}
#[derive(Clone, Debug)]
pub enum ContainerListItem {
HostHeader {
alias: String,
total: usize,
running: usize,
},
Container(ContainerRow),
}
impl ContainerListItem {
#[allow(dead_code)]
pub(crate) fn is_header(&self) -> bool {
matches!(self, ContainerListItem::HostHeader { .. })
}
pub(crate) fn as_container(&self) -> Option<&ContainerRow> {
match self {
ContainerListItem::Container(row) => Some(row),
_ => None,
}
}
}
pub(crate) fn visible_items(app: &App) -> Vec<ContainerListItem> {
let fp = view_fingerprint(app);
let cached = app
.containers_overview
.view_cache
.borrow()
.as_ref()
.filter(|(cached_fp, _)| *cached_fp == fp)
.map(|(_, items)| items.clone());
if let Some(items) = cached {
return items;
}
let items = build_visible_items(app);
*app.containers_overview.view_cache.borrow_mut() = Some((fp, items.clone()));
items
}
pub(crate) fn build_visible_items(app: &App) -> Vec<ContainerListItem> {
let mut rows = collect_rows(app);
sort_rows(&mut rows, app.containers_overview.sort_mode);
match app.containers_overview.sort_mode {
ContainersSortMode::AlphaHost => {
intersperse_host_headers(rows, &app.containers_overview.collapsed_hosts)
}
ContainersSortMode::AlphaContainer => {
rows.into_iter().map(ContainerListItem::Container).collect()
}
}
}
pub(crate) fn view_fingerprint(app: &App) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
(app.containers_overview.sort_mode as u8).hash(&mut hasher);
app.search.query().hash(&mut hasher);
let mut collapsed: Vec<&String> = app.containers_overview.collapsed_hosts.iter().collect();
collapsed.sort();
collapsed.len().hash(&mut hasher);
for c in collapsed {
c.hash(&mut hasher);
}
let mut aliases: Vec<&String> = app.container_state.cache().keys().collect();
aliases.sort();
aliases.len().hash(&mut hasher);
for alias in aliases {
let Some(entry) = app.container_state.cache_entry(alias) else {
continue;
};
alias.hash(&mut hasher);
entry.timestamp.hash(&mut hasher);
entry.containers.len().hash(&mut hasher);
}
hasher.finish()
}
#[cfg(test)]
pub(crate) fn visible_rows(app: &App) -> Vec<ContainerRow> {
visible_items(app)
.into_iter()
.filter_map(|item| match item {
ContainerListItem::Container(row) => Some(row),
_ => None,
})
.collect()
}
pub(crate) fn collect_rows(app: &App) -> Vec<ContainerRow> {
let query = app
.search
.query()
.map(|q| q.to_lowercase())
.filter(|q| !q.is_empty());
let mut rows: Vec<ContainerRow> = Vec::new();
for (alias, entry) in app.container_state.cache() {
for c in &entry.containers {
let name = clean_name(&c.names);
if let Some(ref q) = query {
let alias_match = alias.to_lowercase().contains(q);
let name_match = name.to_lowercase().contains(q);
let image_match = c.image.to_lowercase().contains(q);
if !alias_match && !name_match && !image_match {
continue;
}
}
rows.push(ContainerRow {
id: c.id.clone(),
alias: alias.clone(),
name,
image: c.image.clone(),
state: c.state.clone(),
status: c.status.clone(),
ports: c.ports.clone(),
uptime: crate::containers::parse_uptime_from_status(&c.status),
cache_timestamp: entry.timestamp,
});
}
}
rows
}
pub(crate) fn intersperse_host_headers(
rows: Vec<ContainerRow>,
collapsed_hosts: &std::collections::HashSet<String>,
) -> Vec<ContainerListItem> {
let mut totals: std::collections::HashMap<String, (usize, usize)> =
std::collections::HashMap::new();
for row in &rows {
let entry = totals.entry(row.alias.clone()).or_insert((0, 0));
entry.0 += 1;
if is_running(&row.state) {
entry.1 += 1;
}
}
let mut items: Vec<ContainerListItem> = Vec::with_capacity(rows.len() + totals.len());
let mut current_alias: Option<String> = None;
for row in rows {
if Some(&row.alias) != current_alias.as_ref() {
let (total, running) = totals.get(&row.alias).copied().unwrap_or((0, 0));
items.push(ContainerListItem::HostHeader {
alias: row.alias.clone(),
total,
running,
});
current_alias = Some(row.alias.clone());
}
if !collapsed_hosts.contains(&row.alias) {
items.push(ContainerListItem::Container(row));
}
}
items
}
pub(crate) fn sort_rows(rows: &mut [ContainerRow], mode: ContainersSortMode) {
match mode {
ContainersSortMode::AlphaHost => {
rows.sort_by_cached_key(|r| {
(r.alias.to_ascii_lowercase(), r.name.to_ascii_lowercase())
});
}
ContainersSortMode::AlphaContainer => {
rows.sort_by_cached_key(|r| {
(r.name.to_ascii_lowercase(), r.alias.to_ascii_lowercase())
});
}
}
}
pub(crate) fn total_cached_count(app: &App) -> usize {
app.container_state
.cache()
.values()
.map(|e| e.containers.len())
.sum()
}