proto_cli 0.23.2

A multi-language version manager, a unified toolchain.
use crate::error::ProtoCliError;
use crate::helpers::load_configured_tools_with_filters;
use crate::printer::Printer;
use chrono::{DateTime, NaiveDateTime};
use clap::Args;
use miette::IntoDiagnostic;
use proto_core::Id;
use starbase::system;
use starbase_styles::color;
use starbase_utils::json;
use std::collections::{HashMap, HashSet};
use tracing::info;

#[derive(Args, Clone, Debug)]
pub struct ListToolsArgs {
    #[arg(help = "ID of tools to list")]
    ids: Vec<Id>,

    #[arg(long, help = "Print the list in JSON format")]
    json: bool,
}

#[system]
pub async fn list(args: ArgsRef<ListToolsArgs>) {
    if !args.json {
        info!("Loading tools...");
    }

    let tools = load_configured_tools_with_filters(HashSet::from_iter(&args.ids)).await?;

    let mut tools = tools
        .into_iter()
        .filter(|tool| !tool.manifest.installed_versions.is_empty())
        .collect::<Vec<_>>();

    tools.sort_by(|a, d| a.id.cmp(&d.id));

    if tools.is_empty() {
        return Err(ProtoCliError::NoInstalledTools.into());
    }

    if args.json {
        let items = tools
            .into_iter()
            .map(|t| (t.id, t.manifest))
            .collect::<HashMap<_, _>>();

        println!("{}", json::to_string_pretty(&items).into_diagnostic()?);

        return Ok(());
    }

    let mut printer = Printer::new();

    for tool in tools {
        printer.line();
        printer.header(&tool.id, &tool.metadata.name);

        printer.section(|p| {
            p.entry("Store", color::path(tool.get_inventory_dir()));

            p.entry_map(
                "Aliases",
                tool.manifest
                    .aliases
                    .iter()
                    .map(|(k, v)| (color::hash(v.to_string()), color::label(k)))
                    .collect::<Vec<_>>(),
                None,
            );

            let mut versions = tool.manifest.installed_versions.iter().collect::<Vec<_>>();
            versions.sort();

            p.entry_map(
                "Versions",
                versions
                    .iter()
                    .map(|version| {
                        let mut comments = vec![];
                        let mut is_default = false;

                        if let Some(meta) = &tool.manifest.versions.get(version) {
                            if let Some(at) = create_datetime(meta.installed_at) {
                                comments.push(format!("installed {}", at.format("%x")));
                            }

                            if let Some(last_used) = &meta.last_used_at {
                                if let Some(at) = create_datetime(*last_used) {
                                    comments.push(format!("last used {}", at.format("%x")));
                                }
                            }
                        }

                        if tool
                            .manifest
                            .default_version
                            .as_ref()
                            .is_some_and(|dv| *dv == version.to_unresolved_spec())
                        {
                            comments.push("default version".into());
                            is_default = true;
                        }

                        (
                            if is_default {
                                color::invalid(version.to_string())
                            } else {
                                color::hash(version.to_string())
                            },
                            color::muted_light(comments.join(", ")),
                        )
                    })
                    .collect::<Vec<_>>(),
                None,
            );

            Ok(())
        })?;
    }

    printer.flush();
}

fn create_datetime(millis: u128) -> Option<NaiveDateTime> {
    DateTime::from_timestamp((millis / 1000) as i64, ((millis % 1000) * 1_000_000) as u32)
        .map(|dt| dt.naive_local())
}