From ca12c28f744c34b27c57dd832ac5f713eff61ab4 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Mon, 12 Aug 2019 00:09:21 -0400 Subject: start building out a cursive ui --- src/checks.rs | 32 +++++++++++++++++++++++++ src/display.rs | 14 +++++++++++ src/main.rs | 68 +++++++++++++++++++++++------------------------------- src/table.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/ynab.rs | 2 ++ src/ynab/budget.rs | 40 ++++++++++++++++++++------------ src/ynab/util.rs | 2 +- 7 files changed, 167 insertions(+), 55 deletions(-) create mode 100644 src/checks.rs create mode 100644 src/display.rs create mode 100644 src/table.rs (limited to 'src') diff --git a/src/checks.rs b/src/checks.rs new file mode 100644 index 0000000..7882fae --- /dev/null +++ b/src/checks.rs @@ -0,0 +1,32 @@ +pub fn run_checks(budget: &super::ynab::Budget) { + check_reconciled(budget); + check_has_inflows(budget); +} + +fn check_reconciled(budget: &super::ynab::Budget) { + let reconciled_amount: i64 = budget + .reimbursables() + .iter() + .filter(|t| t.reimbursed) + .map(|t| t.amount) + .sum(); + if reconciled_amount != 0 { + eprintln!( + "reconciled reimbursables don't sum to $0.00: ${}", + super::ynab::format_amount(reconciled_amount) + ); + std::process::exit(1); + } +} + +fn check_has_inflows(budget: &super::ynab::Budget) { + let txns = budget + .reimbursables() + .iter() + .filter(|t| !t.reimbursed && t.amount > 0) + .count(); + if txns == 0 { + eprintln!("no transactions to reconcile"); + std::process::exit(1); + } +} diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..5380c5f --- /dev/null +++ b/src/display.rs @@ -0,0 +1,14 @@ +pub fn theme() -> cursive::theme::Theme { + let mut palette = cursive::theme::Palette::default(); + palette[cursive::theme::PaletteColor::Background] = + cursive::theme::Color::TerminalDefault; + palette[cursive::theme::PaletteColor::View] = + cursive::theme::Color::TerminalDefault; + palette[cursive::theme::PaletteColor::Primary] = + cursive::theme::Color::TerminalDefault; + cursive::theme::Theme { + shadow: false, + borders: cursive::theme::BorderStyle::None, + palette, + } +} diff --git a/src/main.rs b/src/main.rs index 0bba75a..7d94fe9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,39 @@ +mod checks; +mod display; +mod table; mod ynab; fn main() { let key = std::env::args().nth(1).unwrap(); let client = ynab::Client::new(&key); let budget = client.default_budget(); - let reimbursables = budget.reimbursables(); - println!("using budget {} ({})", budget.name(), budget.id()); - let reconciled_amount: i64 = reimbursables - .iter() - .filter(|t| t.reimbursed) - .map(|t| t.amount) - .sum(); - if reconciled_amount != 0 { - eprintln!( - "reconciled reimbursables don't sum to $0.00: ${}", - ynab::format_amount(reconciled_amount) - ); - std::process::exit(1); - } - println!("reconciled reimbursables correctly sum to $0.00"); + checks::run_checks(&budget); - println!("ready for reconciliation:"); - for t in reimbursables - .iter() - .filter(|t| !t.reimbursed && t.amount > 0) - { - println!( - "{} | {} | {}", - t.date, - t.payee, - ynab::format_amount(t.amount) - ) - } + let mut app = cursive::Cursive::default(); + app.set_theme(display::theme()); + app.add_global_callback('q', |s| s.quit()); - println!("match against reconcilable:"); - for t in reimbursables - .iter() - .filter(|t| !t.reimbursed && t.amount <= 0) - { - println!( - "{} | {} | {}", - t.date, - t.payee, - ynab::format_amount(t.amount) - ) - } + let mut layout = cursive::views::LinearLayout::vertical(); + layout.add_child(cursive::views::TextView::new(format!( + "Budget: {} ({})", + budget.name(), + budget.id() + ))); + + let inflows_table = table::inflows_table(&budget); + layout.add_child(cursive::views::CircularFocus::wrap_arrows( + cursive::views::BoxView::with_min_height( + std::cmp::min(std::cmp::max(inflows_table.len(), 1), 5) + 2, + cursive::views::BoxView::with_full_width(inflows_table), + ), + )); + + let outflows_table = table::outflows_table(&budget); + layout.add_child(cursive::views::CircularFocus::wrap_arrows( + cursive::views::BoxView::with_full_screen(outflows_table), + )); + + app.add_fullscreen_layer(layout); + app.run(); } diff --git a/src/table.rs b/src/table.rs new file mode 100644 index 0000000..531d1e8 --- /dev/null +++ b/src/table.rs @@ -0,0 +1,64 @@ +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub enum TxnColumn { + Date, + Payee, + Amount, +} + +type TableView = + cursive_table_view::TableView; + +impl cursive_table_view::TableViewItem + for super::ynab::Transaction +{ + fn to_column(&self, column: TxnColumn) -> String { + match column { + TxnColumn::Date => self.date.clone(), + TxnColumn::Payee => self.payee.clone(), + TxnColumn::Amount => super::ynab::format_amount(self.amount), + } + } + + fn cmp(&self, other: &Self, column: TxnColumn) -> std::cmp::Ordering + where + Self: Sized, + { + match column { + TxnColumn::Date => self.date.cmp(&other.date), + TxnColumn::Payee => self.payee.cmp(&other.payee), + TxnColumn::Amount => self.amount.cmp(&other.amount), + } + } +} + +pub fn inflows_table(budget: &super::ynab::Budget) -> TableView { + let inflows = budget + .reimbursables() + .iter() + .filter(|t| !t.reimbursed && t.amount > 0) + .cloned() + .collect(); + txn_table(inflows) +} + +pub fn outflows_table(budget: &super::ynab::Budget) -> TableView { + let outflows = budget + .reimbursables() + .iter() + .filter(|t| !t.reimbursed && t.amount <= 0) + .cloned() + .collect(); + txn_table(outflows) +} + +fn txn_table(txns: Vec) -> TableView { + let mut table = cursive_table_view::TableView::new() + .column(TxnColumn::Date, "Date", |c| c.width(10)) + .column(TxnColumn::Payee, "Payee", |c| c) + .column(TxnColumn::Amount, "Amount", |c| { + c.align(cursive::align::HAlign::Right).width(10) + }) + .default_column(TxnColumn::Date); + table.set_items(txns); + table +} diff --git a/src/ynab.rs b/src/ynab.rs index da80da0..2f0db5a 100644 --- a/src/ynab.rs +++ b/src/ynab.rs @@ -3,5 +3,7 @@ mod client; mod transaction; mod util; +pub use budget::Budget; pub use client::Client; +pub use transaction::Transaction; pub use util::format_amount; diff --git a/src/ynab/budget.rs b/src/ynab/budget.rs index d31b803..fba6dd0 100644 --- a/src/ynab/budget.rs +++ b/src/ynab/budget.rs @@ -1,10 +1,15 @@ pub struct Budget { budget: ynab_api::models::BudgetDetail, + reimbursables: Vec, } impl Budget { pub fn new(budget: ynab_api::models::BudgetDetail) -> Self { - Self { budget } + let reimbursables = Self::get_reimbursables(&budget); + Self { + budget, + reimbursables, + } } pub fn name(&self) -> String { @@ -15,20 +20,25 @@ impl Budget { self.budget.id.clone() } - pub fn reimbursables(&self) -> Vec { - let reimbursables_id = - if let Some(categories) = &self.budget.categories { - categories - .iter() - .find(|c| c.name == "Reimbursables") - .map(|c| c.id.clone()) - .unwrap() - } else { - panic!("no categories found") - }; + pub fn reimbursables(&self) -> &[super::transaction::Transaction] { + &self.reimbursables + } + + fn get_reimbursables( + budget: &ynab_api::models::BudgetDetail, + ) -> Vec { + let reimbursables_id = if let Some(categories) = &budget.categories { + categories + .iter() + .find(|c| c.name == "Reimbursables") + .map(|c| c.id.clone()) + .unwrap() + } else { + panic!("no categories found") + }; let mut reimbursables = vec![]; - if let Some(payees) = &self.budget.payees { + if let Some(payees) = &budget.payees { let mut payee_map = std::collections::HashMap::new(); for p in payees { payee_map.insert(p.id.clone(), p.name.clone()); @@ -36,7 +46,7 @@ impl Budget { let payee_map = payee_map; let mut transaction_map = std::collections::HashMap::new(); - if let Some(transactions) = &self.budget.transactions { + if let Some(transactions) = &budget.transactions { for t in transactions { transaction_map.insert(t.id.clone(), t); @@ -71,7 +81,7 @@ impl Budget { } let transaction_map = transaction_map; - if let Some(subtransactions) = &self.budget.subtransactions { + if let Some(subtransactions) = &budget.subtransactions { for st in subtransactions { if let Some(category_id) = &st.category_id { if category_id != &reimbursables_id { diff --git a/src/ynab/util.rs b/src/ynab/util.rs index 6be9e6a..af8a8d1 100644 --- a/src/ynab/util.rs +++ b/src/ynab/util.rs @@ -2,5 +2,5 @@ pub fn format_amount(amount: i64) -> String { let dollars = amount.abs() / 1000; let cents = (amount.abs() % 1000) / 10; let sign = if amount < 0 { "-" } else { "" }; - format!("{}{}.{:02}", sign, dollars, cents) + format!("${}{}.{:02}", sign, dollars, cents) } -- cgit v1.2.3-54-g00ecf