use std::sync::mpsc;
use crate::app::{App, Screen};
use crate::containers;
use crate::event::AppEvent;
pub(crate) fn handle_container_listing(
app: &mut App,
alias: String,
result: Result<containers::ContainerListing, containers::ContainerError>,
events_tx: &mpsc::Sender<AppEvent>,
) {
if !app.hosts_state.list().iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_listing dropped: alias={} no longer in config",
alias
);
crate::askpass::cleanup_marker(&alias);
app.containers_overview.auto_list_in_flight.remove(&alias);
drive_refresh_batch(app, &alias, events_tx);
return;
}
match &result {
Ok(listing) => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
app.container_state.insert_cache_entry(
alias.clone(),
containers::ContainerCacheEntry {
timestamp: now,
runtime: listing.runtime,
engine_version: listing.engine_version.clone(),
containers: listing.containers.clone(),
},
);
containers::save_container_cache(app.container_state.cache());
crate::handler::containers_overview::prefetch_inspect_for_listing(
app,
&alias,
listing.runtime,
&listing.containers,
events_tx,
);
}
Err(e) => {
if let Some(rt) = e.runtime {
if let Some(entry) = app.container_state.cache_entry_mut(&alias) {
entry.runtime = rt;
}
}
}
}
if let Some(ref mut state) = app.container_session {
if state.alias == alias {
match result {
Ok(listing) => {
state.runtime = Some(listing.runtime);
state.containers = listing.containers;
state.loading = false;
state.error = None;
if let Some(sel) = state.list_state.selected() {
if sel >= state.containers.len() && !state.containers.is_empty() {
state.list_state.select(Some(0));
}
} else if !state.containers.is_empty() {
state.list_state.select(Some(0));
}
}
Err(e) => {
if let Some(rt) = e.runtime {
state.runtime = Some(rt);
}
state.loading = false;
state.error = Some(e.message);
}
}
}
}
crate::askpass::cleanup_marker(&alias);
app.containers_overview.auto_list_in_flight.remove(&alias);
drive_refresh_batch(app, &alias, events_tx);
}
pub(crate) fn drive_refresh_batch(app: &mut App, alias: &str, events_tx: &mpsc::Sender<AppEvent>) {
let Some(batch) = app.containers_overview.refresh_batch.as_mut() else {
return;
};
if !batch.in_flight_aliases.remove(alias) {
log::debug!(
"[purple] refresh_batch: alias={} not in batch in_flight, ignoring",
alias
);
return;
}
batch.in_flight = batch.in_flight.saturating_sub(1);
batch.completed += 1;
let next = batch.queue.pop_front();
let total = batch.total;
let completed = batch.completed;
let queue_remaining = batch.queue.len();
let spawned = next.is_some();
if let Some(item) = next {
batch.in_flight += 1;
batch.in_flight_aliases.insert(item.alias.clone());
let config_path = app.reload.config_path().to_path_buf();
let bw_session = app.bw_session.clone();
let ctx = crate::ssh_context::OwnedSshContext {
alias: item.alias,
config_path,
askpass: item.askpass,
bw_session,
has_tunnel: item.has_tunnel,
};
let tx = events_tx.clone();
containers::spawn_container_listing(ctx, item.cached_runtime, move |a, r| {
let _ = tx.send(AppEvent::ContainerListing {
alias: a,
result: r,
});
});
}
let still_in_flight = app
.containers_overview
.refresh_batch
.as_ref()
.map(|b| b.in_flight)
.unwrap_or(0);
log::debug!(
"[purple] refresh_batch: alias={} done={}/{} in_flight={} queue={} spawned_next={}",
alias,
completed,
total,
still_in_flight,
queue_remaining,
spawned
);
if queue_remaining == 0 && still_in_flight == 0 {
app.containers_overview.clear_refresh();
app.clear_status();
app.notify(crate::messages::container_refresh_complete(total));
} else {
app.notify_progress(crate::messages::container_refresh_progress(
completed, total,
));
}
}
pub(crate) fn handle_container_inspect_complete(
app: &mut App,
alias: String,
container_id: String,
result: Result<containers::ContainerInspect, String>,
) {
if !app.hosts_state.list().iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_inspect_complete dropped: alias={} no longer in config",
alias
);
app.containers_overview
.inspect_cache
.in_flight
.remove(&container_id);
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
match &result {
Ok(_) => log::debug!(
"[purple] container_inspect_complete: alias={} id={} ok=true",
alias,
container_id,
),
Err(msg) => log::warn!(
"[external] container inspect failed: alias={} id={}: {}",
alias,
container_id,
msg,
),
}
app.containers_overview.inspect_cache.entries.insert(
container_id.clone(),
crate::app::InspectCacheEntry {
timestamp: now,
result,
},
);
app.containers_overview
.inspect_cache
.in_flight
.remove(&container_id);
}
pub(crate) fn handle_container_logs_tail_complete(
app: &mut App,
alias: String,
container_id: String,
result: Result<Vec<String>, String>,
) {
if !app.hosts_state.list().iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_logs_tail_complete dropped: alias={} no longer in config",
alias
);
app.containers_overview
.logs_cache
.in_flight
.remove(&container_id);
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
match &result {
Ok(_) => log::debug!(
"[purple] container_logs_tail_complete: alias={} id={} ok=true",
alias,
container_id,
),
Err(msg) => log::warn!(
"[external] container logs tail failed: alias={} id={}: {}",
alias,
container_id,
msg,
),
}
app.containers_overview.logs_cache.entries.insert(
container_id.clone(),
crate::app::LogsCacheEntry {
timestamp: now,
result,
},
);
app.containers_overview
.logs_cache
.in_flight
.remove(&container_id);
}
pub(crate) fn handle_container_logs_complete(
app: &mut App,
alias: String,
container_id: String,
container_name: String,
result: Result<Vec<String>, String>,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if let Screen::ContainerLogs {
alias: scr_alias,
container_id: scr_id,
body,
error,
fetched_at,
scroll,
last_render_height,
search,
..
} = &mut app.screen
{
if scr_alias == &alias && scr_id == &container_id {
match result {
Ok(lines) => {
log::debug!(
"[purple] container_logs_complete: {} lines for alias={} id={}",
lines.len(),
alias,
container_id
);
*body = lines;
*error = None;
*fetched_at = now;
*scroll = crate::handler::container_logs::tail_scroll(
body.len(),
*last_render_height,
);
if let Some(s) = search.as_mut() {
crate::handler::container_logs::refresh_search(body, s);
log::debug!(
"[purple] container_logs: search refreshed query={:?} matches={}",
s.query,
s.matches.len()
);
crate::handler::container_logs::recenter_on_match(
body.len(),
*last_render_height,
s,
scroll,
);
}
}
Err(e) => {
log::warn!(
"[external] container_logs_complete: alias={} id={} error={}",
alias,
container_id,
e
);
body.clear();
*error = Some(e);
*fetched_at = now;
*scroll = 0;
}
}
return;
}
}
log::debug!(
"[purple] container_logs_complete dropped (overlay closed): alias={} id={} name={}",
alias,
container_id,
container_name
);
}
pub(crate) fn handle_container_action_complete(
app: &mut App,
alias: String,
action: containers::ContainerAction,
result: Result<(), String>,
events_tx: &mpsc::Sender<AppEvent>,
) {
let should_refresh = if let Some(ref mut state) = app.container_session {
if state.alias == alias {
state.action_in_progress = None;
match result {
Ok(()) => {
state.loading = true;
Some((state.alias.clone(), state.askpass.clone(), state.runtime))
}
Err(e) => {
state.error = Some(e);
None
}
}
} else {
None
}
} else {
None
};
if let Some((refresh_alias, askpass, cached_runtime)) = should_refresh {
app.notify(crate::messages::container_action_complete(action.as_str()));
let has_tunnel = app.tunnels.active_contains(&refresh_alias);
app.containers_overview
.auto_list_in_flight
.insert(refresh_alias.clone());
let ctx = crate::ssh_context::OwnedSshContext {
alias: refresh_alias,
config_path: app.reload.config_path().to_path_buf(),
askpass,
bw_session: app.bw_session.clone(),
has_tunnel,
};
let tx = events_tx.clone();
containers::spawn_container_listing(ctx, cached_runtime, move |a, r| {
let _ = tx.send(AppEvent::ContainerListing {
alias: a,
result: r,
});
});
}
crate::askpass::cleanup_marker(&alias);
}