pnc 0.1.1

A fast, fully offline, terminal-only reverse Polish notation (RPN) calculator
mod cli;
mod clipboard_impl;
mod terminal;
mod tui;

use clap::{Parser, Subcommand};
use pnc_engine::persist;
use std::process::ExitCode;
use tracing::info;

#[derive(Parser)]
#[command(name = "pnc", version = env!("PNC_VERSION"), about = "Polish Notation Calculator")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    Eval {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        tokens: Vec<String>,
    },
}

fn main() -> ExitCode {
    #[cfg(feature = "logging")]
    {
        let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
        let subscriber = tracing_subscriber::fmt()
            .with_env_filter(env_filter)
            .with_writer(std::io::stderr)
            .finish();
        let _ = tracing::subscriber::set_global_default(subscriber);
    }

    terminal::install_panic_hook();

    info!(
        version = env!("CARGO_PKG_VERSION"),
        platform = std::env::consts::OS,
        arch = std::env::consts::ARCH,
        "pnc starting"
    );

    let args: Vec<String> = std::env::args().collect();
    let direct_tokens: Option<Vec<String>> =
        if args.len() > 1 && !args[1].starts_with('-') && args[1] != "eval" {
            Some(args[1..].to_vec())
        } else {
            None
        };

    if let Some(tokens) = direct_tokens {
        return cli::eval_tokens(tokens);
    }

    let cli = Cli::parse();
    match cli.command {
        Some(Commands::Eval { tokens }) if tokens.is_empty() => {
            return cli::eval_stdin();
        }
        Some(Commands::Eval { tokens }) => {
            return cli::eval_tokens(tokens);
        }
        None => {}
    }

    let _lock = match persist::acquire_lock() {
        Ok(f) => f,
        Err(e) => {
            eprintln!("{}", e);
            return ExitCode::from(2);
        }
    };
    let mut state = match persist::load() {
        Ok(s) => s,
        Err(pnc_engine::error::PncError::UnsupportedSchemaVersion) => {
            eprintln!("unsupported schema version; state file left untouched");
            return ExitCode::from(1);
        }
        Err(e) => {
            let path = persist::state_path();
            let ts = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_secs())
                .unwrap_or(0);
            let quarantined = path.with_file_name(format!("state.toml.corrupt.{}", ts));
            let _ = std::fs::rename(&path, &quarantined);
            tracing::warn!(error = %e, "state file corrupt; quarantined and starting empty");
            pnc_engine::persist::PersistedState::default()
        }
    };
    pnc_engine::ops::set_rational_digit_cap(state.settings.rational_digit_cap);
    crate::tui::set_stack_cap(state.settings.stack_soft_cap);
    let mut stack = state.stack;
    let stack_cap = state.settings.stack_soft_cap.max(1);
    let mut trimmed_baseline: Option<Vec<pnc_engine::Value>> = None;
    if stack.len() > stack_cap {
        let excess = stack.len() - stack_cap;
        stack.drain(0..excess);
        trimmed_baseline = Some(stack.clone());
        tracing::warn!(
            trimmed = excess,
            cap = stack_cap,
            "persisted stack exceeded cap; trimmed oldest entries"
        );
    }
    let undo_depth = state.settings.undo_depth;
    if let Err(e) = tui::run_tui(&mut stack, undo_depth) {
        eprintln!("{}", e);
        let _ = terminal::restore();
        return ExitCode::from(1);
    }
    let should_save = match &trimmed_baseline {
        Some(baseline) => stack != *baseline,
        None => true,
    };
    state.stack = stack;
    if should_save {
        if let Err(e) = persist::save(&state) {
            tracing::error!(error = %e, "failed to persist state");
            eprintln!("failed to save state: {}", e);
            return ExitCode::from(1);
        }
        tracing::info!(stack_depth = state.stack.len(), "state persisted");
    } else {
        tracing::info!("trimmed stack unchanged; no re-save");
    }
    info!("pnc exiting normally");
    crate::terminal::flush_log();
    ExitCode::SUCCESS
}