use clap::Parser;
use std::fmt::Display;
use std::io::{self, BufRead, BufWriter, IsTerminal, Write};
use std::mem;
use std::process::ExitCode;
mod doc;
mod shell;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug)]
enum ArgError {
NotATerminal,
}
impl Display for ArgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArgError::NotATerminal => write!(f, "--interactive requires terminal"),
}
}
}
#[derive(Debug)]
enum RunError {
Io(io::Error),
Tc(String, tc::Error),
}
impl From<io::Error> for RunError {
fn from(err: io::Error) -> Self {
RunError::Io(err)
}
}
#[derive(Parser, Debug)]
#[command(
name = "tc",
version = VERSION,
about = "tc - a simple Terminal Calculator",
after_long_help = doc::AFTER_HELP)]
struct Args {
#[arg(short = 'i', long = "interactive")]
interactive: bool,
#[arg(short = 's', long = "strip")]
strip: bool,
#[arg(short = 'f', long = "functions")]
functions: bool,
#[arg(short = 'g', long = "grammar")]
grammar: bool,
evals: Vec<String>,
}
impl Args {
fn run_shell(&self) -> Result<bool, ArgError> {
let is_terminal =
io::stdin().is_terminal() && io::stdout().is_terminal() && io::stderr().is_terminal();
if self.interactive && !is_terminal {
return Err(ArgError::NotATerminal);
}
Ok((self.evals.is_empty() && is_terminal) || self.interactive)
}
}
fn main() -> ExitCode {
let args = Args::parse();
if args.functions {
println!("TC FUNCTIONS");
println!("============");
if let Err(err) = print_functions() {
eprintln!("IO Error: {}", err);
return ExitCode::FAILURE;
}
return ExitCode::SUCCESS;
}
if args.grammar {
println!("TC GRAMMAR");
println!("==========");
println!();
println!("{}", doc::GRAMMAR);
return ExitCode::SUCCESS;
}
if args.strip && args.interactive {
eprintln!("--strip cannot be used with --interactive");
return ExitCode::FAILURE;
}
let driver = match Driver::new(args) {
Ok(driver) => driver,
Err(err) => {
eprintln!("Error: {}", err);
return ExitCode::FAILURE;
}
};
match driver.run() {
Ok(()) => ExitCode::SUCCESS,
Err(RunError::Io(err)) => {
eprintln!("IO Error: {}", err);
ExitCode::FAILURE
}
Err(RunError::Tc(line, err)) => {
eprintln!("{line}");
let _ = shell::print_diagnostic(&err, 0);
ExitCode::FAILURE
}
}
}
struct Driver {
tc: tc::TermCalc,
interactive: bool,
strip: bool,
arg_evals: Vec<String>,
}
impl Driver {
fn new(args: Args) -> Result<Driver, ArgError> {
let interactive = args.run_shell()?;
Ok(Driver {
tc: tc::TermCalc::new(),
interactive,
strip: args.strip,
arg_evals: args.evals,
})
}
fn run(mut self) -> Result<(), RunError> {
let arg_evals = mem::take(&mut self.arg_evals);
let arg_evals = arg_evals.into_iter().map(Result::Ok);
if io::stdin().is_terminal() {
self.process_non_interactive(arg_evals)?;
} else {
let all_lines = arg_evals.chain(io::stdin().lock().lines());
self.process_non_interactive(all_lines)?;
}
if self.interactive {
let mut sh = shell::Shell::new(self.tc);
sh.main_loop()?;
}
Ok(())
}
fn process_non_interactive<I>(&mut self, lines: I) -> Result<(), RunError>
where
I: Iterator<Item = io::Result<String>>,
{
for line in lines {
let line = line?;
let eval = match self.tc.eval_line(&line) {
Ok(eval) => eval,
Err(err) => {
return Err(RunError::Tc(line, err));
}
};
if self.strip {
println!("{}", eval.val);
} else if eval.sym == "ans" {
println!("{} = {}", line, eval.val);
} else {
println!("{} = {}", eval.sym, eval.val);
}
io::stdout().flush()?;
}
Ok(())
}
}
fn print_functions() -> io::Result<()> {
let stdout = io::stdout().lock();
let style = stdout.is_terminal();
let mut out = BufWriter::new(stdout);
doc::write_functions(&mut out, style)?;
out.flush()
}