pnc 0.2.1

A fast, fully offline, terminal-only reverse Polish notation (RPN) calculator
mod cli;
mod clipboard_impl;
mod display;
mod plot;
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();

    // Shared argv preprocessing pipeline (json-output design).
    struct PreprocessedArgs {
        json: bool,
        angle_present: bool,
        argv: Vec<String>,
    }
    let mut pre = PreprocessedArgs {
        json: false,
        angle_present: false,
        argv: vec![args[0].clone()],
    };
    let mut stop_flags = false;
    for tok in &args[1..] {
        if stop_flags {
            pre.argv.push(tok.clone());
            continue;
        }
        if tok == "--" {
            stop_flags = true;
            pre.argv.push(tok.clone());
            continue;
        }
        if tok == "--json" {
            pre.json = true;
            continue;
        }
        if tok == "--angle" {
            pre.angle_present = true;
            pre.argv.push(tok.clone());
            continue;
        }
        pre.argv.push(tok.clone());
    }
    let json = pre.json;
    let filtered_args = pre.argv;

    let direct_tokens: Option<Vec<String>> =
        if filtered_args.len() > 1 && filtered_args[1] != "eval" {
            let first = &filtered_args[1];
            if first == "--" {
                Some(filtered_args[2..].to_vec())
            } else if !first.starts_with('-') || pnc_engine::parse::is_number_token(first) {
                Some(filtered_args[1..].to_vec())
            } else {
                None
            }
        } else {
            None
        };

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

    let cli = Cli::parse_from(&filtered_args);
    match cli.command {
        Some(Commands::Eval { tokens }) if tokens.is_empty() => {
            return cli::eval_stdin(json);
        }
        Some(Commands::Eval { tokens }) => {
            return cli::eval_tokens(tokens, json);
        }
        None => {
            if json {
                return cli::eval_stdin(true);
            }
        }
    }

    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 history_depth = state.settings.history_depth;
    let history_capacity = history_depth + 1;
    let original_settings = state.settings.clone();
    let state_file_existed = true; // simplified; full impl tracks this
    if let Err(e) = tui::run_tui(&mut stack, &mut state.settings, history_capacity) {
        eprintln!("{}", e);
        let _ = terminal::restore();
        return ExitCode::from(1);
    }
    let stack_changed = match &trimmed_baseline {
        Some(baseline) => stack != *baseline,
        None => true,
    };
    let settings_changed = state.settings != original_settings;
    let should_save = state.migrated || !state_file_existed || stack_changed || settings_changed;
    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
}