updt 0.2.1

Cross-platform update helper for npm, cargo, rustup, fnm, scoop, Homebrew, paru, flatpak, pacman, and pkg.
use chrono::Local;
use crossterm::style::Color as TermColor;
use std::io;
use std::process;

mod checks;
mod cli;
mod command;
mod completion;
mod flow;
mod output;
mod parse;
mod profile;
mod selection;
mod state;
mod ui;
mod upgrade;

use crate::cli::{CliCommand, parse_cli};
use crate::completion::install_fish_completion;
use crate::flow::check::{
    any_check_failed, build_upgradable_targets, resolve_check_targets, run_checks, run_checks_plain,
};
use crate::flow::interactive::{
    InteractiveResult, offer_install_cargo_binstall, offer_install_cargo_update,
    run_interactive_flow,
};
use crate::output::{color_bold, ok_text, print_exit_signal_message, print_section, warn_text};
use crate::profile::{interactive_terminal, parse_profile};
use crate::selection::{resolve_cli_selection, select_targets, select_targets_prompt};
use crate::state::{AppState, profile_name};
use crate::upgrade::upgrade_selected;

#[tokio::main(flavor = "multi_thread")]
async fn main() {
    process::exit(run_app().await);
}

struct RunContext {
    state: AppState,
    requested_updates: Vec<String>,
    start_time: String,
    force_text_flow: bool,
}

async fn run_app() -> i32 {
    let cli = parse_cli();
    if let Some(code) = handle_fish_command(&cli).await {
        return code;
    }

    let mut ctx = RunContext::new(requested_updates(&cli)).await;
    if let Some(code) = try_interactive_start(&mut ctx).await {
        return code;
    }

    print_text_header_if_needed(&ctx);
    run_checks_for_context(&mut ctx).await;
    offer_install_cargo_update(&mut ctx.state).await;
    offer_install_cargo_binstall(&ctx.state).await;
    finish_after_checks(&mut ctx).await
}

impl RunContext {
    async fn new(requested_updates: Vec<String>) -> Self {
        let mut state = AppState::default();
        parse_profile(&mut state).await;
        let start_time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
        Self {
            state,
            requested_updates,
            start_time,
            force_text_flow: false,
        }
    }
}

async fn handle_fish_command(cli: &CliCommand) -> Option<i32> {
    if !matches!(cli, CliCommand::Fish) {
        return None;
    }
    Some(match install_fish_completion().await {
        Ok(path) => {
            println!("fish completion 已写入: {}", path.display());
            0
        }
        Err(err) => {
            eprintln!("[fish] 写入失败: {err}");
            1
        }
    })
}

fn requested_updates(cli: &CliCommand) -> Vec<String> {
    match cli {
        CliCommand::Update(v) => v.clone(),
        _ => Vec::new(),
    }
}

async fn try_interactive_start(ctx: &mut RunContext) -> Option<i32> {
    if interactive_terminal() {
        return handle_interactive_result(
            run_interactive_flow(&mut ctx.state, &ctx.requested_updates, &ctx.start_time).await,
            ctx,
        )
        .await;
    }
    None
}

async fn handle_interactive_result(
    result: io::Result<InteractiveResult>,
    ctx: &mut RunContext,
) -> Option<i32> {
    match result {
        Ok(InteractiveResult::Exit(code)) => Some(code),
        Ok(InteractiveResult::RunUpgrade(selected_targets)) => {
            Some(upgrade_exit_code(&ctx.state, &selected_targets).await)
        }
        Err(err) => {
            if err.kind() == io::ErrorKind::Interrupted {
                print_exit_signal_message();
                return Some(0);
            }
            eprintln!("[ui] TUI 运行失败, 自动回退文本流程: {err}");
            ctx.force_text_flow = true;
            None
        }
    }
}

async fn upgrade_exit_code(state: &AppState, selected_targets: &[String]) -> i32 {
    if upgrade_selected(state, selected_targets).await {
        0
    } else {
        1
    }
}

fn print_text_header_if_needed(ctx: &RunContext) {
    if !interactive_terminal() || ctx.force_text_flow {
        print_section("检查可升级项");
        println!(
            "{}: {}",
            color_bold("开始时间", TermColor::Blue),
            ctx.start_time
        );
        println!(
            "{}: {}",
            color_bold("系统策略", TermColor::Blue),
            profile_name(ctx.state.system_profile)
        );
    }
}

async fn run_checks_for_context(ctx: &mut RunContext) {
    if ctx.force_text_flow {
        let targets = resolve_check_targets(&ctx.state, &ctx.requested_updates);
        run_checks_plain(&mut ctx.state, &targets).await;
    } else {
        run_checks(&mut ctx.state, &ctx.requested_updates, &ctx.start_time).await;
    }
}

async fn finish_after_checks(ctx: &mut RunContext) -> i32 {
    let upgradable_targets = build_upgradable_targets(&ctx.state);

    if upgradable_targets.is_empty() {
        return no_upgrades_exit_code(&ctx.state);
    }

    if !interactive_terminal() {
        print_section("选择要升级的项目");
    }
    let selected_targets = select_after_checks(ctx, &upgradable_targets).await;
    selected_upgrade_exit_code(&ctx.state, &selected_targets).await
}

fn no_upgrades_exit_code(state: &AppState) -> i32 {
    print_section("汇总");
    println!("{}", ok_text("没有可升级项."));
    if any_check_failed(state) {
        println!("{}", warn_text("但有检查失败, 请根据上方日志排查."));
        1
    } else {
        0
    }
}

async fn select_after_checks(ctx: &RunContext, upgradable_targets: &[String]) -> Vec<String> {
    if !ctx.requested_updates.is_empty() {
        return resolve_cli_selection(&ctx.requested_updates, upgradable_targets);
    }
    if ctx.force_text_flow {
        select_targets_prompt(&ctx.state, upgradable_targets).await
    } else {
        select_targets(&ctx.state, upgradable_targets).await
    }
}

async fn selected_upgrade_exit_code(state: &AppState, selected_targets: &[String]) -> i32 {
    if selected_targets.is_empty() {
        println!("{}", warn_text("未选择任何升级项, 已退出."));
        return 0;
    }

    upgrade_exit_code(state, selected_targets).await
}