use crate::app::{ContainerActionRequest, ContainerExecRequest, ContainerLogsRequest};
pub struct ContainerSession {
pub alias: String,
pub askpass: Option<String>,
pub runtime: Option<crate::containers::ContainerRuntime>,
pub containers: Vec<crate::containers::ContainerInfo>,
pub list_state: ratatui::widgets::ListState,
pub loading: bool,
pub error: Option<String>,
pub action_in_progress: Option<String>,
pub confirm_action: Option<(crate::containers::ContainerAction, String, String)>,
}
#[derive(Debug, Default)]
pub struct ContainerState {
pub(in crate::app) pending_exec: Option<ContainerExecRequest>,
pub(in crate::app) pending_logs: Option<ContainerLogsRequest>,
pub(in crate::app) pending_actions: std::collections::VecDeque<ContainerActionRequest>,
pub(in crate::app) pending_fetch_aliases: Vec<String>,
pub(in crate::app) cache:
std::collections::HashMap<String, crate::containers::ContainerCacheEntry>,
}
impl ContainerState {
pub fn cache(
&self,
) -> &std::collections::HashMap<String, crate::containers::ContainerCacheEntry> {
&self.cache
}
pub fn set_cache(
&mut self,
cache: std::collections::HashMap<String, crate::containers::ContainerCacheEntry>,
) {
self.cache = cache;
}
pub fn cache_entry(&self, alias: &str) -> Option<&crate::containers::ContainerCacheEntry> {
self.cache.get(alias)
}
pub fn cache_entry_mut(
&mut self,
alias: &str,
) -> Option<&mut crate::containers::ContainerCacheEntry> {
self.cache.get_mut(alias)
}
pub fn cache_contains(&self, alias: &str) -> bool {
self.cache.contains_key(alias)
}
pub fn cache_len(&self) -> usize {
self.cache.len()
}
pub fn insert_cache_entry(
&mut self,
alias: String,
entry: crate::containers::ContainerCacheEntry,
) {
self.cache.insert(alias, entry);
}
pub fn remove_cache_entry(&mut self, alias: &str) {
self.cache.remove(alias);
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn pending_exec_request(&self) -> Option<&ContainerExecRequest> {
self.pending_exec.as_ref()
}
pub fn pending_logs_request(&self) -> Option<&ContainerLogsRequest> {
self.pending_logs.as_ref()
}
pub fn has_pending_fetches(&self) -> bool {
!self.pending_fetch_aliases.is_empty()
}
pub fn pending_actions_len(&self) -> usize {
self.pending_actions.len()
}
pub fn take_pending_exec(&mut self) -> Option<ContainerExecRequest> {
self.pending_exec.take()
}
pub fn take_pending_logs(&mut self) -> Option<ContainerLogsRequest> {
self.pending_logs.take()
}
pub fn pop_next_action(&mut self) -> Option<ContainerActionRequest> {
self.pending_actions.pop_front()
}
pub fn pending_actions_iter(&self) -> impl Iterator<Item = &ContainerActionRequest> {
self.pending_actions.iter()
}
pub fn pending_actions_at(&self, idx: usize) -> Option<&ContainerActionRequest> {
self.pending_actions.get(idx)
}
pub fn pending_fetch_aliases(&self) -> &[String] {
&self.pending_fetch_aliases
}
pub fn extend_pending_fetches<I: IntoIterator<Item = String>>(&mut self, iter: I) {
self.pending_fetch_aliases.extend(iter);
}
pub fn queue_logs(&mut self, req: ContainerLogsRequest) {
if let Some(prev) = self.pending_logs.as_ref() {
log::debug!(
"[purple] queue_logs replaced pending request for alias={} id={}",
prev.alias,
prev.container_id,
);
}
self.pending_logs = Some(req);
}
pub fn queue_exec(&mut self, req: ContainerExecRequest) {
if let Some(prev) = self.pending_exec.as_ref() {
log::debug!(
"[purple] queue_exec replaced pending request for alias={} id={}",
prev.alias,
prev.container_id,
);
}
self.pending_exec = Some(req);
}
pub fn queue_action(&mut self, req: ContainerActionRequest) {
self.pending_actions.push_back(req);
}
pub fn queue_fetch(&mut self, alias: String) {
self.pending_fetch_aliases.push(alias);
}
pub fn drain_pending_fetches(&mut self) -> Vec<String> {
std::mem::take(&mut self.pending_fetch_aliases)
}
pub fn migrate_alias(&mut self, old: &str, new: &str) -> bool {
if old == new {
return false;
}
if let Some(v) = self.cache.remove(old) {
debug_assert!(
!self.cache.contains_key(new),
"container_state.cache collision on rename {old} -> {new}"
);
self.cache.insert(new.to_string(), v);
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::containers::{ContainerCacheEntry, ContainerRuntime};
fn make_logs_request(alias: &str) -> ContainerLogsRequest {
ContainerLogsRequest {
alias: alias.to_string(),
askpass: None,
runtime: ContainerRuntime::Docker,
container_id: "abc123".to_string(),
container_name: "nginx".to_string(),
}
}
fn make_cache_entry() -> ContainerCacheEntry {
ContainerCacheEntry {
timestamp: 1700000000,
runtime: ContainerRuntime::Docker,
engine_version: Some("28.0.0".to_string()),
containers: vec![],
}
}
#[test]
fn queue_logs_sets_pending() {
let mut state = ContainerState::default();
assert!(state.pending_logs.is_none());
state.queue_logs(make_logs_request("host-a"));
assert!(state.pending_logs.is_some());
assert_eq!(state.pending_logs.as_ref().unwrap().alias, "host-a");
}
#[test]
fn queue_logs_replaces_previous() {
let mut state = ContainerState::default();
state.queue_logs(make_logs_request("host-a"));
state.queue_logs(make_logs_request("host-b"));
assert_eq!(state.pending_logs.as_ref().unwrap().alias, "host-b");
}
#[test]
fn queue_exec_sets_pending() {
let mut state = ContainerState::default();
assert!(state.pending_exec.is_none());
state.queue_exec(ContainerExecRequest {
alias: "host-a".to_string(),
askpass: None,
runtime: ContainerRuntime::Docker,
container_id: "abc".to_string(),
container_name: "nginx".to_string(),
command: Some("echo hi".to_string()),
});
assert!(state.pending_exec.is_some());
assert_eq!(state.pending_exec.as_ref().unwrap().alias, "host-a");
}
#[test]
fn queue_fetch_pushes_alias() {
let mut state = ContainerState::default();
state.queue_fetch("host-a".to_string());
state.queue_fetch("host-b".to_string());
assert_eq!(state.pending_fetch_aliases, vec!["host-a", "host-b"]);
}
#[test]
fn drain_pending_fetches_returns_and_clears() {
let mut state = ContainerState::default();
state.queue_fetch("host-a".to_string());
state.queue_fetch("host-b".to_string());
let drained = state.drain_pending_fetches();
assert_eq!(drained, vec!["host-a", "host-b"]);
assert!(state.pending_fetch_aliases.is_empty());
}
#[test]
fn drain_pending_fetches_empty_when_no_aliases() {
let mut state = ContainerState::default();
let drained = state.drain_pending_fetches();
assert!(drained.is_empty());
assert!(state.pending_fetch_aliases.is_empty());
}
#[test]
fn migrate_alias_renames_cache_entry() {
let mut state = ContainerState::default();
state.cache.insert("old".to_string(), make_cache_entry());
assert!(state.migrate_alias("old", "new"));
assert!(state.cache.contains_key("new"));
assert!(!state.cache.contains_key("old"));
}
#[test]
fn migrate_alias_returns_false_when_no_entry() {
let mut state = ContainerState::default();
assert!(!state.migrate_alias("missing", "new"));
assert!(state.cache.is_empty());
}
#[test]
fn migrate_alias_self_rename_is_noop() {
let mut state = ContainerState::default();
state.cache.insert("same".to_string(), make_cache_entry());
assert!(!state.migrate_alias("same", "same"));
assert!(state.cache.contains_key("same"));
}
#[test]
fn queue_action_pushes_back_in_order() {
let mut state = ContainerState::default();
for id in ["a", "b", "c"] {
state.queue_action(ContainerActionRequest {
alias: "host".to_string(),
askpass: None,
runtime: ContainerRuntime::Docker,
container_id: id.to_string(),
container_name: id.to_string(),
action: crate::containers::ContainerAction::Restart,
});
}
assert_eq!(state.pending_actions.len(), 3);
let ids: Vec<String> = state
.pending_actions
.iter()
.map(|r| r.container_id.clone())
.collect();
assert_eq!(ids, vec!["a", "b", "c"]);
}
}