xbp 10.14.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! Feature-gated Docker helpers. When the `docker` feature is enabled, the
//! `xbp -l` / `xbp list` view can include a snapshot of running containers.

use crate::logging::{log_info, log_warn};
use crate::utils::command_exists;
use colored::Colorize;
use tokio::process::Command;

const COMMAND_NAME: &str = "docker";

/// Print a concise `docker ps` table if Docker is available.
/// Tries non-sudo first, then a non-interactive sudo fallback when possible.
pub async fn print_docker_ps(debug: bool) -> Result<(), String> {
    if !command_exists("docker") {
        if debug {
            println!("{}", "Docker not detected; skipping docker ps.".dimmed());
        }
        return Ok(());
    }

    let has_sudo = command_exists("sudo");
    let candidates = candidate_ps_commands(has_sudo);
    let mut last_error: Option<String> = None;

    for candidate in candidates {
        let (binary, args) = candidate
            .split_first()
            .ok_or_else(|| "docker command args were empty".to_string())?;

        let output = Command::new(binary)
            .args(args)
            .output()
            .await
            .map_err(|e| format!("Failed to run {}: {}", binary, e))?;

        if output.status.success() {
            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
            render_docker_table(&stdout);
            let _ = log_info(COMMAND_NAME, "Rendered docker ps snapshot", None).await;
            return Ok(());
        } else {
            last_error = Some(String::from_utf8_lossy(&output.stderr).to_string());
            if debug {
                println!(
                    "{} {}",
                    "docker ps failed:".yellow(),
                    last_error.as_deref().unwrap_or_default()
                );
            }
        }
    }

    if let Some(err) = last_error {
        let _ = log_warn(COMMAND_NAME, "Unable to render docker ps", Some(&err)).await;
    }
    Ok(())
}

fn render_docker_table(raw: &str) {
    println!("\n{}", "Docker containers:".bright_blue());
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        println!("{}", "No running containers.".dimmed());
    } else {
        println!("{}", trimmed);
    }
}

fn candidate_ps_commands(has_sudo: bool) -> Vec<Vec<String>> {
    let base_format = "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}";
    let mut cmds: Vec<Vec<String>> =
        vec![vec!["docker".to_string(), "ps".to_string(), "--format".to_string(), base_format.to_string()]];

    if has_sudo {
        cmds.push(vec![
            "sudo".to_string(),
            "-n".to_string(),
            "docker".to_string(),
            "ps".to_string(),
            "--format".to_string(),
            base_format.to_string(),
        ]);
    }

    cmds
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builds_non_sudo_first() {
        let cmds = candidate_ps_commands(true);
        assert_eq!(cmds[0][0], "docker");
        assert_eq!(cmds[0][1], "ps");
    }

    #[test]
    fn sudo_variant_appended_when_available() {
        let cmds = candidate_ps_commands(true);
        assert!(cmds.iter().any(|c| c.first().map(|s| s.as_str()) == Some("sudo")));
    }

    #[test]
    fn no_sudo_variant_when_not_available() {
        let cmds = candidate_ps_commands(false);
        assert!(cmds.iter().all(|c| c.first().map(|s| s.as_str()) != Some("sudo")));
    }

    #[test]
    fn render_handles_empty_output() {
        // Should not panic with empty input
        render_docker_table("");
    }
}