use std::collections::HashSet; use itertools::Itertools; // use crate::{check::CheckLevel, data, import::import_from, io, show}; use crate::prelude::*; mod comb; mod infer; mod txnmatch; #[derive(clap::Parser)] pub struct Invocation { /// path to treasure file #[clap(long, short, env = "HOARD_ROOT")] pub file: std::path::PathBuf, /// verbosity of output messages #[clap(long, short, action = clap::ArgAction::Count)] pub verbose: u8, #[clap(subcommand)] pub cmd: Command, } fn load_data( fsdata: &mut io::FilesystemData, invocation: &Invocation, level: check::CheckLevel, ) -> anyhow::Result { let path = std::fs::canonicalize(&invocation.file)?; match data::Hoard::load(fsdata, &path, level) { Ok(data) => Ok(data), Err(DataError::IOError(ioerror)) => Err(ioerror.into()), Err(DataError::Report(report)) => { report.eprint(fsdata)?; Err(anyhow::anyhow!("Error reported")) } } } #[derive(clap::Subcommand)] pub enum Command { Summarize, Ledger { account: String, #[clap(long, short)] raw: bool }, Reformat { #[clap(long)] /// do not add ID annotations to transactions lacking them skip_ids: bool, }, Import { account: String, from: std::path::PathBuf, target: Option, }, Infer, Match { account: Option, }, Comb { account: Option, }, } fn summarize(data: &data::Hoard) { let positive = console::Style::new().green(); let negative = console::Style::new().red(); let neutral = console::Style::new(); let Some(maxlen) = data.account_names().map(|v| v.len()).max() else { return; }; for shortname in data.account_names().sorted_by_key(|an| an.as_str()) { if shortname.as_ref() == "initial" { continue; } if let Some(mut balances) = data.balance(shortname) { // insert a default balance of 0 if no balance is present if let Some(unit) = data.account_spec(shortname).unwrap().unit { balances.entry(unit).or_default(); } let balances = balances .into_iter() .filter(|(_, b)| !b.is_zero()) .sorted() .map(|(u, b)| { ( u, if b.is_sign_positive() { positive.apply_to(b).to_string() } else if b.is_sign_negative() { negative.apply_to(b).to_string() } else { neutral.apply_to("0").to_string() }, ) }); println!( "{shortname:maxlen$} {}", balances.map(|(u, b)| format!("{b:8} {u:5}")).join(" ") ); } } } impl Command { pub fn run(&self, inv: &Invocation) -> anyhow::Result<()> { let mut fsdata = io::FilesystemData::default(); match self { Self::Summarize => { let data = load_data(&mut fsdata, inv, Default::default())?; summarize(&data); } Self::Ledger { account, raw } => { let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?; let aname = data::AccountName::new(account.as_str()); if let Some(ld) = data.ledger_data_for(aname) { if *raw { for txn in ld { show::show_transaction(Some(&data), &txn.borrow()); } } else { let tt = show::TransactionTable::default(); tt.show_refs(Some(&data), aname, ld.iter()); } } else { log::error!("account not found!"); } } Self::Reformat { skip_ids } => { let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?; if !skip_ids { let mut used_ids = HashSet::new(); for le in data.all_raw_ledger_data() { let Some(txn) = le.as_transaction() else { continue; }; let Some(id) = txn.get_annotation("id") else { continue; }; used_ids.insert(id.to_owned()); } // assign IDs if to transactions lacking them for le in data.all_raw_ledger_data_mut() { let Some(txn) = le.as_transaction_mut() else { continue; }; if txn.get_annotation("id").is_none() { // generated unique ID let mut id = nanoid::nanoid!(10); while used_ids.contains(&id) { id = nanoid::nanoid!(10); } // assign ID txn.annotations.insert("id".to_string(), id); } } } data::format_ledger(&mut fsdata, &data, data.all_raw_ledger_data().iter())?; } Self::Import { account, from, target, } => { let data = load_data(&mut fsdata, inv, Default::default())?; let aname = account.into(); let Some(aspec) = data.account_spec(aname) else { log::error!("Account {aname} does not exist!"); return Ok(()); }; let default_destination = data.spec_root().placeholder_account; let imported = import::import_from(aspec, aname, default_destination, from.as_path()).unwrap(); log::info!("Imported {} transaction(s)", imported.len()); if let Some(target) = target { let new_source = std::fs::canonicalize(target.as_os_str())? .into_os_string() .into(); let source_span = io::Span::null_for_file(new_source); let new_data = imported .into_iter() .map(|mut txn| { txn.source = Some(source_span); data::RawLedgerEntry::Transaction(txn) }) .collect_vec(); data::format_ledger( &mut fsdata, &data, data.raw_ledger_data_from(new_source).chain(new_data.iter()), )?; } else { log::info!("No target specified, showing new data on stdout."); let tt = show::TransactionTable::default(); tt.show(Some(&data), aname, imported.iter()); } } Self::Infer => { let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?; if infer::do_inference(&mut data)? { data::format_entire_ledger(&mut fsdata, &data)?; } } Self::Match { account } => { let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?; let account = account.unwrap_or(data.spec_root().placeholder_account); if txnmatch::do_match(&mut data, account)? { data::format_entire_ledger(&mut fsdata, &data)?; } } Self::Comb { account } => { let data = load_data(&mut fsdata, inv, check::CheckLevel::Consistent)?; let account = account.unwrap_or(data.spec_root().placeholder_account); if comb::do_comb(&data, account)? { data::format_entire_ledger(&mut fsdata, &data)?; } } } Ok(()) } }