haj 0.2.0

fast, quiet, beautiful package managemer and tui for Arch Linux
mod cli;
mod config;
mod core;
mod network;
mod tui;
mod ui;

use clap::Parser;
use cli::{Cli, Commands};
use owo_colors::OwoColorize;
use std::io::{self, Write};
use tokio::io::{AsyncBufReadExt, BufReader};

async fn run_pacman(args: &[&str], spinner_msg: &str, success_msg: &str, is_dry_run: bool) {
    if is_dry_run {
        println!(
            "{} no system changes will be made.",
            "[dry run]".bold().yellow()
        );
        let arrow = "".cyan();
        let cmd = args.join(" ");
        println!("{arrow} would execute: pacman {cmd}");
        return;
    }

    let spinner = ui::progress::spinner(spinner_msg);
    let mut child = tokio::process::Command::new("pacman")
        .args(args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("failed to spawn pacman");

    let stdout = child.stdout.take().unwrap();
    let stderr = child.stderr.take().unwrap();

    let err_handle = tokio::spawn(async move {
        let mut reader = BufReader::new(stderr).lines();
        let mut err_str = String::new();
        while let Ok(Some(line)) = reader.next_line().await {
            err_str.push_str(&line);
            err_str.push('\n');
        }
        err_str
    });

    let mut reader = BufReader::new(stdout).lines();

    let status = loop {
        tokio::select! {
            Ok(Some(line)) = reader.next_line() => {
                let clean = line.trim();
                if clean.is_empty() { continue; }

                if clean.starts_with("::") {
                    spinner.set_message(format!("{}", clean.replace("::", "").cyan().bold()));
                } else if clean.starts_with('(') || clean.contains("downloading") || clean.contains("installing") || clean.contains("removing") || clean.contains("upgrading") || clean.contains("cleaning") {
                    spinner.set_message(format!("  {}", clean.dimmed()));
                }
            }
            result = child.wait() => {
                break result;
            }
        }
    };

    let err_output = err_handle.await.unwrap_or_default();

    match status {
        Ok(stat) if stat.success() => {
            spinner.finish_with_message(format!("{} {}", "".green(), success_msg));
        }
        Ok(stat) => {
            spinner.finish_with_message(format!(
                "{} operation failed (code {}):\n{}",
                "".red(),
                stat.code().unwrap_or(1),
                err_output.trim().red()
            ));
        }
        Err(e) => {
            spinner.finish_with_message(format!("{} failed to execute pacman: {}", "".red(), e))
        }
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    let _config = config::load_config();

    match &cli.command {
        Commands::Tui => {
            tui::run().await?;
        }
        Commands::Update => {
            run_pacman(
                &["-Sy", "--noconfirm"],
                "syncing package databases from mirrors...",
                "repositories synced successfully.",
                cli.dry_run,
            )
            .await;
        }
        cmd => {
            let alpm_handle = core::alpm_init::init_alpm()?;
            let local_db = alpm_handle.localdb();

            match cmd {
                Commands::Tui => unreachable!(),
                Commands::Update => unreachable!(),
                Commands::Install { packages } => {
                    println!("{} resolving dependencies...\n", "".green());

                    match core::resolver::get_install_summaries(&alpm_handle, packages) {
                        Ok(summaries) => {
                            let mut total_dl = 0.0;
                            let mut total_inst = 0.0;

                            println!("{}", "installing".bold().white());
                            for sum in &summaries {
                                println!("  {} {}", sum.name.cyan().bold(), sum.version.dimmed());
                                total_dl += sum.download_size_mb;
                                total_inst += sum.install_size_mb;
                            }

                            println!("\n{:<15} {:.2} MB", "download:", total_dl);
                            println!("{:<15} {:.2} MB", "disk usage:", total_inst);

                            print!("\ncontinue? [y/n] ");
                            io::stdout().flush()?;

                            let mut input = String::new();
                            io::stdin().read_line(&mut input)?;

                            if input.trim().eq_ignore_ascii_case("y") || input.trim().is_empty() {
                                let mut args = vec!["-S", "--noconfirm"];
                                args.extend(packages.iter().map(|s| s.as_str()));

                                drop(alpm_handle);

                                run_pacman(
                                    &args,
                                    "initializing transaction...",
                                    "packages installed successfully.",
                                    cli.dry_run,
                                )
                                .await;
                            } else {
                                println!("{} aborted.", "".red());
                            }
                        }
                        Err(e) => println!("{} {}", "".red(), e),
                    }
                }

                Commands::Remove { packages } => {
                    println!("{} resolving targets...\n", "".green());

                    let mut found = true;
                    for pkg in packages {
                        if local_db.pkg(pkg.as_str()).is_err() {
                            println!("{} package '{}' is not installed.", "".red(), pkg.bold());
                            found = false;
                        }
                    }

                    if !found {
                        println!("{} aborted.", "".red());
                        return Ok(());
                    }

                    println!("{}", "tossing".bold().white());
                    for pkg in packages {
                        println!("  {}", pkg.magenta().bold());
                    }

                    print!("\ncontinue? [y/n] ");
                    io::stdout().flush()?;
                    let mut input = String::new();
                    io::stdin().read_line(&mut input)?;

                    if input.trim().eq_ignore_ascii_case("y") {
                        let mut args = vec!["-Rs", "--noconfirm"];
                        args.extend(packages.iter().map(|s| s.as_str()));

                        drop(alpm_handle);

                        run_pacman(
                            &args,
                            "tossing packages back into the ocean...",
                            "packages removed successfully.",
                            cli.dry_run,
                        )
                        .await;
                    } else {
                        println!("{} aborted.", "".red());
                    }
                }

                Commands::Clean => {
                    drop(alpm_handle);

                    run_pacman(
                        &["-Sc", "--noconfirm"],
                        "scrubbing the package cache...",
                        "cache cleared successfully.",
                        cli.dry_run,
                    )
                    .await;
                }

                Commands::Search { query } => {
                    println!("{} searching for '{}'...\n", "".green(), query.cyan());
                    let mut found = false;
                    for db in alpm_handle.syncdbs() {
                        if let Ok(results) = db.search([query.as_str()].into_iter()) {
                            for pkg in results {
                                found = true;
                                let is_installed = local_db.pkg(pkg.name()).is_ok();
                                ui::formatter::print_search_result(pkg, db.name(), is_installed);
                            }
                        }
                    }
                    if !found {
                        println!(
                            "{} no packages found matching '{}'.",
                            "".red(),
                            query.bold()
                        );
                    }
                }

                Commands::Show { package } => match local_db.pkg(package.as_str()) {
                    Ok(pkg) => {
                        println!("found locally: {} v{}", pkg.name(), pkg.version());
                        println!("description: {}", pkg.desc().unwrap_or("none"));
                    }
                    Err(_) => println!(
                        "{} package '{}' not found in local database.",
                        "".red(),
                        package
                    ),
                },

                Commands::Orphan => {
                    println!("{} scanning local database for orphans...\n", "".green());
                    let mut orphans = Vec::new();

                    for pkg in local_db.pkgs() {
                        if pkg.reason() == alpm::PackageReason::Depend
                            && pkg.required_by().is_empty()
                            && pkg.optional_for().is_empty()
                        {
                            orphans.push(pkg);
                        }
                    }

                    if orphans.is_empty() {
                        println!(
                            "{} no orphaned packages found. system is clean!",
                            "".green()
                        );
                    } else {
                        println!("{}", "orphans found:".bold().white());
                        let mut total_size = 0.0;
                        for pkg in &orphans {
                            println!("  {:<25} {}", pkg.name().magenta(), pkg.version().dimmed());
                            total_size += pkg.isize() as f64 / 1024.0 / 1024.0;
                        }
                        println!("\n{:<15} {:.2} MB", "wasted space:", total_size);
                        println!("\nrun {} to remove them.", "haj toss <packages>".cyan());
                    }
                }
            }
        }
    }
    Ok(())
}