hyper-jump 0.11.0

A generalist all-in-one command-line version manager toolset for a more efficient development workflow.
use std::path::PathBuf;

use anyhow::Error;
use anyhow::Result;
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_FULL;
use comfy_table::Cell;
use comfy_table::CellAlignment;
use comfy_table::Color;
use comfy_table::Table;
use serde::Serialize;
use tracing::info;

use crate::domain::package::Package;
use crate::ports::Fs;
use crate::ports::Output;
use crate::ports::Paths;
use crate::ports::UsedVersionStore;
use crate::OutputFormat;

#[derive(Serialize)]
struct ListedVersion {
    version: String,
    status: String,
}

pub async fn list_installed(
    package: Package,
    fmt: OutputFormat,
    output: &impl Output,
    paths: &impl Paths,
    used_store: &impl UsedVersionStore,
    fs: &impl Fs,
) -> Result<(), Error> {
    let downloads_dir = paths.downloads_dir(package.clone()).await?;

    let entries: Vec<PathBuf> = fs.read_dir(&downloads_dir).await?;

    if entries.is_empty() {
        info!("There are no versions installed");
        return Ok(());
    }

    let current = used_store.current(package.clone()).await?;
    let current_norm = current.as_deref().map(normalize_tag);

    match fmt {
        OutputFormat::Json => {
            let mut rows = Vec::new();
            for path in entries {
                if !fs.is_dir(&path).await? {
                    continue;
                }
                let path_name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
                let path_norm = normalize_tag(&path_name);
                let status = match current_norm {
                    Some(cur) if cur == path_norm => "used",
                    _ => "installed",
                };
                rows.push(ListedVersion {
                    version: path_name,
                    status: status.to_string(),
                });
            }
            let json = serde_json::to_string_pretty(&rows)?;
            output.write_line(&json)?;
        }
        OutputFormat::Table => {
            let mut table = Table::new();
            let header = vec!["Version", "Status"];
            table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS);
            table.set_header(header);

            for path in entries {
                if !fs.is_dir(&path).await? {
                    continue;
                }

                let path_name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
                let path_norm = normalize_tag(&path_name);
                let status = match current_norm {
                    Some(cur) if cur == path_norm => Cell::new("Used").fg(Color::Green),
                    _ => Cell::new("Installed"),
                };

                table.add_row(vec![
                    Cell::new(path_name).set_alignment(CellAlignment::Center),
                    status,
                ]);
            }

            output.write_line(&table.to_string())?;
        }
    }

    Ok(())
}

fn normalize_tag(tag: &str) -> &str {
    match tag.strip_prefix('v') {
        Some(rest) if rest.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) => rest,
        _ => tag,
    }
}