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 {
let log_dir = directories::BaseDirs::new()
.and_then(|b| b.state_dir().map(|p| p.join("pnc")))
.or_else(|| {
directories::ProjectDirs::from("", "", "pnc").map(|p| p.data_dir().to_path_buf())
})
.unwrap_or_else(|| std::path::PathBuf::from("."));
let _ = std::fs::create_dir_all(&log_dir);
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("pnc.log")
.max_log_files(7)
.build(&log_dir)
.expect("failed to init log appender");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
crate::terminal::set_log_guard(guard);
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(non_blocking)
.with_ansi(false)
.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
}