vscli 1.3.3

A CLI tool to launch vscode projects, which supports devcontainers.
use super::{Pickable, WrappedPickable};
use crate::history::{Entry, EntryId};
use chrono::{DateTime, Local};
use ratatui::layout::Constraint;
use std::borrow::Cow;

macro_rules! impl_wrapped_pickable {
    ($wrapper:ty, $inner:ty) => {
        impl WrappedPickable for $wrapper {
            type Inner = $inner;

            fn from_inner(inner: Self::Inner) -> Self {
                Self(inner)
            }

            fn into_inner(self) -> Self::Inner {
                self.0
            }
        }
    };
}

#[derive(Debug, Clone)]
pub struct HistoryItem {
    pub id: EntryId,
    pub entry: Entry,
}

impl Pickable for HistoryItem {
    fn title() -> &'static str {
        "Recent Workspaces"
    }

    fn headers() -> &'static [&'static str] {
        &[
            "Workspace",
            "Dev Container",
            "Config",
            "Path",
            "Last Opened",
        ]
    }

    fn cells(&self) -> Vec<String> {
        vec![
            self.entry.workspace_name.clone(),
            self.entry
                .dev_container_name
                .as_deref()
                .unwrap_or("")
                .to_string(),
            self.entry.config_name.as_deref().unwrap_or("").to_string(),
            self.entry.workspace_path.to_string_lossy().to_string(),
            DateTime::<Local>::from(self.entry.last_opened)
                .format("%Y-%m-%d %H:%M:%S")
                .to_string(),
        ]
    }

    fn search_fields(&self) -> Vec<String> {
        vec![
            self.entry.workspace_name.clone(),
            self.entry.dev_container_name.clone().unwrap_or_default(),
            self.entry.config_name.clone().unwrap_or_default(),
            self.entry.workspace_path.to_string_lossy().to_string(),
        ]
    }

    fn status_lines(&self) -> Vec<String> {
        let args_count = self.entry.behavior.args.len();
        let args_joined = self
            .entry
            .behavior
            .args
            .iter()
            .map(|arg| arg.to_string_lossy())
            .collect::<Vec<Cow<'_, str>>>()
            .join(", ");
        let config_path = self
            .entry
            .config_path
            .as_ref()
            .map(|path| path.to_string_lossy().into_owned())
            .unwrap_or_default();

        vec![
            format!(
                "Strategy: {} • Command: {} • Args ({args_count}): {args_joined}",
                self.entry.behavior.strategy, self.entry.behavior.command,
            ),
            format!("Dev Container: {config_path}"),
        ]
    }

    fn column_constraints(max_widths: &[usize]) -> Vec<Constraint> {
        let workspace_width = max_widths.first().copied().unwrap_or(20).clamp(9, 60);
        let devcontainer_width = max_widths.get(1).copied().unwrap_or(20).clamp(9, 60);
        let config_width = max_widths.get(2).copied().unwrap_or(6).clamp(6, 40);

        vec![
            Constraint::Min(u16::try_from(workspace_width).unwrap_or(u16::MAX)),
            Constraint::Min(u16::try_from(devcontainer_width).unwrap_or(u16::MAX)),
            Constraint::Min(u16::try_from(config_width).unwrap_or(u16::MAX)),
            Constraint::Percentage(70),
            Constraint::Min(20),
        ]
    }
}

#[derive(Clone, Debug)]
pub struct ContainerItem(pub crate::container::Container);

impl Pickable for ContainerItem {
    fn title() -> &'static str {
        "Devcontainers"
    }

    fn headers() -> &'static [&'static str] {
        &["Container ID", "Status", "Image", "Project Path"]
    }

    fn cells(&self) -> Vec<String> {
        vec![
            self.0.short_id.clone(),
            self.0.status.clone(),
            self.0.image.clone(),
            self.0.local_folder.clone(),
        ]
    }

    fn search_fields(&self) -> Vec<String> {
        let mut fields = self.cells();
        fields.push(self.0.config_file.clone());
        fields
    }

    fn status_lines(&self) -> Vec<String> {
        vec![format!("Config: {}", self.0.config_file)]
    }

    fn column_constraints(max_widths: &[usize]) -> Vec<Constraint> {
        let status_width = max_widths.get(1).copied().unwrap_or(6).clamp(6, 30);
        vec![
            Constraint::Min(13),
            Constraint::Min(u16::try_from(status_width).unwrap_or(6)),
            Constraint::Percentage(40),
            Constraint::Percentage(60),
        ]
    }
}

impl_wrapped_pickable!(ContainerItem, crate::container::Container);

#[derive(Clone, Debug)]
pub struct ConfigItem(pub crate::config_store::ConfigEntry);

impl Pickable for ConfigItem {
    fn title() -> &'static str {
        "Configs"
    }

    fn headers() -> &'static [&'static str] {
        &["Name", "Description", "Path"]
    }

    fn cells(&self) -> Vec<String> {
        vec![
            self.0.name.clone(),
            self.0.description.as_deref().unwrap_or("").to_string(),
            self.0.root.display().to_string(),
        ]
    }

    fn search_fields(&self) -> Vec<String> {
        vec![
            self.0.name.clone(),
            self.0.description.clone().unwrap_or_default(),
            self.0.root.to_string_lossy().to_string(),
        ]
    }

    fn status_lines(&self) -> Vec<String> {
        vec![]
    }

    fn column_constraints(max_widths: &[usize]) -> Vec<Constraint> {
        let name_width = max_widths.first().copied().unwrap_or(4).clamp(4, 30);
        let description_width = max_widths.get(1).copied().unwrap_or(4).clamp(4, 40);
        vec![
            Constraint::Min(u16::try_from(name_width).unwrap_or(4)),
            Constraint::Min(u16::try_from(description_width).unwrap_or(4)),
            Constraint::Percentage(70),
        ]
    }
}

impl_wrapped_pickable!(ConfigItem, crate::config_store::ConfigEntry);

#[derive(Clone, Debug)]
pub struct DevContainerItem(pub crate::workspace::DevContainer);

impl Pickable for DevContainerItem {
    fn title() -> &'static str {
        "Select Dev Container"
    }

    fn headers() -> &'static [&'static str] {
        &["Name", "Config Path"]
    }

    fn cells(&self) -> Vec<String> {
        vec![
            self.0.name.as_deref().unwrap_or("(unnamed)").to_string(),
            self.0.config_path.display().to_string(),
        ]
    }

    fn search_fields(&self) -> Vec<String> {
        vec![
            self.0.name.clone().unwrap_or_default(),
            self.0.config_path.to_string_lossy().to_string(),
        ]
    }

    fn status_lines(&self) -> Vec<String> {
        vec![format!("Workspace: {}", self.0.workspace_path_in_container)]
    }

    fn column_constraints(max_widths: &[usize]) -> Vec<Constraint> {
        let name_width = max_widths.first().copied().unwrap_or(9).clamp(9, 40);
        vec![
            Constraint::Min(u16::try_from(name_width).unwrap_or(9)),
            Constraint::Percentage(80),
        ]
    }
}

impl_wrapped_pickable!(DevContainerItem, crate::workspace::DevContainer);