use std::collections::BTreeMap;
use crate::error::{ComposeError, Result};
use crate::libpod::types::container::ContainerListEntry;
use crate::libpod::{urlencoded, Client, API_PREFIX};
#[derive(Debug, Clone, Default)]
pub struct LsOptions {
pub all: bool,
pub quiet: bool,
pub json: bool,
}
fn is_running(status: &str) -> bool {
let s = status.trim();
s.eq_ignore_ascii_case("running") || s.to_ascii_lowercase().starts_with("up")
}
struct Tally {
running: usize,
total: usize,
}
pub async fn list_projects(client: &Client, opts: LsOptions) -> Result<()> {
let filters = serde_json::json!({ "label": ["podup.project"] });
let path = format!(
"{API_PREFIX}/containers/json?all=true&filters={}",
urlencoded(&filters.to_string()),
);
let containers = client
.get_json::<Vec<ContainerListEntry>>(&path)
.await
.map_err(ComposeError::Podman)?;
let mut projects: BTreeMap<String, Tally> = BTreeMap::new();
for c in &containers {
let Some(project) = c.labels.get("podup.project") else {
continue;
};
let tally = projects.entry(project.clone()).or_insert(Tally {
running: 0,
total: 0,
});
tally.total += 1;
if is_running(&c.state) || is_running(&c.status) {
tally.running += 1;
}
}
let rows: Vec<(&String, &Tally)> = projects
.iter()
.filter(|(_, t)| opts.all || t.running > 0)
.collect();
if opts.quiet {
for (name, _) in &rows {
println!("{name}");
}
return Ok(());
}
if opts.json {
let arr: Vec<_> = rows
.iter()
.map(|(name, t)| serde_json::json!({ "Name": name, "Status": status_label(t) }))
.collect();
println!("{}", serde_json::to_string_pretty(&arr).unwrap_or_default());
return Ok(());
}
println!("{:<32} {:<20}", "NAME", "STATUS");
for (name, t) in &rows {
println!("{:<32} {:<20}", name, status_label(t));
}
Ok(())
}
fn status_label(t: &Tally) -> String {
if t.running > 0 {
format!("running({})", t.running)
} else {
format!("exited({})", t.total)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_running_detects_live_statuses() {
for up in ["running", "Up 2 minutes", "UP", "up about an hour"] {
assert!(is_running(up), "{up} should be running");
}
for down in ["exited", "Exited (0) 3s ago", "created", "", "stopped"] {
assert!(!is_running(down), "{down} should not be running");
}
}
#[test]
fn status_label_reflects_running_then_total() {
assert_eq!(
status_label(&Tally {
running: 2,
total: 3
}),
"running(2)"
);
assert_eq!(
status_label(&Tally {
running: 0,
total: 3
}),
"exited(3)"
);
}
}