fend 1.5.8

Arbitrary-precision unit-aware calculator
#![deny(unsafe_code)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![deny(elided_lifetimes_in_paths)]

use std::fmt::Write;
use std::{error, io, process};

mod args;
mod color;
mod config;
mod context;
mod custom_units;
mod exchange_rates;
mod file_paths;
mod helper;
mod interrupt;
mod terminal;

use args::Action as ArgsAction;
use context::Context;
use process::ExitCode;
use tokio::sync::RwLock;

type Error = Box<dyn error::Error + Send + Sync + 'static>;

enum EvalResult {
	Ok,
	Err,
	NoInput,
}

fn print_spans(spans: Vec<fend_core::SpanRef<'_>>, config: &config::Config) -> String {
	let mut result = String::new();
	for span in spans {
		let style = config.colors.get_color(span.kind());
		write!(result, "{style}{}\x1b[0m", span.string()).unwrap();
	}
	result
}

async fn eval_and_print_res(
	line: &str,
	context: &mut Context<'_>,
	print_res: bool,
	int: &impl fend_core::Interrupt,
	config: &config::Config,
) -> EvalResult {
	match context.eval(line, print_res, int).await {
		Ok(res) => {
			let result: Vec<_> = res.get_main_result_spans().collect();
			if result.is_empty() || res.output_is_empty() {
				return EvalResult::NoInput;
			}
			let string_result = if config.enable_colors {
				print_spans(result, config)
			} else {
				res.get_main_result().to_string()
			};
			print!("{string_result}");
			if res.has_trailing_newline() && !res.get_main_result().ends_with('\n') {
				println!();
			}
			EvalResult::Ok
		}
		Err(msg) => {
			eprintln!("Error: {msg}");
			EvalResult::Err
		}
	}
}

fn print_help(explain_quitting: bool) {
	println!("For more information on how to use fend, please take a look at the manual:");
	println!("https://printfn.github.io/fend/documentation/");
	println!();
	println!("Version: {}", fend_core::get_version());
	if let Ok(config_path) = file_paths::get_config_file_location() {
		println!("Config file: {}", config_path.to_string_lossy());
	} else {
		println!("Failed to get config file location");
	}
	if let Ok(history_path) = file_paths::get_history_file_location(file_paths::DirMode::DontCreate)
	{
		println!("History file: {}", history_path.to_string_lossy());
	} else {
		println!("Failed to get history file location");
	}
	if let Ok(cache_path) = file_paths::get_cache_dir(file_paths::DirMode::DontCreate) {
		println!("Cache directory: {}", cache_path.to_string_lossy());
	} else {
		println!("Failed to get cache directory location");
	}
	if explain_quitting {
		println!("\nTo quit, type `quit`.");
	}
}

async fn repl_loop(config: &config::Config) -> ExitCode {
	let core_context = RwLock::new(context::InnerCtx::new(config));
	let mut context = Context::new(&core_context);
	let mut prompt_state = match terminal::init_prompt(config, &context) {
		Ok(prompt_state) => prompt_state,
		Err(err) => {
			println!("Error: {err}");
			return ExitCode::FAILURE;
		}
	};
	let mut initial_run = true; // set to false after first successful command
	let mut last_command_success = true;
	let interrupt = interrupt::register_handler();
	loop {
		match prompt_state.read_line() {
			Ok(line) => match line.as_str() {
				"exit" | "exit()" | ".exit" | ":exit" | "quit" | "quit()" | ":quit" | ":q"
				| ":wq" | ":q!" | ":wq!" | ":qa" | ":wqa" | ":qa!" | ":wqa!" => break,
				"help" | "?" => {
					print_help(true);
				}
				"!serialize" => match context.serialize().await {
					Ok(res) => {
						for b in res {
							print!("{b:02x}");
						}
						println!();
					}
					Err(e) => eprintln!("{e}"),
				},
				"clear" | "clear()" | ":clear" => {
					print!("\x1b[2J\x1b[H");
				}
				line => {
					interrupt.reset();
					match eval_and_print_res(line, &mut context, true, &interrupt, config).await {
						EvalResult::Ok => {
							last_command_success = true;
							initial_run = false;
						}
						EvalResult::NoInput => {
							last_command_success = true;
						}
						EvalResult::Err => {
							last_command_success = false;
						}
					}
				}
			},
			Err(terminal::ReadLineError::Interrupted) => {
				match (initial_run, context.get_input_typed().await) {
					(_, true) => {
						// input has been typed => do nothing
					}
					(true, false) => {
						// initial run, no input => terminate
						break;
					}
					(false, false) => {
						// later run, no input => show message
						println!("Use Ctrl-D (i.e. EOF) to exit");
					}
				}
			}
			Err(terminal::ReadLineError::Eof) => break,
			Err(terminal::ReadLineError::Error(err)) => {
				println!("Error: {err}");
				break;
			}
		}
	}
	if last_command_success {
		ExitCode::SUCCESS
	} else {
		ExitCode::FAILURE
	}
}

async fn eval_exprs(exprs: &[String]) -> ExitCode {
	let config = config::read();
	let core_context = RwLock::new(context::InnerCtx::new(&config));
	for (i, expr) in exprs.iter().enumerate() {
		let print_res = i == exprs.len() - 1;
		match eval_and_print_res(
			expr.as_str(),
			&mut Context::new(&core_context),
			print_res,
			&interrupt::Never::default(),
			&config,
		)
		.await
		{
			EvalResult::Ok | EvalResult::NoInput => (),
			EvalResult::Err => return ExitCode::FAILURE,
		}
	}
	ExitCode::SUCCESS
}

async fn real_main() -> ExitCode {
	// Assemble the action from all but the first argument.
	let action = match ArgsAction::get() {
		Ok(action) => action,
		Err(e) => {
			eprintln!("Error: {e}");
			return ExitCode::FAILURE;
		}
	};
	match action {
		ArgsAction::Help => {
			print_help(false);
		}
		ArgsAction::Version => {
			println!("{}", fend_core::get_version());
		}
		ArgsAction::DefaultConfig => {
			println!("{}", config::DEFAULT_CONFIG_FILE);
		}
		ArgsAction::Eval { exprs } => {
			return eval_exprs(&exprs).await;
		}
		ArgsAction::Repl => {
			if terminal::is_terminal_stdin() {
				let config = config::read();
				return repl_loop(&config).await;
			}
			let mut input = String::new();
			match io::Read::read_to_string(&mut io::stdin(), &mut input) {
				Ok(_) => (),
				Err(e) => {
					eprintln!("Error: {e}");
					return ExitCode::FAILURE;
				}
			}
			return eval_exprs(&[input]).await;
		}
	}
	ExitCode::SUCCESS
}

#[tokio::main]
async fn main() -> process::ExitCode {
	real_main().await
}