crax 0.1.8

An interesting CLI for frontend programmer
Documentation
use crate::log::Log;
use anyhow::Ok;
use clap::Subcommand;
use crax_monorepo::ctx;
use delete::{delete_file_async, delete_folder_all};
use dialoguer::{theme::ColorfulTheme, MultiSelect};
use globset::{Glob, GlobSet, GlobSetBuilder};
use jwalk::WalkDir;
use loading::Loading;
use owo_colors::OwoColorize;
use path_absolutize::*;
use std::{fs, path::PathBuf, time::Instant};
use tokio::task::JoinSet;

const DEF_WS: &'static str = "packages:
  - packages/*
";

#[derive(Subcommand)]
pub enum Cmd {
    #[command(long_about = "Clear any *-lock.{json,yaml} and remove all node_modules.")]
    Cls {
        #[arg(short, long, help = "Directory you will walk")]
        dir: Option<String>,
        #[arg(short, long, default_value_t = false, help = "Weather include lock file")]
        lock: bool,
        #[arg(short, long, help = "Ignore pattern see: https://crates.io/crates/globset")]
        ignore: Option<String>,
    },

    #[command(long_about = "Add pnpm-workspace file.")]
    W {
        #[arg(short, long, help = "Directory you will walk")]
        dir: Option<String>,
    },
}

// ignore . and target
fn is_hidden(fname: Option<&str>) -> bool {
    fname.map(|s| s.starts_with(".")).unwrap_or(false) || fname.unwrap().to_string() == "target"
}

fn create_gs(glob: &str) -> Result<GlobSet, anyhow::Error> {
    let mut builder = GlobSetBuilder::new();
    builder.add(Glob::new(glob)?);
    let set = builder.build()?;

    Ok(set)
}

async fn rm(v: String) {
    let p = PathBuf::from(v);
    let n = p.display().to_string();

    if p.is_dir() {
        delete_folder_all(&n).unwrap();
    }

    if p.is_file() {
        delete_file_async(&n).await.unwrap();
    }
}

pub async fn run(ctx: &mut ctx::Ctx, command: &Option<Cmd>) -> anyhow::Result<()> {
    match command {
        Some(Cmd::Cls { dir, lock, ignore }) => {
            let cwd = match dir {
                Some(d) => {
                    let p = PathBuf::from(d);
                    match p.is_relative() {
                        true => ctx.cwd.join(p),
                        false => p,
                    }
                }
                None => ctx.cwd.clone(),
            };

            let gsf = create_gs("**/{pnpm,package,yarn}-lock.{json,yaml}")?;
            // should skip children
            let gsd = create_gs("**/node_modules")?;
            let gig: Option<_> =
                if ignore.is_some() { Some(create_gs(ignore.as_ref().unwrap())?) } else { None };

            // let walker = WalkDir::new(cwd.absolutize().unwrap());
            let mut v: Vec<String> = vec![];
            let loading = Loading::default();
            let start_time = Instant::now();
            let mut size: usize = 0;
            let mut skip: usize = 0;

            for entry in WalkDir::new(cwd.absolutize()?) {
                size += 1;
                let path = entry.unwrap().path();
                let name = path.display().to_string();

                // skip hidden
                if is_hidden(path.file_name().unwrap_or_default().to_str()) {
                    skip += 1;
                    continue;
                };

                if let Some(gb) = gig.as_ref() {
                    if gb.is_match(&path) {
                        skip += 1;
                        continue;
                    }
                }

                // skip sub node_modules // TODO: ops
                if v.iter().any(|base| name.starts_with(base)) {
                    continue;
                }

                if gsd.is_match(&path) {
                    v.push(name);
                    continue;
                }

                if *lock && gsf.is_match(&path) {
                    v.push(name);
                    continue;
                }
            }

            loading.end();

            let defaults: Vec<bool> = v.iter().enumerate().map(|_| false).collect();
            let prompt_str = format!(
                "In: {}, Total: {}, Find: {}, Skip: {}, Time: {}ms. Press {} give up {} select all",
                cwd.display(),
                size,
                v.len(),
                skip,
                start_time.elapsed().as_millis(),
                "q".green(),
                "a".green()
            );

            if !v.is_empty() {
                let mut tasks = JoinSet::new();

                let selects = MultiSelect::with_theme(&ColorfulTheme::default())
                    .report(false)
                    .with_prompt(prompt_str)
                    .items(&v[..])
                    .defaults(&defaults[..])
                    .interact_opt()?;

                if let Some(selects) = selects {
                    let start_time = Instant::now();
                    let loading = Loading::default();

                    for s in selects {
                        tasks.spawn(rm((&v[s]).clone()));
                    }

                    tasks.join_all().await;

                    loading.end();

                    Log::Suc(&format!("Done: {}ms", start_time.elapsed().as_millis())).println();

                    return Ok(());
                } else {
                    // println!("Bye Bye !!")
                }
            }

            Ok(())
        }
        Some(Cmd::W { dir }) => {
            let cwd = match dir {
                Some(d) => {
                    let p = PathBuf::from(d);
                    match p.is_relative() {
                        true => ctx.cwd.join(p),
                        false => p,
                    }
                }
                None => ctx.cwd.clone(),
            };

            let p_ws = cwd.join("pnpm-workspace.yaml");

            if !p_ws.exists() {
                fs::write(p_ws, DEF_WS)?
            }

            Ok(())
        }
        None => Err(anyhow::Error::msg("Command not found!")),
    }
}