use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use chrono::{Local, NaiveDate};
use pactole_core::{AccountName, CommodityName};
use crate::config::default_import_config_path;
pub(crate) struct Options {
pub(crate) ledger: PathBuf,
pub(crate) config: Option<PathBuf>,
pub(crate) account: String,
pub(crate) currency: String,
pub(crate) counter_account: String,
pub(crate) date: Option<NaiveDate>,
pub(crate) stdout: bool,
pub(crate) no_config: bool,
pub(crate) no_ledger: bool,
}
pub(crate) fn run(options: Options) -> ExitCode {
let Options {
ledger,
config,
account,
currency,
counter_account,
date,
stdout,
no_config,
no_ledger,
} = options;
if no_config && no_ledger {
eprintln!("error: --no-config and --no-ledger cannot be used together");
return ExitCode::FAILURE;
}
if let Err(err) = AccountName::new(account.clone()) {
return failure(format!("invalid bank account `{account}`: {err}"));
}
if let Err(err) = AccountName::new(counter_account.clone()) {
return failure(format!(
"invalid counter account `{counter_account}`: {err}"
));
}
if let Err(err) = CommodityName::new(currency.clone()) {
return failure(format!("invalid currency `{currency}`: {err}"));
}
let date = date.unwrap_or_else(|| Local::now().date_naive());
let ledger_source = render_ledger(&date, &account, ¤cy, &counter_account);
let config_source = render_config(&account, ¤cy, &counter_account);
if !no_ledger && stdout {
print!("{ledger_source}");
}
if !no_ledger && !stdout {
match create_file_if_missing(&ledger, &ledger_source) {
Ok(true) => println!("created ledger: {}", ledger.display()),
Ok(false) => println!("ledger already exists: {}", ledger.display()),
Err(err) => return failure(format!("cannot initialize {}: {err}", ledger.display())),
}
}
if !no_config {
let config_path = match config.or_else(default_import_config_path) {
Some(path) => path,
None => {
return failure(
"cannot determine the Pactole configuration directory (set XDG_CONFIG_HOME or HOME, or pass --config)",
);
}
};
if let Some(parent) = config_path.parent()
&& let Err(err) = fs::create_dir_all(parent)
{
return failure(format!("cannot create {}: {err}", parent.display()));
}
match create_file_if_missing(&config_path, &config_source) {
Ok(true) => println!("created configuration: {}", config_path.display()),
Ok(false) => println!("configuration already exists: {}", config_path.display()),
Err(err) => {
return failure(format!(
"cannot initialize {}: {err}",
config_path.display()
));
}
}
}
ExitCode::SUCCESS
}
fn render_ledger(date: &NaiveDate, account: &str, currency: &str, counter_account: &str) -> String {
format!(
"; Pactole ledger\n\
; Generated by `pactole init`.\n\
; Add transactions below and run `pactole check ledger.pactole`.\n\n\
{date} commodity {currency}\n\
{date} open {account}\n\
{date} open {counter_account}\n"
)
}
fn render_config(account: &str, currency: &str, counter_account: &str) -> String {
format!(
"# Pactole OFX import configuration\n\
# Generated by `pactole init`.\n\n\
bank_account = \"{account}\"\n\
currency = \"{currency}\"\n\
default_counter_account = \"{counter_account}\"\n\n\
# Add categorization rules below.\n\
#\n\
# [[rules]]\n\
# payee_contains = \"supermarket\"\n\
# counter_account = \"Expenses:Groceries\"\n\
# payee = \"Supermarket\"\n"
)
}
fn create_file_if_missing(path: &Path, contents: &str) -> io::Result<bool> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let mut file = match OpenOptions::new().write(true).create_new(true).open(path) {
Ok(file) => file,
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => return Ok(false),
Err(err) => return Err(err),
};
file.write_all(contents.as_bytes())?;
file.flush()?;
Ok(true)
}
fn failure(error: impl std::fmt::Display) -> ExitCode {
eprintln!("error: {error}");
ExitCode::FAILURE
}