From f555d23355cdd548aa46e497a4bbb9280f77f1ec Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 17:33:40 +0600 Subject: [PATCH 01/12] Fix issues where filtering ignore the last day of a month --- app/src/modifier/shared.rs | 10 ++++++---- db/src/models/activities.rs | 3 ++- db/src/models/txs.rs | 26 ++++++++++++++++++-------- shared/Cargo.toml | 1 + shared/src/models.rs | 4 ++++ tui/src/key_checker/key_handler.rs | 12 ++++++------ 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/app/src/modifier/shared.rs b/app/src/modifier/shared.rs index 4db16cb..7c7d28a 100644 --- a/app/src/modifier/shared.rs +++ b/app/src/modifier/shared.rs @@ -2,7 +2,7 @@ use anyhow::{Result, anyhow}; use chrono::{Days, Local, Months, NaiveDate, NaiveTime}; use rex_db::ConnCache; use rex_db::models::{Balance, DateNature, FetchNature, NewSearch, NewTx, Tx, TxType}; -use rex_shared::models::Dollar; +use rex_shared::models::{Dollar, LAST_POSSIBLE_TIME}; use crate::utils::parse_amount_nature_cent; @@ -118,7 +118,7 @@ pub fn parse_search_fields<'a>( let end_date = NaiveDate::from_ymd_opt(year + 1, 1, 1) .ok_or_else(|| anyhow!("{year} is an invalid year"))? - .and_time(NaiveTime::MIN); + .and_time(LAST_POSSIBLE_TIME); Some(DateNature::ByYear { start_date, @@ -130,11 +130,13 @@ pub fn parse_search_fields<'a>( let month = split_date[1].parse::()?; let start_date = NaiveDate::from_ymd_opt(year, month, 1) - .ok_or_else(|| anyhow!("{year} or {month} value is invalid"))? - .and_time(NaiveTime::MIN); + .ok_or_else(|| anyhow!("{year} or {month} value is invalid"))?; let end_date = start_date + Months::new(1) - Days::new(1); + let start_date = start_date.and_time(NaiveTime::MIN); + let end_date = end_date.and_time(LAST_POSSIBLE_TIME); + Some(DateNature::ByMonth { start_date, end_date, diff --git a/db/src/models/activities.rs b/db/src/models/activities.rs index d313f6a..def6113 100644 --- a/db/src/models/activities.rs +++ b/db/src/models/activities.rs @@ -1,6 +1,7 @@ use chrono::{Datelike, Days, Local, Months, NaiveDate, NaiveDateTime, NaiveTime}; use diesel::prelude::*; use diesel::result::Error; +use rex_shared::models::LAST_POSSIBLE_TIME; use crate::ConnCache; use crate::models::{ActivityNature, ActivityTx, FullActivityTx}; @@ -77,7 +78,7 @@ impl Activity { let end_date = start_date + Months::new(1) - Days::new(1); let start_date = start_date.and_time(NaiveTime::MIN); - let end_date = end_date.and_time(NaiveTime::MIN); + let end_date = end_date.and_time(LAST_POSSIBLE_TIME); let results: Vec<(Activity, ActivityTx)> = act::activities .inner_join(tx::activity_txs.on(tx::activity_num.eq(act::id))) diff --git a/db/src/models/txs.rs b/db/src/models/txs.rs index 7ef19b1..a9da5a3 100644 --- a/db/src/models/txs.rs +++ b/db/src/models/txs.rs @@ -3,7 +3,7 @@ use diesel::dsl::{exists, sql}; use diesel::prelude::*; use diesel::result::Error; use diesel::sql_types::{Integer, Text}; -use rex_shared::models::Cent; +use rex_shared::models::{Cent, LAST_POSSIBLE_TIME}; use std::collections::HashMap; use crate::ConnCache; @@ -54,7 +54,13 @@ impl<'a> NewSearch<'a> { if let Some(d) = self.date.as_ref() { match d { DateNature::Exact(d) => { - query = query.filter(date.eq(d)); + let start_date = NaiveDate::from_ymd_opt(d.year(), d.month(), d.day()).unwrap(); + let end_date = start_date; + + let start_date = start_date.and_time(NaiveTime::MIN); + let end_date = end_date.and_time(LAST_POSSIBLE_TIME); + + query = query.filter(date.between(start_date, end_date)); } DateNature::ByMonth { start_date, @@ -422,19 +428,23 @@ impl Tx { let dates = match nature { FetchNature::Monthly => { - let start_date = NaiveDate::from_ymd_opt(d.year(), d.month(), 1) - .unwrap() - .and_time(NaiveTime::MIN); + let start_date = NaiveDate::from_ymd_opt(d.year(), d.month(), 1).unwrap(); let end_date = start_date + Months::new(1) - Days::new(1); + + let start_date = start_date.and_time(NaiveTime::MIN); + let end_date = end_date.and_time(LAST_POSSIBLE_TIME); + Some((start_date, end_date)) } FetchNature::Yearly => { - let start_date = NaiveDate::from_ymd_opt(d.year(), 1, 1) - .unwrap() - .and_time(NaiveTime::MIN); + let start_date = NaiveDate::from_ymd_opt(d.year(), 1, 1).unwrap(); let end_date = start_date + Months::new(12) - Days::new(1); + + let start_date = start_date.and_time(NaiveTime::MIN); + let end_date = end_date.and_time(LAST_POSSIBLE_TIME); + Some((start_date, end_date)) } FetchNature::All => None, diff --git a/shared/Cargo.toml b/shared/Cargo.toml index ac420ed..bf1e42d 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -10,4 +10,5 @@ repository = "https://github.com/TheRustyPickle/Rex" license = "MIT" [dependencies] +chrono.workspace = true diff --git a/shared/src/models.rs b/shared/src/models.rs index d8e1ddd..ca1320b 100644 --- a/shared/src/models.rs +++ b/shared/src/models.rs @@ -1,7 +1,11 @@ +use chrono::NaiveTime; use std::cmp::Ordering; use std::fmt; use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; +pub const LAST_POSSIBLE_TIME: NaiveTime = + NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(); + impl Add for Cent { type Output = Cent; diff --git a/tui/src/key_checker/key_handler.rs b/tui/src/key_checker/key_handler.rs index 0d0be52..f489f40 100644 --- a/tui/src/key_checker/key_handler.rs +++ b/tui/src/key_checker/key_handler.rs @@ -302,12 +302,12 @@ impl<'a> InputKeyHandler<'a> { pub fn do_summary_hidden_mode(&mut self) { *self.summary_hidden_mode = !*self.summary_hidden_mode; - if *self.summary_hidden_mode { - if self.summary_table.state.selected().is_none() && !self.summary_table.items.is_empty() - { - *self.summary_tab = SummaryTab::Table; - self.summary_table.state.select(Some(0)); - } + if *self.summary_hidden_mode + && self.summary_table.state.selected().is_none() + && !self.summary_table.items.is_empty() + { + *self.summary_tab = SummaryTab::Table; + self.summary_table.state.select(Some(0)); } } From 5133ee461359a7e064349e286b697af1b72460d5 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 17:34:56 +0600 Subject: [PATCH 02/12] Update --- Cargo.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bcab823..62f036d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1960,6 +1960,9 @@ dependencies = [ [[package]] name = "rex-shared" version = "0.2.0" +dependencies = [ + "chrono", +] [[package]] name = "rex-tui" From 67c57068c70b73e93e1d62da1e2e7cc1f4f27a2c Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 17:57:43 +0600 Subject: [PATCH 03/12] Add is primary for tx tags --- app/src/migration.rs | 4 ++-- app/src/modifier/new_tx.rs | 6 ++--- .../down.sql | 13 ++++++++++ .../2025-10-31-114912-0000_primary_tag/up.sql | 24 +++++++++++++++++++ db/src/models/tx_tags.rs | 9 +++++-- db/src/schema.rs | 1 + 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql create mode 100644 db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql diff --git a/app/src/migration.rs b/app/src/migration.rs index 9234877..de6c264 100644 --- a/app/src/migration.rs +++ b/app/src/migration.rs @@ -348,10 +348,10 @@ fn migrate_tx( let mut tx_tags = Vec::new(); - for tag in tag_list { + for (index, tag) in tag_list.into_iter().enumerate() { let tag_data = NewTag::new(&tag).insert(db_conn)?; - let tx_tag = TxTag::new(added_tx.id, tag_data.id); + let tx_tag = TxTag::new(added_tx.id, tag_data.id, index == 0); tx_tags.push(tx_tag); } diff --git a/app/src/modifier/new_tx.rs b/app/src/modifier/new_tx.rs index 2256bdb..7b44b39 100644 --- a/app/src/modifier/new_tx.rs +++ b/app/src/modifier/new_tx.rs @@ -93,9 +93,9 @@ pub(crate) fn add_new_tx( let mut tx_tags = Vec::new(); - for tag in tag_list { + for (index, tag) in tag_list.into_iter().enumerate() { if let Ok(tag_id) = db_conn.cache().get_tag_id(&tag) { - let tx_tag = TxTag::new(added_tx.id, tag_id); + let tx_tag = TxTag::new(added_tx.id, tag_id, index == 0); tx_tags.push(tx_tag); continue; } @@ -106,7 +106,7 @@ pub(crate) fn add_new_tx( new_tags.push(tag_data.clone()); - let tx_tag = TxTag::new(added_tx.id, tag_data.id); + let tx_tag = TxTag::new(added_tx.id, tag_data.id, index == 0); tx_tags.push(tx_tag); } diff --git a/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql b/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql new file mode 100644 index 0000000..a5df6fe --- /dev/null +++ b/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql @@ -0,0 +1,13 @@ +CREATE TABLE tx_tags_old ( + tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (tx_id, tag_id) +); + +INSERT INTO tx_tags_old (tx_id, tag_id) +SELECT tx_id, tag_id +FROM tx_tags; + +DROP TABLE tx_tags; + +ALTER TABLE tx_tags_old RENAME TO tx_tags; diff --git a/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql b/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql new file mode 100644 index 0000000..a415c56 --- /dev/null +++ b/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql @@ -0,0 +1,24 @@ +CREATE TABLE tx_tags_new ( + tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (tx_id, tag_id) +); + +INSERT INTO tx_tags_new (tx_id, tag_id, is_primary) +SELECT + tx_id, + tag_id, + CASE + WHEN ROW_NUMBER() OVER (PARTITION BY tx_id ORDER BY tag_id) = 1 THEN 1 + ELSE 0 + END AS is_primary +FROM tx_tags; + +DROP TABLE tx_tags; + +ALTER TABLE tx_tags_new RENAME TO tx_tags; + +CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags(tx_id); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags(tag_id); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags(tag_id, tx_id); diff --git a/db/src/models/tx_tags.rs b/db/src/models/tx_tags.rs index 02fd644..5997ad6 100644 --- a/db/src/models/tx_tags.rs +++ b/db/src/models/tx_tags.rs @@ -8,12 +8,17 @@ use crate::schema::tx_tags; pub struct TxTag { pub tx_id: i32, pub tag_id: i32, + pub is_primary: bool, } impl TxTag { #[must_use] - pub fn new(tx_id: i32, tag_id: i32) -> Self { - TxTag { tx_id, tag_id } + pub fn new(tx_id: i32, tag_id: i32, is_primary: bool) -> Self { + TxTag { + tx_id, + tag_id, + is_primary, + } } pub fn get_by_tx_ids( diff --git a/db/src/schema.rs b/db/src/schema.rs index d548e70..8227fdb 100644 --- a/db/src/schema.rs +++ b/db/src/schema.rs @@ -60,6 +60,7 @@ diesel::table! { tx_tags (tx_id, tag_id) { tx_id -> Integer, tag_id -> Integer, + is_primary -> Bool, } } From 6e9fb572d0342393c95ddcf8f6ee1d5a039e5e34 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 18:43:28 +0600 Subject: [PATCH 04/12] Add filtering by primary + force all tag match in search --- db/src/models/tx_tags.rs | 7 +++++-- db/src/models/txs.rs | 18 +++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/db/src/models/tx_tags.rs b/db/src/models/tx_tags.rs index 5997ad6..9a3d81e 100644 --- a/db/src/models/tx_tags.rs +++ b/db/src/models/tx_tags.rs @@ -25,9 +25,12 @@ impl TxTag { tx_ids: Vec, db_conn: &mut impl ConnCache, ) -> Result, Error> { - use crate::schema::tx_tags::dsl::{tx_id, tx_tags}; + use crate::schema::tx_tags::dsl::{is_primary, tx_id, tx_tags}; - tx_tags.filter(tx_id.eq_any(tx_ids)).load(db_conn.conn()) + tx_tags + .filter(tx_id.eq_any(tx_ids)) + .order(is_primary.desc()) + .load(db_conn.conn()) } pub fn insert_batch(txs: Vec, db_conn: &mut impl ConnCache) -> Result { diff --git a/db/src/models/txs.rs b/db/src/models/txs.rs index a9da5a3..bf92f74 100644 --- a/db/src/models/txs.rs +++ b/db/src/models/txs.rs @@ -1,5 +1,5 @@ use chrono::{Datelike, Days, Months, NaiveDate, NaiveDateTime, NaiveTime}; -use diesel::dsl::{exists, sql}; +use diesel::dsl::{count_star, exists, sql}; use diesel::prelude::*; use diesel::result::Error; use diesel::sql_types::{Integer, Text}; @@ -111,12 +111,16 @@ impl<'a> NewSearch<'a> { } } - if let Some(tag_ids) = self.tags.as_ref() { - query = query.filter(exists( - tx_tags::table - .filter(tx_tags::tx_id.eq(id)) - .filter(tx_tags::tag_id.eq_any(tag_ids)), - )); + if let Some(tag_ids) = self.tags.as_ref() + && !tag_ids.is_empty() + { + let subquery = tx_tags::table + .filter(tx_tags::tag_id.eq_any(tag_ids)) + .group_by(tx_tags::tx_id) + .select((tx_tags::tx_id, count_star())) + .having(count_star().eq(tag_ids.len() as i64)); + + query = query.filter(id.eq_any(subquery.select(tx_tags::tx_id))); } let result = query.select(Tx::as_select()).load(db_conn.conn())?; From 57893d126d5abe35f7d5fa618a21c870bda8324f Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 20:31:52 +0600 Subject: [PATCH 05/12] Add borrow/lend column per tag --- app/src/views/summary_view.rs | 45 +++++++++++++++++++++++++++------ db/src/models/txs.rs | 2 +- tui/src/pages/summary_ui.rs | 47 ++++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/app/src/views/summary_view.rs b/app/src/views/summary_view.rs index 64f9437..e9d30e5 100644 --- a/app/src/views/summary_view.rs +++ b/app/src/views/summary_view.rs @@ -87,6 +87,8 @@ impl SummaryView { ) -> Vec> { let mut income_tags = HashMap::new(); let mut expense_tags = HashMap::new(); + let mut borrow_tags = HashMap::new(); + let mut lend_tags = HashMap::new(); let mut total_income = Cent::new(0); let mut total_expense = Cent::new(0); @@ -107,7 +109,7 @@ impl SummaryView { } for tx in &self.txs { - for tag in &tx.tags { + if let Some(tag) = tx.tags.first() { match tx.tx_type { TxType::Income => { total_income += tx.amount; @@ -121,12 +123,23 @@ impl SummaryView { let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); *value += tx.amount; } - // TODO: Check borrow and lends for two new columns - TxType::Transfer - | TxType::Borrow - | TxType::Lend - | TxType::BorrowRepay - | TxType::LendRepay => {} + TxType::Borrow => { + let value = borrow_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); + *value += tx.amount; + } + TxType::BorrowRepay => { + let value = borrow_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); + *value -= tx.amount; + } + TxType::Lend => { + let value = lend_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); + *value += tx.amount; + } + TxType::LendRepay => { + let value = lend_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); + *value -= tx.amount; + } + TxType::Transfer => {} } } } @@ -144,6 +157,9 @@ impl SummaryView { let mut income_amount = Dollar::new(0.0); let mut expense_amount = Dollar::new(0.0); + let mut borrow_amount = Dollar::new(0.0); + let mut lend_amount = Dollar::new(0.0); + if let Some(income) = income_tags.get(&tag.name) { income_percentage = (income.value() as f64 / total_income.value() as f64) * 100.0; income_amount = income.dollar(); @@ -159,6 +175,18 @@ impl SummaryView { no_push = false; } + if let Some(borrow) = borrow_tags.get(&tag.name) { + borrow_amount = borrow.dollar(); + + no_push = false; + } + + if let Some(lend) = lend_tags.get(&tag.name) { + lend_amount = lend.dollar(); + + no_push = false; + } + if no_push { continue; } @@ -208,6 +236,9 @@ impl SummaryView { } } + to_push.push(format!("{borrow_amount:.2}")); + to_push.push(format!("{lend_amount:.2}")); + to_return.push(to_push); } diff --git a/db/src/models/txs.rs b/db/src/models/txs.rs index bf92f74..9faddd9 100644 --- a/db/src/models/txs.rs +++ b/db/src/models/txs.rs @@ -1,5 +1,5 @@ use chrono::{Datelike, Days, Months, NaiveDate, NaiveDateTime, NaiveTime}; -use diesel::dsl::{count_star, exists, sql}; +use diesel::dsl::{count_star, sql}; use diesel::prelude::*; use diesel::result::Error; use diesel::sql_types::{Integer, Text}; diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index 3e58502..e75b8ad 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -63,6 +63,9 @@ pub fn summary_ui( table_headers.push("YoY Expense %"); } + table_headers.push("Borrow"); + table_headers.push("Lend"); + let header_cells = table_headers .into_iter() .map(|h| Cell::from(h).style(Style::default().fg(theme.background()))); @@ -177,10 +180,16 @@ pub fn summary_ui( .enumerate() .map(|(row_index, item)| { let cells = item.iter().enumerate().map(|(index, c)| { + if index == 0 { + return Cell::from(c.to_string()); + } + + let lerp_id = format!("summary_table_main:{index}:{row_index}"); + let Ok(parsed_num) = c.parse::() else { if c == "∞" { - let lerp_id = format!("summary_table_main:{index}:{row_index}"); - lerp_state.lerp(&lerp_id, 0.0); + let new_c = lerp_state.lerp(&lerp_id, 0.0); + return Cell::from(format!("{new_c:.2}").separate_with_commas()); } let symbol = if c.contains('↑') || c.contains('↓') { @@ -192,18 +201,22 @@ pub fn summary_ui( if let Some(sym) = symbol { let c = c.replace(sym, ""); if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_main:{index}:{row_index}"); let new_c = lerp_state.lerp(&lerp_id, parsed_num); return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); } + } else { + log::info!("No symbol found. {c}"); } return Cell::from(c.separate_with_commas()); }; - let lerp_id = format!("summary_table_main:{index}:{row_index}"); let new_c = lerp_state.lerp(&lerp_id, parsed_num); + if index == 7 && row_index == 0 && new_c != 0.0 { + log::info!("{new_c} {c} {parsed_num} {lerp_id}") + } + Cell::from(format!("{new_c:.2}").separate_with_commas()) }); Row::new(cells) @@ -214,21 +227,25 @@ pub fn summary_ui( let table_width = if mode_selection.index == 2 { vec![ - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - ] - } else { - vec![ - Constraint::Percentage(14), + Constraint::Percentage(15), + Constraint::Percentage(15), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), - Constraint::Percentage(15), + ] + } else { + vec![ + Constraint::Percentage(12), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), ] }; @@ -449,7 +466,7 @@ pub fn summary_ui( if let Some(sym) = symbol { let c = c.replace(sym, ""); if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_main:{index}:{row_index}"); + let lerp_id = format!("summary_net_rows:{index}:{row_index}"); let new_c = lerp_state.lerp(&lerp_id, parsed_num); return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); From 5554e785860bb508111623d01f8b9cb3ca041e1d Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Fri, 31 Oct 2025 20:32:27 +0600 Subject: [PATCH 06/12] Remove logs --- tui/src/pages/summary_ui.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index e75b8ad..702cb65 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -205,18 +205,12 @@ pub fn summary_ui( return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); } - } else { - log::info!("No symbol found. {c}"); } return Cell::from(c.separate_with_commas()); }; let new_c = lerp_state.lerp(&lerp_id, parsed_num); - if index == 7 && row_index == 0 && new_c != 0.0 { - log::info!("{new_c} {c} {parsed_num} {lerp_id}") - } - Cell::from(format!("{new_c:.2}").separate_with_commas()) }); Row::new(cells) From 32a9a18fdfdd6cc09a9df213c3e56ce437785691 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sat, 1 Nov 2025 06:39:56 +0600 Subject: [PATCH 07/12] Update --- Cargo.toml | 2 +- tui/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2231a1..0cf1e57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ rex-shared = "0.2.0" rex-db = "0.2.2" rex-app = "0.2.2" chrono = "0.4.42" -diesel = { version = "2.3.2", default-features = false, features = [ +diesel = { version = "2.3.3", default-features = false, features = [ "returning_clauses_for_sqlite_3_35", "sqlite", "chrono", diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 4ce36a4..3986f0e 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -24,8 +24,8 @@ bench = false rex-app.workspace = true rex-shared.workspace = true anyhow.workspace = true +chrono.workspace = true crossterm = "0.29.0" -chrono = "0.4.42" open = "5.3.2" atty = "0.2.14" reqwest = { version = "0.12.23", features = ["blocking", "json"] } From f11659f6629c69764164021b88b90321587e362a Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sat, 1 Nov 2025 07:50:08 +0600 Subject: [PATCH 08/12] Add borrow + lend summary --- app/src/utils.rs | 27 ++++++ app/src/views/summary_models.rs | 40 ++++++++ app/src/views/summary_view.rs | 160 ++++++++++++++------------------ tui/src/pages/summary_ui.rs | 121 ++++++++++++++++++++---- 4 files changed, 239 insertions(+), 109 deletions(-) diff --git a/app/src/utils.rs b/app/src/utils.rs index f811291..e886fca 100644 --- a/app/src/utils.rs +++ b/app/src/utils.rs @@ -56,3 +56,30 @@ pub fn parse_amount_nature_cent(amount: &str) -> Result> { Ok(Some(res)) } + +pub fn compare_change(current: Dollar, previous: Dollar) -> String { + if previous == 0.0 { + "∞".to_string() + } else { + let diff = ((current - previous) / previous) * 100.0; + if diff < 0.0 { + format!("↓{:.2}", diff.abs()) + } else { + format!("↑{:.2}", diff) + } + } +} + +pub fn compare_change_opt(current: Dollar, previous: Option) -> String { + match previous { + Some(prev) if prev != 0.0 => { + let diff = ((current - prev) / prev) * 100.0; + if diff < 0.0 { + format!("↓{:.2}", diff.abs()) + } else { + format!("↑{:.2}", diff) + } + } + _ => "∞".to_string(), + } +} diff --git a/app/src/views/summary_models.rs b/app/src/views/summary_models.rs index 66d55da..4a906fb 100644 --- a/app/src/views/summary_models.rs +++ b/app/src/views/summary_models.rs @@ -280,3 +280,43 @@ impl SummaryMethods { to_return } } + +#[derive(Debug, PartialEq)] +pub(crate) struct SummaryLendBorrows { + pub(crate) borrows: Dollar, + pub(crate) lends: Dollar, + mom_yoy_borrows: Option, + mom_yoy_lends: Option, +} + +impl SummaryLendBorrows { + pub(crate) fn new( + borrows: Dollar, + lends: Dollar, + mom_yoy_borrows: Option, + mom_yoy_lends: Option, + ) -> Self { + Self { + borrows, + lends, + mom_yoy_borrows, + mom_yoy_lends, + } + } + + pub(crate) fn array(&self) -> Vec { + let mut to_return = vec![ + format!("{:.2}", self.borrows.value()), + format!("{:.2}", self.lends.value()), + ]; + + if let Some(mom_yoy_borrows) = &self.mom_yoy_borrows { + to_return.push(mom_yoy_borrows.to_string()); + } + if let Some(mom_yoy_lends) = &self.mom_yoy_lends { + to_return.push(mom_yoy_lends.to_string()); + } + + to_return + } +} diff --git a/app/src/views/summary_view.rs b/app/src/views/summary_view.rs index e9d30e5..a93bd00 100644 --- a/app/src/views/summary_view.rs +++ b/app/src/views/summary_view.rs @@ -5,10 +5,10 @@ use rex_db::models::{FetchNature, FullTx, TxType}; use rex_shared::models::{Cent, Dollar}; use std::collections::HashMap; -use crate::utils::{get_percentages, month_year_to_unique}; +use crate::utils::{compare_change, compare_change_opt, get_percentages, month_year_to_unique}; use crate::views::{ - LargestMomvement, LargestType, PeakMonthlyMovement, PeakType, SummaryLargest, SummaryMethods, - SummaryNet, SummaryPeak, + LargestMomvement, LargestType, PeakMonthlyMovement, PeakType, SummaryLargest, + SummaryLendBorrows, SummaryMethods, SummaryNet, SummaryPeak, }; /// Contains `FullTx` to generate summary data. Will always contain the exact number of txs from @@ -23,6 +23,7 @@ pub struct FullSummary { largest: Vec, peak: Vec, net: SummaryNet, + lend_borrows: SummaryLendBorrows, } impl FullSummary { @@ -42,6 +43,10 @@ impl FullSummary { pub fn largest_array(&self) -> Vec> { self.largest.iter().map(SummaryLargest::array).collect() } + + pub fn lend_borrows_array(&self) -> Vec> { + vec![self.lend_borrows.array()] + } } type CacheTxs = HashMap>; @@ -202,38 +207,14 @@ impl SummaryView { let compare_expense = compare_expense_tags.get(&tag.name); - if let Some(last_income) = compare_income { - let last_earning = last_income.dollar(); - - let earning_increased_percentage = - ((income_amount - last_earning) / last_earning) * 100.0; - - if last_earning == 0.0 { - to_push.push("∞".to_string()); - } else if earning_increased_percentage < 0.0 { - to_push.push(format!("↓{:.2}", earning_increased_percentage.abs())); - } else { - to_push.push(format!("↑{earning_increased_percentage:.2}")); - } - } else { - to_push.push("∞".to_string()); - } - - if let Some(last_expense) = compare_expense { - let last_expense = last_expense.dollar(); - let expense_increased_percentage = - ((expense_amount - last_expense) / last_expense) * 100.0; - - if last_expense == 0.0 { - to_push.push("∞".to_string()); - } else if expense_increased_percentage < 0.0 { - to_push.push(format!("↓{:.2}", expense_increased_percentage.abs())); - } else { - to_push.push(format!("↑{expense_increased_percentage:.2}")); - } - } else { - to_push.push("∞".to_string()); - } + to_push.push(compare_change_opt( + income_amount, + compare_income.map(|x| x.dollar()), + )); + to_push.push(compare_change_opt( + expense_amount, + compare_expense.map(|x| x.dollar()), + )); } to_push.push(format!("{borrow_amount:.2}")); @@ -260,7 +241,6 @@ impl SummaryView { let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0)); *value += tx.amount; } - // TODO: Check borrow and lends for two new columns TxType::Transfer | TxType::Borrow | TxType::Lend @@ -273,7 +253,6 @@ impl SummaryView { (income_tags, expense_tags) } - #[allow(private_interfaces)] pub fn generate_summary( &self, last_summary: Option<&FullSummary>, @@ -311,6 +290,9 @@ impl SummaryView { let mut last_peak_earning = PeakMonthlyMovement::default(); let mut last_peak_expense = PeakMonthlyMovement::default(); + let mut outstanding_borrows = Cent::new(0); + let mut outstanding_lends = Cent::new(0); + for tx in &self.txs { ongoing_date = tx.date.date(); @@ -368,12 +350,19 @@ impl SummaryView { last_peak_expense.amount += tx.amount; } - // TODO: Check borrow and lends for two new columns - TxType::Transfer - | TxType::Borrow - | TxType::Lend - | TxType::BorrowRepay - | TxType::LendRepay => {} + TxType::Borrow => { + outstanding_borrows += tx.amount; + } + TxType::Lend => { + outstanding_lends += tx.amount; + } + TxType::BorrowRepay => { + outstanding_borrows -= tx.amount; + } + TxType::LendRepay => { + outstanding_lends -= tx.amount; + } + TxType::Transfer => {} } } @@ -452,27 +441,8 @@ impl SummaryView { let current_earning = method_earning[&method.name].dollar(); let current_expense = method_expense[&method.name].dollar(); - let earning_increased_percentage = - ((current_earning - last_earning) / last_earning) * 100.0; - - if last_earning == 0.0 { - mom_yoy_earning = Some("∞".to_string()); - } else if earning_increased_percentage < 0.0 { - mom_yoy_earning = Some(format!("↓{:.2}", earning_increased_percentage.abs())); - } else { - mom_yoy_earning = Some(format!("↑{earning_increased_percentage:.2}")); - } - - let expense_increased_percentage = - ((current_expense - last_expense) / last_expense) * 100.0; - - if last_expense == 0.0 { - mom_yoy_expense = Some("∞".to_string()); - } else if expense_increased_percentage < 0.0 { - mom_yoy_expense = Some(format!("↓{:.2}", expense_increased_percentage.abs())); - } else { - mom_yoy_expense = Some(format!("↑{expense_increased_percentage:.2}")); - } + mom_yoy_earning = Some(compare_change(current_earning, last_earning)); + mom_yoy_expense = Some(compare_change(current_expense, last_expense)); } if !no_mom_yoy && mom_yoy_expense.is_none() && mom_yoy_earning.is_none() { @@ -503,35 +473,32 @@ impl SummaryView { let mut net_mom_yoy_earning = None; let mut net_mom_yoy_expense = None; + let mut mom_yoy_borrows = None; + let mut mom_yoy_lends = None; + if let Some(last_summary) = last_summary && !no_mom_yoy { - let comparison = &last_summary.net; - - let last_earning = comparison.total_income; - let last_expense = comparison.total_expense; - - let earning_increased_percentage = - ((total_income.dollar() - last_earning) / last_earning) * 100.0; - - if last_earning == 0.0 { - net_mom_yoy_earning = Some("∞".to_string()); - } else if earning_increased_percentage < 0.0 { - net_mom_yoy_earning = Some(format!("↓{:.2}", earning_increased_percentage.abs())); - } else { - net_mom_yoy_earning = Some(format!("↑{earning_increased_percentage:.2}")); - } - - let expense_increased_percentage = - ((total_expense.dollar() - last_expense) / last_expense) * 100.0; - - if last_expense == 0.0 { - net_mom_yoy_expense = Some("∞".to_string()); - } else if expense_increased_percentage < 0.0 { - net_mom_yoy_expense = Some(format!("↓{:.2}", expense_increased_percentage.abs())); - } else { - net_mom_yoy_expense = Some(format!("↑{expense_increased_percentage:.2}")); - } + let net_comparison = &last_summary.net; + + let lend_borrows_comparison = &last_summary.lend_borrows; + + net_mom_yoy_earning = Some(compare_change( + total_income.dollar(), + net_comparison.total_income, + )); + net_mom_yoy_expense = Some(compare_change( + total_expense.dollar(), + net_comparison.total_expense, + )); + mom_yoy_borrows = Some(compare_change( + outstanding_borrows.dollar(), + lend_borrows_comparison.borrows, + )); + mom_yoy_lends = Some(compare_change( + outstanding_lends.dollar(), + lend_borrows_comparison.lends, + )); } if !no_mom_yoy && net_mom_yoy_expense.is_none() && net_mom_yoy_earning.is_none() { @@ -539,6 +506,11 @@ impl SummaryView { net_mom_yoy_expense = Some("∞".to_string()); } + if !no_mom_yoy && mom_yoy_borrows.is_none() && mom_yoy_lends.is_none() { + mom_yoy_borrows = Some("∞".to_string()); + mom_yoy_lends = Some("∞".to_string()); + } + let summary_net = SummaryNet::new( total_income.dollar(), total_expense.dollar(), @@ -578,11 +550,19 @@ impl SummaryView { ), ]; + let summary_lend_borrows = SummaryLendBorrows::new( + outstanding_borrows.dollar(), + outstanding_lends.dollar(), + mom_yoy_borrows, + mom_yoy_lends, + ); + FullSummary { methods: method_data, net: summary_net, largest: summary_largest, peak: summary_peak, + lend_borrows: summary_lend_borrows, } } } diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index 702cb65..aa940a3 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -47,7 +47,7 @@ pub fn summary_ui( "Total Expense" }; - let mut table_headers = vec![ + let mut tag_table_headers = vec![ tag_header, total_income_header, total_expense_header, @@ -55,20 +55,24 @@ pub fn summary_ui( "Expense %", ]; + let mut lend_borrow_headers = vec!["Borrow", "Lend"]; + if mode_selection.index == 0 { - table_headers.push("MoM Income %"); - table_headers.push("MoM Expense %"); + tag_table_headers.push("MoM Income %"); + tag_table_headers.push("MoM Expense %"); + + lend_borrow_headers.push("MoM Borrow %"); + lend_borrow_headers.push("MoM Lend %"); } else if mode_selection.index == 1 { - table_headers.push("YoY Income %"); - table_headers.push("YoY Expense %"); + tag_table_headers.push("YoY Income %"); + tag_table_headers.push("YoY Expense %"); + + lend_borrow_headers.push("YoY Borrow %"); + lend_borrow_headers.push("YoY Lend %"); } - table_headers.push("Borrow"); - table_headers.push("Lend"); - - let header_cells = table_headers - .into_iter() - .map(|h| Cell::from(h).style(Style::default().fg(theme.background()))); + tag_table_headers.push("Borrow"); + tag_table_headers.push("Lend"); let mut method_headers = vec!["Method", "Total Income", "Total Expense"]; @@ -92,7 +96,15 @@ pub fn summary_ui( .iter() .map(|h| Cell::from(*h).style(Style::default().fg(theme.background()))); - let header = Row::new(header_cells) + let tag_table_header_cells = tag_table_headers + .into_iter() + .map(|h| Cell::from(h).style(Style::default().fg(theme.background()))); + + let lend_borrow_header_cells = lend_borrow_headers + .into_iter() + .map(|h| Cell::from(h).style(Style::default().fg(theme.background()))); + + let tag_table_header = Row::new(tag_table_header_cells) .style(Style::default().bg(theme.header())) .height(1) .bottom_margin(0); @@ -102,6 +114,11 @@ pub fn summary_ui( .height(1) .bottom_margin(0); + let lend_borrow_header = Row::new(lend_borrow_header_cells) + .style(Style::default().bg(theme.header())) + .height(1) + .bottom_margin(0); + let method_len = conn.get_tx_methods().len() as u16; let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2); @@ -111,6 +128,7 @@ pub fn summary_ui( main_layout = main_layout.constraints([ Constraint::Length(method_len + 2), Constraint::Length(3), + Constraint::Length(3), Constraint::Length(4), Constraint::Min(0), ]); @@ -126,6 +144,7 @@ pub fn summary_ui( Constraint::Length(method_len + 2), Constraint::Length(3), Constraint::Length(4), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout @@ -138,6 +157,7 @@ pub fn summary_ui( Constraint::Length(method_len + 2), Constraint::Length(3), Constraint::Length(4), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout @@ -149,20 +169,24 @@ pub fn summary_ui( Constraint::Length(method_len + 2), Constraint::Length(3), Constraint::Length(4), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout - .constraints([Constraint::Percentage(100), Constraint::Percentage(50)]); + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]); } _ => {} } } let chunks = main_layout.split(size); + + // Horizontal split for the peak + largest area. Split the main block into several vertical + // chunk => Take a specific vertical take and split it into two Horizontal chunk once again let summary_chunk = if summary_hidden_mode { - summary_layout.split(chunks[2]) + summary_layout.split(chunks[3]) } else { - summary_layout.split(chunks[5 - mode_selection.index]) + summary_layout.split(chunks[6 - mode_selection.index]) }; f.render_widget(main_block(theme), size); @@ -244,7 +268,7 @@ pub fn summary_ui( }; let mut table_area = Table::new(rows, table_width) - .header(header) + .header(tag_table_header) .block(styled_block("Tags", theme)) .style(Style::default().fg(theme.border())); @@ -434,6 +458,62 @@ pub fn summary_ui( .block(styled_block_no_bottom("", theme)) .style(Style::default().fg(theme.border())); + let lend_borrow_rows = full_summary + .lend_borrows_array() + .into_iter() + .enumerate() + .map(|(row_index, item)| { + let cells = item.into_iter().enumerate().map(|(index, c)| { + let lerp_id = format!("lend_borrow_table:{index}:{row_index}"); + + if let Ok(parsed_num) = c.parse::() { + let new_c = lerp_state.lerp(&lerp_id, parsed_num); + + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + let symbol = if c.contains('↑') || c.contains('↓') { + c.chars().next() + } else { + None + }; + + if let Some(sym) = symbol { + let c = c.replace(sym, ""); + if let Ok(parsed_num) = c.parse::() { + let new_c = lerp_state.lerp(&lerp_id, parsed_num); + + return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); + } + } + + if c == "∞" { + lerp_state.lerp(&lerp_id, 0.0); + } + Cell::from(c.separate_with_commas()) + } + }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(theme.text())) + }); + + let lend_borrow_widths = if mode_selection.index == 2 { + vec![Constraint::Percentage(50), Constraint::Percentage(50)] + } else { + vec![ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + }; + + let lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths) + .header(lend_borrow_header) + .block(styled_block("", theme)) + .style(Style::default().fg(theme.border())); + let net_row = full_summary .net_array() .into_iter() @@ -541,19 +621,22 @@ pub fn summary_ui( 0 => { f.render_widget(year_tab, chunks[1]); f.render_widget(month_tab, chunks[2]); - f.render_stateful_widget(table_area, chunks[6], &mut table_data.state); + f.render_stateful_widget(table_area, chunks[7], &mut table_data.state); f.render_widget(net_area, chunks[4]); + f.render_widget(lend_borrow_area, chunks[5]); f.render_widget(method_area, chunks[3]); } 1 => { f.render_widget(year_tab, chunks[1]); - f.render_stateful_widget(table_area, chunks[5], &mut table_data.state); + f.render_stateful_widget(table_area, chunks[6], &mut table_data.state); f.render_widget(net_area, chunks[3]); + f.render_widget(lend_borrow_area, chunks[4]); f.render_widget(method_area, chunks[2]); } 2 => { - f.render_stateful_widget(table_area, chunks[4], &mut table_data.state); + f.render_stateful_widget(table_area, chunks[5], &mut table_data.state); f.render_widget(net_area, chunks[2]); + f.render_widget(lend_borrow_area, chunks[3]); f.render_widget(method_area, chunks[1]); } _ => {} From 19396c8a7730852eeb22404813be2ae062a22216 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sat, 1 Nov 2025 07:50:13 +0600 Subject: [PATCH 09/12] Update --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62f036d..a4e6f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -545,9 +545,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8496eeb328dce26ee9d9b73275d396d9bddb433fa30106cf6056dd8c3c2764c" +checksum = "5e7624a3bb9fffd82fff016be9a7f163d20e5a89eb8d28f9daaa6b30fff37500" dependencies = [ "chrono", "diesel_derives", @@ -2041,7 +2041,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From bbca70af3c04fed00ea6055042049ed56793f8f0 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sat, 1 Nov 2025 07:52:37 +0600 Subject: [PATCH 10/12] Clippy fix --- app/src/migration.rs | 2 +- app/src/ui_helper/shared.rs | 2 +- app/src/ui_helper/verifier.rs | 6 +++--- app/src/utils.rs | 4 ++-- app/src/views/summary_models.rs | 16 ++++++++-------- app/src/views/summary_view.rs | 11 ++++++----- app/src/views/tx_view.rs | 8 ++++---- tui/src/key_checker/key_handler.rs | 8 ++++---- tui/src/pages/activity_ui.rs | 2 +- tui/src/pages/popups/info.rs | 4 ++-- tui/src/pages/popups/models.rs | 2 +- tui/src/pages/search_ui.rs | 2 +- tui/src/pages/summary_ui.rs | 2 +- tui/src/tx_handler/tx_data.rs | 8 ++++---- tui/src/utility/utils.rs | 2 +- 15 files changed, 40 insertions(+), 39 deletions(-) diff --git a/app/src/migration.rs b/app/src/migration.rs index de6c264..b8488fe 100644 --- a/app/src/migration.rs +++ b/app/src/migration.rs @@ -117,7 +117,7 @@ pub fn start_migration(mut old_db_conn: DbConn, db_conn: &mut DbConn) -> Result< let mut skipped_activity_id = HashSet::new(); for row in rows { - let method = row.tx_method.to_string(); + let method = row.tx_method.clone(); let from_method; let mut to_method = String::new(); diff --git a/app/src/ui_helper/shared.rs b/app/src/ui_helper/shared.rs index f9aa7d1..ba5a7ce 100644 --- a/app/src/ui_helper/shared.rs +++ b/app/src/ui_helper/shared.rs @@ -14,7 +14,7 @@ pub(crate) fn get_best_match(data: &str, matching_set: &[String]) -> String { best_score = new_score; } } - best_match.to_string() + best_match.clone() } #[derive(Copy, Clone)] diff --git a/app/src/ui_helper/verifier.rs b/app/src/ui_helper/verifier.rs index 7129cde..64041f7 100644 --- a/app/src/ui_helper/verifier.rs +++ b/app/src/ui_helper/verifier.rs @@ -306,9 +306,9 @@ impl<'a> Verifier<'a> { // If either of them is empty, the one that is not empty is the value we want to use for using in replacement let final_value = if first_value.is_empty() || last_value.is_empty() { if first_value.is_empty() { - last_value.to_string() + last_value.clone() } else { - first_value.to_string() + first_value.clone() } } else { // If both value is intact, do the calculation and the result is for replacement @@ -417,7 +417,7 @@ impl<'a> Verifier<'a> { let method_name = &method.name; if method_name.to_lowercase() == user_method.to_lowercase() { - *user_method = method_name.to_string(); + *user_method = method_name.clone(); return Ok(Output::Accepted(Field::Amount)); } } diff --git a/app/src/utils.rs b/app/src/utils.rs index e886fca..cfa0532 100644 --- a/app/src/utils.rs +++ b/app/src/utils.rs @@ -65,7 +65,7 @@ pub fn compare_change(current: Dollar, previous: Dollar) -> String { if diff < 0.0 { format!("↓{:.2}", diff.abs()) } else { - format!("↑{:.2}", diff) + format!("↑{diff:.2}") } } } @@ -77,7 +77,7 @@ pub fn compare_change_opt(current: Dollar, previous: Option) -> String { if diff < 0.0 { format!("↓{:.2}", diff.abs()) } else { - format!("↑{:.2}", diff) + format!("↑{diff:.2}") } } _ => "∞".to_string(), diff --git a/app/src/views/summary_models.rs b/app/src/views/summary_models.rs index 4a906fb..deffb44 100644 --- a/app/src/views/summary_models.rs +++ b/app/src/views/summary_models.rs @@ -85,11 +85,11 @@ impl SummaryNet { }; if let Some(mom_yoy_earning) = &self.mom_yoy_earning { - to_return[0].push(mom_yoy_earning.to_string()); + to_return[0].push(mom_yoy_earning.clone()); } if let Some(mom_yoy_expense) = &self.mom_yoy_expense { - to_return[0].push(mom_yoy_expense.to_string()); + to_return[0].push(mom_yoy_expense.clone()); } to_return @@ -158,14 +158,14 @@ impl SummaryLargest { self.largest_type.to_string(), String::from("-"), format!("{:.2}", self.amount.value()), - self.method.to_string(), + self.method.clone(), ] } else { vec![ self.largest_type.to_string(), self.date.format("%d-%m-%Y").to_string(), format!("{:.2}", self.amount.value()), - self.method.to_string(), + self.method.clone(), ] } } @@ -271,10 +271,10 @@ impl SummaryMethods { }; if let Some(mom_yoy_earning) = &self.mom_yoy_earning { - to_return.push(mom_yoy_earning.to_string()); + to_return.push(mom_yoy_earning.clone()); } if let Some(mom_yoy_expense) = &self.mom_yoy_expense { - to_return.push(mom_yoy_expense.to_string()); + to_return.push(mom_yoy_expense.clone()); } to_return @@ -311,10 +311,10 @@ impl SummaryLendBorrows { ]; if let Some(mom_yoy_borrows) = &self.mom_yoy_borrows { - to_return.push(mom_yoy_borrows.to_string()); + to_return.push(mom_yoy_borrows.clone()); } if let Some(mom_yoy_lends) = &self.mom_yoy_lends { - to_return.push(mom_yoy_lends.to_string()); + to_return.push(mom_yoy_lends.clone()); } to_return diff --git a/app/src/views/summary_view.rs b/app/src/views/summary_view.rs index a93bd00..ffc41e2 100644 --- a/app/src/views/summary_view.rs +++ b/app/src/views/summary_view.rs @@ -44,6 +44,7 @@ impl FullSummary { self.largest.iter().map(SummaryLargest::array).collect() } + #[must_use] pub fn lend_borrows_array(&self) -> Vec> { vec![self.lend_borrows.array()] } @@ -209,11 +210,11 @@ impl SummaryView { to_push.push(compare_change_opt( income_amount, - compare_income.map(|x| x.dollar()), + compare_income.map(Cent::dollar), )); to_push.push(compare_change_opt( expense_amount, - compare_expense.map(|x| x.dollar()), + compare_expense.map(Cent::dollar), )); } @@ -276,8 +277,8 @@ impl SummaryView { let mut method_expense = HashMap::new(); for method in conn.cache().get_methods() { - method_earning.insert(method.name.to_string(), Cent::new(0)); - method_expense.insert(method.name.to_string(), Cent::new(0)); + method_earning.insert(method.name.clone(), Cent::new(0)); + method_expense.insert(method.name.clone(), Cent::new(0)); } let mut ongoing_month = 0; @@ -451,7 +452,7 @@ impl SummaryView { } let method_summary = SummaryMethods::new( - method.name.to_string(), + method.name.clone(), method_earning[&method.name].dollar(), method_expense[&method.name].dollar(), earning_percentage, diff --git a/app/src/views/tx_view.rs b/app/src/views/tx_view.rs index b76e7d5..42a121b 100644 --- a/app/src/views/tx_view.rs +++ b/app/src/views/tx_view.rs @@ -113,7 +113,7 @@ impl TxViewGroup { let mut to_return = vec![vec![String::new()]]; - to_return[0].extend(sorted_methods.iter().map(|m| m.name.to_string())); + to_return[0].extend(sorted_methods.iter().map(|m| m.name.clone())); to_return[0].push(String::from("Total")); @@ -171,7 +171,7 @@ impl TxViewGroup { } let changes_value = changes.get(&method_id).unwrap(); - to_insert_changes.push(changes_value.to_string()); + to_insert_changes.push(changes_value.clone()); let method_income = *income.get(&method_id).unwrap(); total_income += method_income; @@ -357,7 +357,7 @@ impl TxViewGroup { let mut to_return = vec![vec![String::new()]]; - to_return[0].extend(sorted_methods.iter().map(|m| m.name.to_string())); + to_return[0].extend(sorted_methods.iter().map(|m| m.name.clone())); to_return[0].push(String::from("Total")); @@ -472,7 +472,7 @@ impl TxViewGroup { to_insert_balance.push(format!("{:.2}", method_balance.dollar())); let changes_value = changes.get(&method_id).unwrap(); - to_insert_changes.push(changes_value.to_string()); + to_insert_changes.push(changes_value.clone()); } to_insert_balance.push(format!("{:.2}", total_balance.dollar())); diff --git a/tui/src/key_checker/key_handler.rs b/tui/src/key_checker/key_handler.rs index f489f40..185945d 100644 --- a/tui/src/key_checker/key_handler.rs +++ b/tui/src/key_checker/key_handler.rs @@ -2152,12 +2152,12 @@ impl InputKeyHandler<'_> { if self.summary_months.index > 0 { let month_value = self.summary_months.index; - previous_month = Some(self.summary_months.titles[month_value - 1].to_string()); + previous_month = Some(self.summary_months.titles[month_value - 1].clone()); previous_year = Some(self.summary_years.get_selected_value().to_string()); } else if self.summary_months.index == 0 && self.summary_years.index > 0 { - previous_month = Some(self.summary_months.titles[MONTHS.len() - 1].to_string()); + previous_month = Some(self.summary_months.titles[MONTHS.len() - 1].clone()); previous_year = - Some(self.summary_years.titles[self.summary_years.index - 1].to_string()); + Some(self.summary_years.titles[self.summary_years.index - 1].clone()); } } 1 => { @@ -2165,7 +2165,7 @@ impl InputKeyHandler<'_> { let year_value = self.summary_years.index; previous_month = Some(self.summary_months.get_selected_value().to_string()); - previous_year = Some(self.summary_years.titles[year_value - 1].to_string()); + previous_year = Some(self.summary_years.titles[year_value - 1].clone()); } } _ => {} diff --git a/tui/src/pages/activity_ui.rs b/tui/src/pages/activity_ui.rs index f538e85..4a68625 100644 --- a/tui/src/pages/activity_ui.rs +++ b/tui/src/pages/activity_ui.rs @@ -123,7 +123,7 @@ pub fn activity_ui( Cell::from(c.separate_with_commas()) } else { first_index_passed = true; - Cell::from(c.to_string()) + Cell::from(c.clone()) } }); Row::new(cells) diff --git a/tui/src/pages/popups/info.rs b/tui/src/pages/popups/info.rs index 1249c68..3a76316 100644 --- a/tui/src/pages/popups/info.rs +++ b/tui/src/pages/popups/info.rs @@ -47,11 +47,11 @@ impl InfoPopup { } InfoPopupState::Error(err) => { title = "Error"; - message = err.to_string(); + message = err.clone(); } InfoPopupState::ShowDetails(details) => { title = "Transaction Details"; - message = details.to_string(); + message = details.clone(); x_value = 40; y_value = 20; diff --git a/tui/src/pages/popups/models.rs b/tui/src/pages/popups/models.rs index 05ceaeb..66c7145 100644 --- a/tui/src/pages/popups/models.rs +++ b/tui/src/pages/popups/models.rs @@ -575,7 +575,7 @@ impl PopupType { if let Some(modifying) = &input.modifying_method { conn.rename_tx_method(modifying, &input.text)?; } else { - conn.add_new_methods(&vec![input.text.to_string()])?; + conn.add_new_methods(&vec![input.text.clone()])?; } Ok(true) diff --git a/tui/src/pages/search_ui.rs b/tui/src/pages/search_ui.rs index 0518865..7b71972 100644 --- a/tui/src/pages/search_ui.rs +++ b/tui/src/pages/search_ui.rs @@ -56,7 +56,7 @@ pub fn search_ui( let cells = item.iter().enumerate().map(|(index, c)| { let Ok(parsed_num) = c.parse::() else { if index == 0 { - return Cell::from(c.to_string()); + return Cell::from(c.clone()); } return Cell::from(c.separate_with_commas()); }; diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index aa940a3..627b4fe 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -205,7 +205,7 @@ pub fn summary_ui( .map(|(row_index, item)| { let cells = item.iter().enumerate().map(|(index, c)| { if index == 0 { - return Cell::from(c.to_string()); + return Cell::from(c.clone()); } let lerp_id = format!("summary_table_main:{index}:{row_index}"); diff --git a/tui/src/tx_handler/tx_data.rs b/tui/src/tx_handler/tx_data.rs index a7f1e60..9d8cf83 100644 --- a/tui/src/tx_handler/tx_data.rs +++ b/tui/src/tx_handler/tx_data.rs @@ -273,10 +273,10 @@ impl TxData { pub fn accept_autofill(&mut self, current_tab: &TxTab) { match current_tab { - TxTab::Details => self.details = self.autofill.to_string(), - TxTab::FromMethod => self.from_method = self.autofill.to_string(), - TxTab::ToMethod => self.to_method = self.autofill.to_string(), - TxTab::TxType => self.tx_type = self.autofill.to_string(), + TxTab::Details => self.details = self.autofill.clone(), + TxTab::FromMethod => self.from_method = self.autofill.clone(), + TxTab::ToMethod => self.to_method = self.autofill.clone(), + TxTab::TxType => self.tx_type = self.autofill.clone(), TxTab::Tags => { let mut split_tags = self.tags.split(',').map(str::trim).collect::>(); diff --git a/tui/src/utility/utils.rs b/tui/src/utility/utils.rs index b803314..887c972 100644 --- a/tui/src/utility/utils.rs +++ b/tui/src/utility/utils.rs @@ -90,7 +90,7 @@ pub fn styled_block_no_bottom<'a>(title: &'a str, theme: &'a Theme) -> Block<'a> } #[must_use] -pub fn main_block<'a>(theme: &'a Theme) -> Block<'a> { +pub fn main_block(theme: &Theme) -> Block<'_> { Block::default().style(Style::default().bg(theme.background()).fg(theme.border())) } From 50bba9879dd83fe5ad8b58e060ab2c1e6519548d Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sun, 9 Nov 2025 12:03:47 +0600 Subject: [PATCH 11/12] Fix tx type autofiller, stepper, verifier --- Cargo.lock | 9 +++-- Cargo.toml | 2 ++ app/Cargo.toml | 3 ++ app/src/ui_helper/autofiller.rs | 56 +++++++++++++++-------------- app/src/ui_helper/outputs.rs | 4 ++- app/src/ui_helper/stepper.rs | 37 +++++++++----------- app/src/ui_helper/verifier.rs | 58 ++++++++++++++++++++++--------- db/Cargo.toml | 2 ++ db/src/models/others.rs | 25 +++++-------- tui/Cargo.toml | 4 +-- tui/src/pages/popups/help_text.rs | 7 ++-- 11 files changed, 120 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4e6f3e..cb92454 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,9 +1396,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -1939,9 +1939,12 @@ dependencies = [ "anyhow", "chrono", "diesel", + "log", "rex-db", "rex-shared", "strsim", + "strum 0.27.2", + "strum_macros 0.27.2", "thiserror", ] @@ -1955,6 +1958,8 @@ dependencies = [ "diesel_migrations", "libsqlite3-sys", "rex-shared", + "strum 0.27.2", + "strum_macros 0.27.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0cf1e57..8fe0b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ diesel = { version = "2.3.3", default-features = false, features = [ "chrono", ] } anyhow = "1.0.100" +strum = "0.27" +strum_macros = "0.27" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/app/Cargo.toml b/app/Cargo.toml index 490bfe6..95d054c 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -15,6 +15,9 @@ rex-shared.workspace = true chrono.workspace = true anyhow.workspace = true diesel.workspace = true +strum.workspace = true +strum_macros.workspace = true +log = "0.4.28" strsim = "0.11.1" thiserror = "2.0.17" diff --git a/app/src/ui_helper/autofiller.rs b/app/src/ui_helper/autofiller.rs index 4e77a4e..b7dec77 100644 --- a/app/src/ui_helper/autofiller.rs +++ b/app/src/ui_helper/autofiller.rs @@ -1,5 +1,6 @@ use rex_db::ConnCache; use rex_db::models::TxType; +use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::get_best_match; @@ -8,8 +9,6 @@ pub struct Autofiller<'a> { conn: MutDbConn<'a>, } -pub(crate) const TX_TYPES: [&str; 3] = ["Income", "Expense", "Transfer"]; - impl<'a> Autofiller<'a> { pub(crate) fn new(conn: MutDbConn<'a>) -> Self { Self { conn } @@ -50,43 +49,48 @@ impl<'a> Autofiller<'a> { return String::new(); } - let tx_type = if lowercase.starts_with('t') { - TxType::Transfer - } else if lowercase.starts_with('e') { - TxType::Expense - } else if lowercase.starts_with('i') { - TxType::Income - } else if lowercase.starts_with("br") { - TxType::BorrowRepay - } else if lowercase.starts_with("lr") { - TxType::LendRepay - } else if lowercase.starts_with('b') { - TxType::Borrow - } else if lowercase.starts_with('l') { - TxType::Lend - } else { - let tx_types = TX_TYPES - .iter() - .map(|s| (*s).to_string()) + let return_best_match = || { + let tx_types = TxType::iter() + .map(|s| s.to_string()) .collect::>(); let best_match = get_best_match(user_input, &tx_types); - let to_return = if best_match == trimmed_input { + if best_match == trimmed_input { String::new() } else { best_match + } + }; + + let best_match = if lowercase.len() <= 2 { + let tx_type = if lowercase.starts_with('t') { + TxType::Transfer.to_string() + } else if lowercase.starts_with('e') { + TxType::Expense.to_string() + } else if lowercase.starts_with('i') { + TxType::Income.to_string() + } else if lowercase.starts_with("br") { + TxType::BorrowRepay.to_string() + } else if lowercase.starts_with("lr") { + TxType::LendRepay.to_string() + } else if lowercase.starts_with('b') { + TxType::Borrow.to_string() + } else if lowercase.starts_with('l') { + TxType::Lend.to_string() + } else { + return_best_match() }; - return to_return; + tx_type.to_string() + } else { + return_best_match() }; - let to_return = tx_type.to_string(); - - if to_return == trimmed_input { + if best_match == trimmed_input { String::new() } else { - to_return + best_match } } diff --git a/app/src/ui_helper/outputs.rs b/app/src/ui_helper/outputs.rs index e930e16..11e7b6b 100644 --- a/app/src/ui_helper/outputs.rs +++ b/app/src/ui_helper/outputs.rs @@ -49,7 +49,9 @@ pub enum VerifierError { AmountBelowZero, #[error("TX Method: Transaction Method not found")] InvalidTxMethod, - #[error("TX Type: Transaction Type not acceptable. Values: Expense/Income/E/I")] + #[error( + "TX Type: Transaction Type not acceptable. Values: Expense/Income/Transfer/Borrow/Borrow Repay/Lend/Lend Repay/I/E/T/B/BR/L/LR" + )] InvalidTxType, #[error("{0}: Error acquired while validating input")] ParsingError(Field), diff --git a/app/src/ui_helper/stepper.rs b/app/src/ui_helper/stepper.rs index 4e9c0d9..ad1f9ed 100644 --- a/app/src/ui_helper/stepper.rs +++ b/app/src/ui_helper/stepper.rs @@ -1,9 +1,11 @@ use chrono::{Duration, Months, NaiveDate}; use rex_db::ConnCache; +use rex_db::models::TxType; +use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::{ - DateType, Field, Output, StepType, SteppingError, TX_TYPES, VerifierError, get_best_match, + DateType, Field, Output, StepType, SteppingError, VerifierError, get_best_match, }; pub struct Stepper<'a> { @@ -184,27 +186,22 @@ impl<'a> Stepper<'a> { ) -> Result<(), SteppingError> { let verify_status = self.conn.verify().tx_type(user_type); - if !user_type.is_empty() { - let mut current_index: usize = - match user_type.chars().next().unwrap().to_ascii_lowercase() { - 'e' => 1, - 't' => 2, - // 'I' is 0 - _ => 0, - }; - - match step_type { - StepType::StepUp => current_index = (current_index + 1) % TX_TYPES.len(), - StepType::StepDown => { - current_index = (current_index + TX_TYPES.len() - 1) % TX_TYPES.len(); - } - } - *user_type = TX_TYPES[current_index].to_string(); - } - match verify_status { Ok(data) => match data { - Output::Accepted(_) => {} + Output::Accepted(_) => { + let tx_types: Vec = TxType::iter().map(|s| s.to_string()).collect(); + + let mut current_index = tx_types.iter().position(|t| t == user_type).unwrap(); + + match step_type { + StepType::StepUp => current_index = (current_index + 1) % tx_types.len(), + StepType::StepDown => { + current_index = (current_index + tx_types.len() - 1) % tx_types.len() + } + } + + *user_type = tx_types[current_index].to_string(); + } Output::Nothing(_) => { *user_type = "Income".to_string(); } diff --git a/app/src/ui_helper/verifier.rs b/app/src/ui_helper/verifier.rs index 64041f7..baadd3e 100644 --- a/app/src/ui_helper/verifier.rs +++ b/app/src/ui_helper/verifier.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use chrono::NaiveDate; use rex_db::ConnCache; use rex_db::models::TxType; +use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::{DateType, Field, Output, VerifierError, get_best_match}; @@ -440,27 +441,52 @@ impl<'a> Verifier<'a> { /// /// Auto expands E to Expense, I to Income and T to transfer. pub fn tx_type(&self, user_type: &mut String) -> Result { - *user_type = user_type.replace(' ', ""); + let trimmed_input = user_type.trim(); if user_type.is_empty() { return Ok(Output::Nothing(Field::TxType)); } - if user_type.to_lowercase().starts_with('e') { - *user_type = TxType::Expense.to_string(); - } else if user_type.to_lowercase().starts_with('i') { - *user_type = TxType::Income.to_string(); - } else if user_type.to_lowercase().starts_with('t') { - *user_type = TxType::Transfer.to_string(); - } else if user_type.to_lowercase().starts_with("br") { - *user_type = TxType::BorrowRepay.to_string(); - } else if user_type.to_lowercase().starts_with("lr") { - *user_type = TxType::LendRepay.to_string(); - } else if user_type.to_lowercase().starts_with('b') { - *user_type = TxType::Borrow.to_string(); - } else if user_type.to_lowercase().starts_with('l') { - *user_type = TxType::Lend.to_string(); + + let tx_types = TxType::iter() + .map(|s| s.to_string()) + .collect::>(); + + let return_best_match = || { + let best_match = get_best_match(user_type, &tx_types); + + if best_match == trimmed_input { + String::new() + } else { + best_match + } + }; + + let lowercase = user_type.to_lowercase(); + + if lowercase.len() <= 2 { + if lowercase.starts_with('e') { + *user_type = TxType::Expense.to_string(); + } else if lowercase.starts_with('i') { + *user_type = TxType::Income.to_string(); + } else if lowercase.starts_with('t') { + *user_type = TxType::Transfer.to_string(); + } else if lowercase.starts_with("br") { + *user_type = TxType::BorrowRepay.to_string(); + } else if lowercase.starts_with("lr") { + *user_type = TxType::LendRepay.to_string(); + } else if lowercase.starts_with('b') { + *user_type = TxType::Borrow.to_string(); + } else if lowercase.starts_with('l') { + *user_type = TxType::Lend.to_string(); + } else { + *user_type = return_best_match(); + return Err(VerifierError::InvalidTxType); + } } else { - *user_type = String::new(); + if tx_types.contains(user_type) { + return Ok(Output::Accepted(Field::TxType)); + } + *user_type = return_best_match(); return Err(VerifierError::InvalidTxType); } diff --git a/db/Cargo.toml b/db/Cargo.toml index 153ef57..1a15bca 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -14,5 +14,7 @@ rex-shared.workspace = true diesel.workspace = true chrono.workspace = true anyhow.workspace = true +strum.workspace = true +strum_macros.workspace = true diesel_migrations = "2.3.0" libsqlite3-sys = { version = "0.35.0", features = ["bundled"] } diff --git a/db/src/models/others.rs b/db/src/models/others.rs index 2696eba..c1dc767 100644 --- a/db/src/models/others.rs +++ b/db/src/models/others.rs @@ -1,15 +1,23 @@ use chrono::NaiveDateTime; use rex_shared::models::Cent; use std::fmt::{self, Display}; +use strum_macros::{Display, EnumIter}; -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, Display, EnumIter)] pub enum TxType { + #[strum(to_string = "Income")] Income, + #[strum(to_string = "Expense")] Expense, + #[strum(to_string = "Transfer")] Transfer, + #[strum(to_string = "Borrow")] Borrow, + #[strum(to_string = "Lend")] Lend, + #[strum(to_string = "Borrow Repay")] BorrowRepay, + #[strum(to_string = "Lend Repay")] LendRepay, } @@ -186,18 +194,3 @@ impl From<&str> for TxType { } } } - -impl Display for TxType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - TxType::Income => "Income", - TxType::Expense => "Expense", - TxType::Transfer => "Transfer", - TxType::Borrow => "Borrow", - TxType::Lend => "Lend", - TxType::BorrowRepay => "Borrow Repay", - TxType::LendRepay => "Lend Repay", - }; - write!(f, "{s}") - } -} diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 3986f0e..874f63e 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -25,6 +25,8 @@ rex-app.workspace = true rex-shared.workspace = true anyhow.workspace = true chrono.workspace = true +strum.workspace = true +strum_macros.workspace = true crossterm = "0.29.0" open = "5.3.2" atty = "0.2.14" @@ -35,8 +37,6 @@ thousands = "0.2.0" semver = "1.0.27" ratatui = "0.29.0" serde_json = "1.0.145" -strum = "0.27" -strum_macros = "0.27" rfd = "0.15.4" [lints.clippy] diff --git a/tui/src/pages/popups/help_text.rs b/tui/src/pages/popups/help_text.rs index 75a3972..f2eb692 100644 --- a/tui/src/pages/popups/help_text.rs +++ b/tui/src/pages/popups/help_text.rs @@ -27,6 +27,7 @@ On Transfer transaction there will be one additional field pushing Tags to the k 1: Date Example: 2022-05-12, YYYY-MM-DD 2: TX details Example: For Grocery, Salary 5: TX Type Example: Income/Expense/Transfer/I/E/T +5: New TX Type Example: Borrow/Borrow Repay/Lend/Lend Repay/b/br/l/lr 3: TX Method Example: Cash, Bank, Card 4: Amount Example: 1000, 100+50, b - 100 6: Tags Example: Food, Car. Add a Comma for a new tag @@ -88,9 +89,7 @@ Arrow Left/Right: Move value of the widget pub fn summary_help_text() -> String { format!( - "This page shows various information based on all transactions \ - and is for tracking incomes and expenses based on tags \ - Transfer Transaction are not shown here + "This page shows various information based on all transactions primary tag within a given period. Transfer Transaction are not shown here Following are the supported keys here @@ -116,7 +115,7 @@ pub fn home_help_text() -> String { format!( "This is the Home page where all txs added so far, the balances and the changes are shown -J: Take user input for various actions +J: Configuration E: Edit the selected transaction on the table D: Delete the selected transaction on the table ,: Swaps the location of the selected transaction with the transaction above it From 52f35c7a8722569b2f7db80879ccb648c9dff452 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Sun, 9 Nov 2025 12:06:02 +0600 Subject: [PATCH 12/12] Clippy fix --- app/src/ui_helper/autofiller.rs | 2 +- app/src/ui_helper/stepper.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/ui_helper/autofiller.rs b/app/src/ui_helper/autofiller.rs index b7dec77..6a6ee1a 100644 --- a/app/src/ui_helper/autofiller.rs +++ b/app/src/ui_helper/autofiller.rs @@ -82,7 +82,7 @@ impl<'a> Autofiller<'a> { return_best_match() }; - tx_type.to_string() + tx_type.clone() } else { return_best_match() }; diff --git a/app/src/ui_helper/stepper.rs b/app/src/ui_helper/stepper.rs index ad1f9ed..5c33bd2 100644 --- a/app/src/ui_helper/stepper.rs +++ b/app/src/ui_helper/stepper.rs @@ -196,11 +196,11 @@ impl<'a> Stepper<'a> { match step_type { StepType::StepUp => current_index = (current_index + 1) % tx_types.len(), StepType::StepDown => { - current_index = (current_index + tx_types.len() - 1) % tx_types.len() + current_index = (current_index + tx_types.len() - 1) % tx_types.len(); } } - *user_type = tx_types[current_index].to_string(); + *user_type = tx_types[current_index].clone(); } Output::Nothing(_) => { *user_type = "Income".to_string();