pado 0.1.0

A tidal flow through your projects
Documentation
use anyhow::{Context, Result};
use std::io::Write;
use std::process::{Command, Stdio, exit};

pub fn run_list(format: Option<String>, sort_by: Option<String>, starred: bool) -> Result<()> {
    let config = pado::GlobalConfig::load().unwrap_or_default();
    let list = pado::load_project_list().context("failed to load project list")?;

    let sort_order = sort_by.as_ref().unwrap_or(&config.defaults.sort_order);
    let output_format = format.as_ref().unwrap_or(&config.defaults.output_format);

    let projects = if starred {
        list.get_starred_projects()
    } else {
        list.get_projects_sorted_by(sort_order)
    };

    if projects.is_empty() {
        if starred {
            eprintln!("No starred projects. Use 'pd star' to star a project.");
        } else {
            eprintln!("No projects tracked yet. Use 'pd add' or navigate to a project with 'pd'.");
        }
        return Ok(());
    }

    match output_format.as_str() {
        "paths" => {
            for project in projects {
                println!("{}", project.path.display());
            }
        }
        "json" => {
            println!("{}", serde_json::to_string_pretty(&list)?);
        }
        _ => {
            use comfy_table::Table;

            let mut table = Table::new();
            table.set_header(vec!["Name", "Type", "Path", "Accessed", "Count", ""]);

            for project in projects {
                let last_accessed = project.last_accessed.format("%Y-%m-%d %H:%M").to_string();
                let star = if project.starred { "" } else { "" };
                table.add_row(vec![
                    &project.name,
                    &project.project_type,
                    &project.path.display().to_string(),
                    &last_accessed,
                    &project.access_count.to_string(),
                    star,
                ]);
            }

            println!("{}", table);
        }
    }
    Ok(())
}

pub fn run_recent(limit: Option<usize>) -> Result<()> {
    let config = pado::GlobalConfig::load().unwrap_or_default();
    let list = pado::load_project_list().context("failed to load project list")?;

    let limit = limit.unwrap_or(config.defaults.recent_limit);
    let projects = list.get_recent_projects(limit);

    if projects.is_empty() {
        eprintln!("No projects tracked yet.");
        return Ok(());
    }

    use comfy_table::Table;

    let mut table = Table::new();
    table.set_header(vec!["Name", "Type", "Path", "Last Accessed"]);

    for project in projects {
        let last_accessed = pado::format_time_ago(project.last_accessed.timestamp());
        table.add_row(vec![
            &project.name,
            &project.project_type,
            &project.path.display().to_string(),
            &last_accessed,
        ]);
    }

    println!("{}", table);
    Ok(())
}

pub fn run_stats() -> Result<()> {
    let list = pado::load_project_list().context("failed to load project list")?;

    let projects: Vec<_> = list.projects.values().collect();

    if projects.is_empty() {
        eprintln!("No projects tracked yet.");
        return Ok(());
    }

    println!("\nProject Statistics:\n");
    println!("Total projects: {}", projects.len());
    println!(
        "Starred projects: {}",
        projects.iter().filter(|p| p.starred).count()
    );

    let mut sorted_by_access: Vec<_> = projects.clone();
    sorted_by_access.sort_by(|a, b| b.access_count.cmp(&a.access_count));

    println!("\nMost Accessed:");
    for project in sorted_by_access.iter().take(10) {
        let star = if project.starred { "" } else { "  " };
        println!(
            "  {}{:<30} {} accesses",
            star, project.name, project.access_count
        );
    }

    let mut sorted_by_time: Vec<_> = projects.clone();
    sorted_by_time.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));

    println!("\nRecently Accessed:");
    for project in sorted_by_time.iter().take(10) {
        let star = if project.starred { "" } else { "  " };
        let time_ago = pado::format_time_ago(project.last_accessed.timestamp());
        println!("  {}{:<30} {}", star, project.name, time_ago);
    }

    let mut type_counts: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();
    for project in &projects {
        *type_counts.entry(project.project_type.clone()).or_insert(0) += 1;
    }

    println!("\nProject Types:");
    let mut types: Vec<_> = type_counts.iter().collect();
    types.sort_by(|a, b| b.1.cmp(a.1));
    for (ptype, count) in types {
        println!("  {:<15} {}", ptype, count);
    }

    println!();
    Ok(())
}

pub fn run_switch(recent: bool, starred: bool) -> Result<()> {
    let config = pado::GlobalConfig::load().unwrap_or_default();
    let list = pado::load_project_list().context("failed to load project list")?;

    let projects = if starred {
        list.get_starred_projects()
    } else if recent {
        list.get_recent_projects(config.defaults.recent_limit)
    } else {
        list.get_projects_sorted_by(&config.defaults.sort_order)
    };

    if projects.is_empty() {
        if starred {
            eprintln!("No starred projects. Use 'pd star' to star a project.");
        } else if recent {
            eprintln!("No recent projects.");
        } else {
            eprintln!("No projects tracked yet. Use 'pd add' or navigate to a project with 'pd'.");
        }
        exit(1);
    }

    let mut fzf = Command::new("fzf")
        .arg("--height=40%")
        .arg("--reverse")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .context("failed to spawn fzf - is it installed?")?;

    if let Some(stdin) = fzf.stdin.as_mut() {
        for project in projects {
            let star = if project.starred { "" } else { "  " };
            writeln!(
                stdin,
                "{}{}\t{}\t{}",
                star,
                project.name,
                project.project_type,
                project.path.display()
            )?;
        }
    }

    let output = fzf.wait_with_output()?;

    if output.status.success() && !output.stdout.is_empty() {
        let selection = String::from_utf8_lossy(&output.stdout);
        if let Some(path) = selection.split('\t').nth(2) {
            let path = path.trim();
            println!("cd \"{}\"", path);
        }
    }
    Ok(())
}