aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2019-08-12 00:09:21 -0400
committerJesse Luehrs <doy@tozt.net>2019-08-12 00:09:21 -0400
commitca12c28f744c34b27c57dd832ac5f713eff61ab4 (patch)
tree866f8a6e995c79391002891cb4e004cf7eb09a17 /src
parent430b17c92ee8e15bacce2c058d3e56f187da5fc3 (diff)
downloadynab-api-ca12c28f744c34b27c57dd832ac5f713eff61ab4.tar.gz
ynab-api-ca12c28f744c34b27c57dd832ac5f713eff61ab4.zip
start building out a cursive ui
Diffstat (limited to 'src')
-rw-r--r--src/checks.rs32
-rw-r--r--src/display.rs14
-rw-r--r--src/main.rs68
-rw-r--r--src/table.rs64
-rw-r--r--src/ynab.rs2
-rw-r--r--src/ynab/budget.rs40
-rw-r--r--src/ynab/util.rs2
7 files changed, 167 insertions, 55 deletions
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<super::ynab::Transaction, TxnColumn>;
+
+impl cursive_table_view::TableViewItem<TxnColumn>
+ 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<super::ynab::Transaction>) -> 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<super::transaction::Transaction>,
}
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<super::transaction::Transaction> {
- 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<super::transaction::Transaction> {
+ 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)
}