1use crate::app::{ContainerActionRequest, ContainerExecRequest, ContainerLogsRequest};
4
5pub struct ContainerSession {
10 pub alias: String,
11 pub askpass: Option<String>,
12 pub runtime: Option<crate::containers::ContainerRuntime>,
13 pub containers: Vec<crate::containers::ContainerInfo>,
14 pub list_state: ratatui::widgets::ListState,
15 pub loading: bool,
16 pub error: Option<String>,
17 pub action_in_progress: Option<String>,
18 pub confirm_action: Option<(crate::containers::ContainerAction, String, String)>,
20}
21
22#[derive(Debug, Default)]
25pub struct ContainerState {
26 pub(in crate::app) pending_exec: Option<ContainerExecRequest>,
27 pub(in crate::app) pending_logs: Option<ContainerLogsRequest>,
28 pub(in crate::app) pending_actions: std::collections::VecDeque<ContainerActionRequest>,
29 pub(in crate::app) pending_fetch_aliases: Vec<String>,
30 pub(in crate::app) cache:
31 std::collections::HashMap<String, crate::containers::ContainerCacheEntry>,
32}
33
34impl ContainerState {
35 pub fn cache(
36 &self,
37 ) -> &std::collections::HashMap<String, crate::containers::ContainerCacheEntry> {
38 &self.cache
39 }
40
41 pub fn set_cache(
42 &mut self,
43 cache: std::collections::HashMap<String, crate::containers::ContainerCacheEntry>,
44 ) {
45 self.cache = cache;
46 }
47
48 pub fn cache_entry(&self, alias: &str) -> Option<&crate::containers::ContainerCacheEntry> {
49 self.cache.get(alias)
50 }
51
52 pub fn cache_entry_mut(
53 &mut self,
54 alias: &str,
55 ) -> Option<&mut crate::containers::ContainerCacheEntry> {
56 self.cache.get_mut(alias)
57 }
58
59 pub fn cache_contains(&self, alias: &str) -> bool {
60 self.cache.contains_key(alias)
61 }
62
63 pub fn cache_len(&self) -> usize {
64 self.cache.len()
65 }
66
67 pub fn insert_cache_entry(
68 &mut self,
69 alias: String,
70 entry: crate::containers::ContainerCacheEntry,
71 ) {
72 self.cache.insert(alias, entry);
73 }
74
75 pub fn remove_cache_entry(&mut self, alias: &str) {
76 self.cache.remove(alias);
77 }
78
79 pub fn clear_cache(&mut self) {
80 self.cache.clear();
81 }
82
83 pub fn pending_exec_request(&self) -> Option<&ContainerExecRequest> {
84 self.pending_exec.as_ref()
85 }
86
87 pub fn pending_logs_request(&self) -> Option<&ContainerLogsRequest> {
88 self.pending_logs.as_ref()
89 }
90
91 pub fn has_pending_fetches(&self) -> bool {
92 !self.pending_fetch_aliases.is_empty()
93 }
94
95 pub fn pending_actions_len(&self) -> usize {
96 self.pending_actions.len()
97 }
98
99 pub fn take_pending_exec(&mut self) -> Option<ContainerExecRequest> {
100 self.pending_exec.take()
101 }
102
103 pub fn take_pending_logs(&mut self) -> Option<ContainerLogsRequest> {
104 self.pending_logs.take()
105 }
106
107 pub fn pop_next_action(&mut self) -> Option<ContainerActionRequest> {
108 self.pending_actions.pop_front()
109 }
110
111 pub fn pending_actions_iter(&self) -> impl Iterator<Item = &ContainerActionRequest> {
112 self.pending_actions.iter()
113 }
114
115 pub fn pending_actions_at(&self, idx: usize) -> Option<&ContainerActionRequest> {
116 self.pending_actions.get(idx)
117 }
118
119 pub fn pending_fetch_aliases(&self) -> &[String] {
120 &self.pending_fetch_aliases
121 }
122
123 pub fn extend_pending_fetches<I: IntoIterator<Item = String>>(&mut self, iter: I) {
124 self.pending_fetch_aliases.extend(iter);
125 }
126
127 pub fn queue_logs(&mut self, req: ContainerLogsRequest) {
131 if let Some(prev) = self.pending_logs.as_ref() {
132 log::debug!(
133 "[purple] queue_logs replaced pending request for alias={} id={}",
134 prev.alias,
135 prev.container_id,
136 );
137 }
138 self.pending_logs = Some(req);
139 }
140
141 pub fn queue_exec(&mut self, req: ContainerExecRequest) {
144 if let Some(prev) = self.pending_exec.as_ref() {
145 log::debug!(
146 "[purple] queue_exec replaced pending request for alias={} id={}",
147 prev.alias,
148 prev.container_id,
149 );
150 }
151 self.pending_exec = Some(req);
152 }
153
154 pub fn queue_action(&mut self, req: ContainerActionRequest) {
158 self.pending_actions.push_back(req);
159 }
160
161 pub fn queue_fetch(&mut self, alias: String) {
164 self.pending_fetch_aliases.push(alias);
165 }
166
167 pub fn drain_pending_fetches(&mut self) -> Vec<String> {
169 std::mem::take(&mut self.pending_fetch_aliases)
170 }
171
172 pub fn migrate_alias(&mut self, old: &str, new: &str) -> bool {
176 if old == new {
177 return false;
178 }
179 if let Some(v) = self.cache.remove(old) {
180 debug_assert!(
181 !self.cache.contains_key(new),
182 "container_state.cache collision on rename {old} -> {new}"
183 );
184 self.cache.insert(new.to_string(), v);
185 true
186 } else {
187 false
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::containers::{ContainerCacheEntry, ContainerRuntime};
196
197 fn make_logs_request(alias: &str) -> ContainerLogsRequest {
198 ContainerLogsRequest {
199 alias: alias.to_string(),
200 askpass: None,
201 runtime: ContainerRuntime::Docker,
202 container_id: "abc123".to_string(),
203 container_name: "nginx".to_string(),
204 }
205 }
206
207 fn make_cache_entry() -> ContainerCacheEntry {
208 ContainerCacheEntry {
209 timestamp: 1700000000,
210 runtime: ContainerRuntime::Docker,
211 engine_version: Some("28.0.0".to_string()),
212 containers: vec![],
213 }
214 }
215
216 #[test]
217 fn queue_logs_sets_pending() {
218 let mut state = ContainerState::default();
219 assert!(state.pending_logs.is_none());
220 state.queue_logs(make_logs_request("host-a"));
221 assert!(state.pending_logs.is_some());
222 assert_eq!(state.pending_logs.as_ref().unwrap().alias, "host-a");
223 }
224
225 #[test]
226 fn queue_logs_replaces_previous() {
227 let mut state = ContainerState::default();
228 state.queue_logs(make_logs_request("host-a"));
229 state.queue_logs(make_logs_request("host-b"));
230 assert_eq!(state.pending_logs.as_ref().unwrap().alias, "host-b");
231 }
232
233 #[test]
234 fn queue_exec_sets_pending() {
235 let mut state = ContainerState::default();
236 assert!(state.pending_exec.is_none());
237 state.queue_exec(ContainerExecRequest {
238 alias: "host-a".to_string(),
239 askpass: None,
240 runtime: ContainerRuntime::Docker,
241 container_id: "abc".to_string(),
242 container_name: "nginx".to_string(),
243 command: Some("echo hi".to_string()),
244 });
245 assert!(state.pending_exec.is_some());
246 assert_eq!(state.pending_exec.as_ref().unwrap().alias, "host-a");
247 }
248
249 #[test]
250 fn queue_fetch_pushes_alias() {
251 let mut state = ContainerState::default();
252 state.queue_fetch("host-a".to_string());
253 state.queue_fetch("host-b".to_string());
254 assert_eq!(state.pending_fetch_aliases, vec!["host-a", "host-b"]);
255 }
256
257 #[test]
258 fn drain_pending_fetches_returns_and_clears() {
259 let mut state = ContainerState::default();
260 state.queue_fetch("host-a".to_string());
261 state.queue_fetch("host-b".to_string());
262 let drained = state.drain_pending_fetches();
263 assert_eq!(drained, vec!["host-a", "host-b"]);
264 assert!(state.pending_fetch_aliases.is_empty());
265 }
266
267 #[test]
268 fn drain_pending_fetches_empty_when_no_aliases() {
269 let mut state = ContainerState::default();
270 let drained = state.drain_pending_fetches();
271 assert!(drained.is_empty());
272 assert!(state.pending_fetch_aliases.is_empty());
273 }
274
275 #[test]
276 fn migrate_alias_renames_cache_entry() {
277 let mut state = ContainerState::default();
278 state.cache.insert("old".to_string(), make_cache_entry());
279 assert!(state.migrate_alias("old", "new"));
280 assert!(state.cache.contains_key("new"));
281 assert!(!state.cache.contains_key("old"));
282 }
283
284 #[test]
285 fn migrate_alias_returns_false_when_no_entry() {
286 let mut state = ContainerState::default();
287 assert!(!state.migrate_alias("missing", "new"));
288 assert!(state.cache.is_empty());
289 }
290
291 #[test]
292 fn migrate_alias_self_rename_is_noop() {
293 let mut state = ContainerState::default();
294 state.cache.insert("same".to_string(), make_cache_entry());
295 assert!(!state.migrate_alias("same", "same"));
296 assert!(state.cache.contains_key("same"));
297 }
298
299 #[test]
300 fn queue_action_pushes_back_in_order() {
301 let mut state = ContainerState::default();
302 for id in ["a", "b", "c"] {
303 state.queue_action(ContainerActionRequest {
304 alias: "host".to_string(),
305 askpass: None,
306 runtime: ContainerRuntime::Docker,
307 container_id: id.to_string(),
308 container_name: id.to_string(),
309 action: crate::containers::ContainerAction::Restart,
310 });
311 }
312 assert_eq!(state.pending_actions.len(), 3);
313 let ids: Vec<String> = state
314 .pending_actions
315 .iter()
316 .map(|r| r.container_id.clone())
317 .collect();
318 assert_eq!(ids, vec!["a", "b", "c"]);
319 }
320}