use std::collections::{HashMap, HashSet, VecDeque};
use super::host_state::ViewMode;
use crate::containers::{ContainerInspect, ContainerRuntime};
#[derive(Debug, Clone)]
pub struct RefreshQueueItem {
pub alias: String,
pub askpass: Option<String>,
pub cached_runtime: Option<ContainerRuntime>,
pub has_tunnel: bool,
}
#[derive(Debug, Default)]
pub struct RefreshBatch {
pub queue: VecDeque<RefreshQueueItem>,
pub in_flight: usize,
pub total: usize,
pub completed: usize,
pub in_flight_aliases: HashSet<String>,
}
pub const REFRESH_MAX_PARALLEL: usize = 4;
#[derive(Debug, Clone)]
pub struct ContainerExecRequest {
pub alias: String,
pub askpass: Option<String>,
pub runtime: ContainerRuntime,
pub container_id: String,
pub container_name: String,
pub command: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ContainerLogsRequest {
pub alias: String,
pub askpass: Option<String>,
pub runtime: ContainerRuntime,
pub container_id: String,
pub container_name: String,
}
#[derive(Debug, Clone)]
pub struct ContainerActionRequest {
pub alias: String,
pub askpass: Option<String>,
pub runtime: ContainerRuntime,
pub container_id: String,
pub container_name: String,
pub action: crate::containers::ContainerAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContainersSortMode {
#[default]
AlphaHost,
AlphaContainer,
}
impl ContainersSortMode {
pub fn next(self) -> Self {
match self {
ContainersSortMode::AlphaHost => ContainersSortMode::AlphaContainer,
ContainersSortMode::AlphaContainer => ContainersSortMode::AlphaHost,
}
}
pub fn label(self) -> &'static str {
match self {
ContainersSortMode::AlphaHost => "A-Z host",
ContainersSortMode::AlphaContainer => "A-Z container",
}
}
pub fn to_key(self) -> &'static str {
match self {
ContainersSortMode::AlphaHost => "alpha_host",
ContainersSortMode::AlphaContainer => "alpha_container",
}
}
pub fn from_key(s: &str) -> Self {
match s {
"alpha_container" => ContainersSortMode::AlphaContainer,
_ => ContainersSortMode::AlphaHost,
}
}
}
#[derive(Debug, Clone)]
pub struct InspectCacheEntry {
pub timestamp: u64,
pub result: Result<ContainerInspect, String>,
}
#[derive(Debug, Default)]
pub struct InspectCache {
pub entries: HashMap<String, InspectCacheEntry>,
pub in_flight: HashSet<String>,
}
#[derive(Debug, Clone)]
pub struct LogsCacheEntry {
pub timestamp: u64,
pub result: Result<Vec<String>, String>,
}
#[derive(Debug, Default)]
pub struct LogsCache {
pub entries: HashMap<String, LogsCacheEntry>,
pub in_flight: HashSet<String>,
}
pub const INSPECT_CACHE_TTL_SECS: u64 = 30;
pub const LOGS_CACHE_TTL_SECS: u64 = 30;
pub const LOGS_TAIL: usize = 50;
pub const LIST_CACHE_TTL_SECS: u64 = 30;
#[derive(Debug)]
pub struct ContainersOverviewState {
pub sort_mode: ContainersSortMode,
pub inspect_cache: InspectCache,
pub logs_cache: LogsCache,
pub refresh_batch: Option<RefreshBatch>,
pub auto_list_in_flight: HashSet<String>,
pub view_mode: ViewMode,
pub collapsed_hosts: HashSet<String>,
pub view_cache:
std::cell::RefCell<Option<(u64, Vec<crate::ui::containers_overview::ContainerListItem>)>>,
}
impl Default for ContainersOverviewState {
fn default() -> Self {
Self {
sort_mode: ContainersSortMode::default(),
inspect_cache: InspectCache::default(),
logs_cache: LogsCache::default(),
refresh_batch: None,
auto_list_in_flight: HashSet::new(),
view_mode: ViewMode::Detailed,
collapsed_hosts: HashSet::new(),
view_cache: std::cell::RefCell::new(None),
}
}
}
impl ContainersOverviewState {
pub fn start_refresh(&mut self, batch: RefreshBatch) {
self.refresh_batch = Some(batch);
}
pub fn clear_refresh(&mut self) {
self.refresh_batch = None;
}
pub(crate) fn hydrate_from_prefs(&mut self) {
self.view_mode = crate::preferences::load_containers_view_mode();
self.sort_mode = crate::preferences::load_containers_sort_mode();
self.collapsed_hosts = crate::preferences::load_containers_collapsed_hosts();
}
pub fn set_view_mode(&mut self, mode: ViewMode) -> std::io::Result<()> {
self.view_mode = mode;
crate::preferences::save_containers_view_mode(mode).inspect_err(|e| {
log::warn!("[config] Failed to persist containers view mode: {e}");
})
}
pub fn set_sort_mode(&mut self, mode: ContainersSortMode) -> std::io::Result<()> {
self.sort_mode = mode;
crate::preferences::save_containers_sort_mode(mode).inspect_err(|e| {
log::warn!("[config] Failed to persist containers sort mode: {e}");
})
}
pub fn migrate_alias(&mut self, old: &str, new: &str) -> bool {
if old == new {
return false;
}
if self.auto_list_in_flight.remove(old) {
debug_assert!(
!self.auto_list_in_flight.contains(new),
"auto_list_in_flight collision on rename {old} -> {new}"
);
self.auto_list_in_flight.insert(new.to_string());
}
if let Some(batch) = self.refresh_batch.as_mut() {
if batch.in_flight_aliases.remove(old) {
debug_assert!(
!batch.in_flight_aliases.contains(new),
"refresh_batch.in_flight_aliases collision on rename {old} -> {new}"
);
batch.in_flight_aliases.insert(new.to_string());
}
}
if self.collapsed_hosts.remove(old) {
debug_assert!(
!self.collapsed_hosts.contains(new),
"collapsed_hosts collision on rename {old} -> {new}"
);
self.collapsed_hosts.insert(new.to_string());
true
} else {
false
}
}
}
impl InspectCache {
pub fn fresh(&self, container_id: &str, now: u64) -> Option<&InspectCacheEntry> {
self.entries
.get(container_id)
.filter(|e| now.saturating_sub(e.timestamp) < INSPECT_CACHE_TTL_SECS)
}
}
impl LogsCache {
pub fn fresh(&self, container_id: &str, now: u64) -> Option<&LogsCacheEntry> {
self.entries
.get(container_id)
.filter(|e| now.saturating_sub(e.timestamp) < LOGS_CACHE_TTL_SECS)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::preferences::tests_helpers::with_temp_prefs;
fn batch_with_aliases(aliases: &[&str]) -> RefreshBatch {
RefreshBatch {
queue: VecDeque::new(),
in_flight: aliases.len(),
total: aliases.len(),
completed: 0,
in_flight_aliases: aliases.iter().map(|a| a.to_string()).collect(),
}
}
#[test]
fn start_refresh_installs_batch() {
let mut state = ContainersOverviewState::default();
assert!(state.refresh_batch.is_none());
state.start_refresh(batch_with_aliases(&["host-a", "host-b"]));
let batch = state.refresh_batch.as_ref().unwrap();
assert_eq!(batch.total, 2);
assert_eq!(batch.in_flight, 2);
assert!(batch.in_flight_aliases.contains("host-a"));
}
#[test]
fn clear_refresh_drops_batch() {
let mut state = ContainersOverviewState::default();
state.start_refresh(batch_with_aliases(&["host-a"]));
state.clear_refresh();
assert!(state.refresh_batch.is_none());
}
#[test]
fn hydrate_from_prefs_reads_persisted_values() {
with_temp_prefs("hydrate_from_prefs", |_path| {
crate::preferences::save_containers_view_mode(ViewMode::Compact).unwrap();
crate::preferences::save_containers_sort_mode(ContainersSortMode::AlphaContainer)
.unwrap();
let mut collapsed = std::collections::HashSet::new();
collapsed.insert("folded-host".to_string());
crate::preferences::save_containers_collapsed_hosts(&collapsed).unwrap();
let mut state = ContainersOverviewState::default();
state.hydrate_from_prefs();
assert_eq!(state.view_mode, ViewMode::Compact);
assert_eq!(state.sort_mode, ContainersSortMode::AlphaContainer);
assert!(state.collapsed_hosts.contains("folded-host"));
});
}
#[test]
fn set_view_mode_updates_field_and_persists() {
with_temp_prefs("set_view_mode", |_path| {
let mut state = ContainersOverviewState::default();
state.set_view_mode(ViewMode::Compact).unwrap();
assert_eq!(state.view_mode, ViewMode::Compact);
assert_eq!(
crate::preferences::load_containers_view_mode(),
ViewMode::Compact
);
});
}
#[test]
fn set_sort_mode_updates_field_and_persists() {
with_temp_prefs("set_sort_mode", |_path| {
let mut state = ContainersOverviewState::default();
state
.set_sort_mode(ContainersSortMode::AlphaContainer)
.unwrap();
assert_eq!(state.sort_mode, ContainersSortMode::AlphaContainer);
assert_eq!(
crate::preferences::load_containers_sort_mode(),
ContainersSortMode::AlphaContainer
);
});
}
#[test]
fn migrate_alias_renames_auto_list_in_flight() {
let mut state = ContainersOverviewState::default();
state.auto_list_in_flight.insert("old".to_string());
state.migrate_alias("old", "new");
assert!(state.auto_list_in_flight.contains("new"));
assert!(!state.auto_list_in_flight.contains("old"));
}
#[test]
fn migrate_alias_renames_refresh_batch_in_flight() {
let mut state = ContainersOverviewState::default();
state.start_refresh(batch_with_aliases(&["old"]));
assert!(!state.migrate_alias("old", "new"));
let batch = state.refresh_batch.as_ref().unwrap();
assert!(batch.in_flight_aliases.contains("new"));
assert!(!batch.in_flight_aliases.contains("old"));
}
#[test]
fn migrate_alias_self_rename_is_noop() {
let mut state = ContainersOverviewState::default();
state.collapsed_hosts.insert("same".to_string());
state.auto_list_in_flight.insert("same".to_string());
assert!(!state.migrate_alias("same", "same"));
assert!(state.collapsed_hosts.contains("same"));
assert!(state.auto_list_in_flight.contains("same"));
}
#[test]
fn migrate_alias_renames_collapsed_hosts_and_returns_true() {
let mut state = ContainersOverviewState::default();
state.collapsed_hosts.insert("old".to_string());
assert!(state.migrate_alias("old", "new"));
assert!(state.collapsed_hosts.contains("new"));
assert!(!state.collapsed_hosts.contains("old"));
}
#[test]
fn migrate_alias_returns_false_when_collapsed_unchanged() {
let mut state = ContainersOverviewState::default();
state.auto_list_in_flight.insert("old".to_string());
assert!(!state.migrate_alias("old", "new"));
assert!(state.auto_list_in_flight.contains("new"));
}
#[test]
fn migrate_alias_is_noop_when_nothing_matches() {
let mut state = ContainersOverviewState::default();
assert!(!state.migrate_alias("missing", "new"));
}
}