flyboat 2.0.0

Container environment manager for development
Documentation
use crate::Result;
use crate::config::GlobalConfig;
use crate::config::Paths;
use colored::Colorize;
use std::{fs, io, path::PathBuf};

pub fn execute_status(verbose: bool) -> Result<()> {
    let paths = Paths::new()?;
    let global_config = GlobalConfig::load(&paths.global_config())?;
    let engine = global_config.get_container_engine()?;

    if verbose {
        eprintln!("Using engine: {}", engine);
    }

    // Show flyboat directory size
    let flyboat_dir = paths.flyboat_dir();
    if flyboat_dir.exists() {
        let folder_size = dir_size(&flyboat_dir)?;

        println!(
            "{}: {} ({})",
            "Flyboat directory".bold(),
            flyboat_dir.display(),
            human_size(folder_size).cyan()
        );
    } else {
        println!(
            "{}: {} (not found)",
            "Flyboat directory".bold(),
            flyboat_dir.display()
        );
    }

    println!();

    // Show flyboat images
    println!("{}", "Flyboat Images:".bold());
    let images_output = crate::docker::execute_command_output(
        &engine,
        [
            "images",
            "--filter",
            "reference=flyboat-*",
            "--format",
            "{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}",
        ],
        &paths.flyboat_dir(),
    );

    match images_output {
        Ok(output) if !output.stdout.is_empty() => {
            let images = String::from_utf8_lossy(&output.stdout);
            println!(
                "{:<40} {:<12} {}",
                "IMAGE".dimmed(),
                "SIZE".dimmed(),
                "CREATED".dimmed()
            );
            for line in images.lines() {
                let parts: Vec<&str> = line.split('\t').collect();
                if parts.len() >= 3 {
                    println!("{:<40} {:<12} {}", parts[0].green(), parts[1], parts[2]);
                }
            }
        }
        _ => {
            println!("  No flyboat images found");
        }
    }

    println!();

    // Show running flyboat containers
    println!("{}", "Running Containers:".bold());

    let containers_output = crate::docker::execute_command_output(
        &engine,
        [
            "ps",
            "--filter",
            "name=flyboat-",
            "--format",
            "{{.Names}}\t{{.Status}}\t{{.Ports}}",
        ],
        &paths.flyboat_dir(),
    );

    match containers_output {
        Ok(output) if !output.stdout.is_empty() => {
            let containers = String::from_utf8_lossy(&output.stdout);
            println!(
                "{:<30} {:<20} {}",
                "CONTAINER".dimmed(),
                "STATUS".dimmed(),
                "PORTS".dimmed()
            );
            for line in containers.lines() {
                let parts: Vec<&str> = line.split('\t').collect();
                if parts.len() >= 2 {
                    let ports = parts.get(2).unwrap_or(&"-");
                    println!("{:<30} {:<20} {}", parts[0].green(), parts[1], ports);
                }
            }
        }
        _ => {
            println!("  No running containers");
        }
    }

    println!();

    // Show disk usage
    println!("{}", "Docker Disk Usage:".bold());
    let df_output =
        crate::docker::execute_command_output(&engine, ["system", "df"], &paths.flyboat_dir());

    if let Ok(output) = df_output {
        let df = String::from_utf8_lossy(&output.stdout);
        for line in df.lines() {
            println!("  {}", line);
        }
    }

    Ok(())
}

fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
    fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
        dir.try_fold(0, |acc, file| {
            let file = file?;
            let size = match file.metadata()? {
                data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
                data => data.len(),
            };
            Ok(acc + size)
        })
    }

    dir_size(fs::read_dir(path.into())?)
}

fn human_size(bytes: u64) -> String {
    let formats = ["B", "kB", "MB", "GB", "TB", "PB", "EB"];
    let mut value = bytes;
    let mut value_unit_index = 0;
    while value > 1000 && value_unit_index < formats.len() - 1 {
        value /= 1000;
        value_unit_index += 1;
    }
    format!(
        "{value}{}",
        formats.get(value_unit_index).map_or("?", |v| v)
    )
}