From 90e873c84ce7cd03a266e566a213693269c1de62 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Tue, 20 Oct 2026 14:57:19 +0600 Subject: [PATCH 1/4] Update snapshot --- .rot/snapshot.json | 576 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 453 insertions(+), 123 deletions(-) diff --git a/.rot/snapshot.json b/.rot/snapshot.json index 03c50bc..3e8bb34 100644 --- a/.rot/snapshot.json +++ b/.rot/snapshot.json @@ -1,7 +1,77 @@ { "version": 1, - "generated_at": 1790857477, + "generated_at": 1792486433, "entries": { + "app/src/conn.rs|crate::impl DbConn::encrypt_into#free:3ba4d670ddda": { + "kind": "Free", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::encrypt_into#free:3ba4d670ddda", + "comment_kind": "Line", + "comment_text": "// Reopen whichever file is in place so the connection is never left on the placeholder", + "comment_hash": "3ba4d670ddda93231446bf4ddeef4a770c79897c20a550adeaf1dfb991b71e43", + "body_text": "let key = swapped.is_ok().then_some(password);\nself.conn = open_connection(&self.location, key)\n .map_err(|e| anyhow!(\"Failed to reopen the database: {e}\"))?;\nswapped?;\nself.encrypted = true;\nOk(())\n}", + "body_hash": "7df0ae926a7afe1c4f92397b65b9170b08d9e91be67f26067a3d7efc7fb27f01" + }, + "app/src/conn.rs|crate::impl DbConn::encrypt_into#free:8814a4dc0b94": { + "kind": "Free", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::encrypt_into#free:8814a4dc0b94", + "comment_kind": "Line", + "comment_text": "// The files can't be swapped while either connection holds them open", + "comment_hash": "8814a4dc0b941907d997b0d69291baf2d6406d380f109cab0783bfc65853d14f", + "body_text": "drop(copy);\nlet placeholder = SqliteConnection::establish(\":memory:\")?;\ndrop(std::mem::replace(&mut self.conn, placeholder));\nlet swapped = std::fs::rename(temp_path, &self.location)\n .with_context(|| format!(\"Failed to replace {}\", self.location));", + "body_hash": "dc512ca4357c7e2c95b8465aa85241688e94dd1b0395a4bbc0d2d68bbe013c0e" + }, + "app/src/conn.rs|crate::impl DbConn::encrypt_into#free:d9e94c5b4200": { + "kind": "Free", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::encrypt_into#free:d9e94c5b4200", + "comment_kind": "Line", + "comment_text": "// Prove the copy can be opened with the password and holds everything before the\n// original is replaced", + "comment_hash": "d9e94c5b4200b941f5db183e9a7cfe23902e222a0a3abeed977527b46d3bed70", + "body_text": "let mut copy = open_connection(temp_path, Some(password))\n .map_err(|e| anyhow!(\"Encrypted copy could not be reopened: {e}\"))?;\nlet expected = table_row_counts(&mut self.conn)?;\nlet actual = table_row_counts(&mut copy)?;\nif expected != actual {}", + "body_hash": "1c3ea980c43897b6fcdc1934336322f42e3c0a7dc1795f76ce0904baea796730" + }, + "app/src/conn.rs|crate::impl DbConn::location": { + "kind": "Function", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::location", + "comment_kind": "OuterDoc", + "comment_text": " Path of the database file this connection is using\n", + "comment_hash": "94516d4694a8ff2e1d24003007a48b8bba288e3a40e0f20cceebc826b628da8c", + "body_text": "#[must_use]\npub fn location(&self) -> &str {\n &self.location\n }", + "body_hash": "d55a260e1197938b1dbd2465a0a1275e15021eb1a1b48818fd67310d06eb417a" + }, + "app/src/conn.rs|crate::impl DbConn::open": { + "kind": "Function", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::open", + "comment_kind": "OuterDoc", + "comment_text": " Opens the database, unlocking it with `key` if it is encrypted.\n", + "comment_hash": "b1307a5aa933e25bf1ffc6f8ef26af2204944e89d2b56a4ea04b0d4c5401d797", + "body_text": "pub fn open(db_url: &str, key: Option<&str>) -> Result {\n let conn = open_connection(db_url, key)?;\n\n let mut to_return = DbConn {\n conn,\n cache: Cache {\n tags: HashMap::new(),\n tx_methods: HashMap::new(),\n details: HashSet::new(),\n },\n location: db_url.to_string(),\n encrypted: key.is_some(),\n };\n\n to_return.reload_methods();\n to_return.reload_tags();\n to_return.reload_details();\n\n Ok(to_return)\n }", + "body_hash": "5d8cc94f63a26e5c9caf45f2ee9d9f7586ae07b6113c9cfd8cb9d8fe84499d3b" + }, + "app/src/conn.rs|crate::impl DbConn::set_password": { + "kind": "Function", + "file": "app/src/conn.rs", + "item_path": "crate::impl DbConn::set_password", + "comment_kind": "OuterDoc", + "comment_text": " Encrypts the database with `password`, or changes the password if it already is.\n\n\n\n An unencrypted database is copied into a new encrypted file, which is checked to hold\n\n the same rows before it replaces the original. Nothing is touched if any step fails.\n", + "comment_hash": "73c14f4d0329e2c0ed96a6067d0f8bdf89badc0e1ce8c57d4dee98a9f6eddd88", + "body_text": "pub fn set_password(&mut self, password: &str) -> Result<()> {\n if password.is_empty() {\n return Err(anyhow!(\"Password cannot be empty\"));\n }\n\n if self.encrypted {\n change_key(&mut self.conn, password)?;\n return Ok(());\n }\n\n let temp_path = format!(\"{}.encrypting\", self.location);\n remove_file_if_exists(&temp_path)?;\n\n let result = self.encrypt_into(&temp_path, password);\n\n if result.is_err() {\n let _ = std::fs::remove_file(&temp_path);\n }\n\n result\n }", + "body_hash": "2ab960ededdac2d1e1560f788bf05e1220be150c98cf1e57f044637387b98d28" + }, + "app/src/conn.rs|crate::table_row_counts": { + "kind": "Function", + "file": "app/src/conn.rs", + "item_path": "crate::table_row_counts", + "comment_kind": "OuterDoc", + "comment_text": " Row count of every table, to compare two databases that should hold the same data\n", + "comment_hash": "d7a8f21c1d8c1d3dce9d2dbcb59825f8c59da37372518d55940ca71a09238b80", + "body_text": "fn table_row_counts(conn: &mut SqliteConnection) -> Result> {\n #[derive(QueryableByName)]\n struct TableName {\n #[diesel(sql_type = Text)]\n name: String,\n }\n\n #[derive(QueryableByName)]\n struct Count {\n #[diesel(sql_type = BigInt)]\n count: i64,\n }\n\n let tables: Vec = sql_query(\n \"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' \\\n ORDER BY name\",\n )\n .load(conn)?;\n\n let mut counts = Vec::new();\n\n for table in tables {\n let count: Count = sql_query(format!(\"SELECT COUNT(*) AS count FROM \\\"{}\\\"\", table.name))\n .get_result(conn)?;\n counts.push((table.name, count.count));\n }\n\n Ok(counts)\n}", + "body_hash": "4de2b447b8fa3dd6ba0aed220b190ad1f69a06c20c9c79e3707b382c54ca5f64" + }, "app/src/migration/v1.rs|crate::migrate_to_v1": { "kind": "Function", "file": "app/src/migration/v1.rs", @@ -562,6 +632,16 @@ "body_text": "pub struct SummaryView {\n txs: Vec,\n nature: FetchNature,\n}", "body_hash": "939ef9e02744f323ee199b3acf1b5a23acb7fc1367ebd8ffb066fd4abfc4dbb2" }, + "app/src/views/summary_view.rs|crate::impl SummaryView::generate_summary#free:66a3f77db986": { + "kind": "Free", + "file": "app/src/views/summary_view.rs", + "item_path": "crate::impl SummaryView::generate_summary#free:66a3f77db986", + "comment_kind": "Line", + "comment_text": "// Income and expense totals per month, keyed by year-month", + "comment_hash": "66a3f77db9866e3db5a427dc32c244a8367e7642dcac74c1e1116833b062e7dc", + "body_text": "let mut monthly_totals: BTreeMap = BTreeMap::new();\nlet mut outstanding_borrows = Cent::new(0);\nlet mut outstanding_lends = Cent::new(0);\nfor tx in &self.txs {}\ntotal_month_checked += 1;\nlet mut peak_earning = PeakMonthlyMovement::default();\nlet mut peak_expense = PeakMonthlyMovement::default();\nfor (date, earning, expense) in monthly_totals.values() {}\nlet (income_percentage, expense_percentage) =\n get_percentages(total_income.value() as f64, total_expense.value() as f64);\nlet mut average_income = if total_income == 0 {} else {};\nlet mut average_expense = if total_expense == 0 {} else {};\nlet mut method_data = Vec::new();\nfor (index, method) in conn.cache().get_methods().iter().enumerate() {}\nif let FetchNature::Monthly = self.nature {}\nlet mut net_mom_yoy_earning = None;\nlet mut net_mom_yoy_expense = None;\nlet mut mom_yoy_borrows = None;\nlet mut mom_yoy_lends = None;\nif let Some(last_summary) = last_summary\n && !no_mom_yoy\n {}\nif !no_mom_yoy && net_mom_yoy_expense.is_none() && net_mom_yoy_earning.is_none() {}\nif !no_mom_yoy && mom_yoy_borrows.is_none() && mom_yoy_lends.is_none() {}\nlet summary_net = SummaryNet::new(\n total_income.dollar(),\n total_expense.dollar(),\n average_income,\n average_expense,\n income_percentage,\n expense_percentage,\n net_mom_yoy_earning,\n net_mom_yoy_expense,\n );\nlet summary_largest = vec![\n SummaryLargest::new(\n LargestType::Earning,\n biggest_earning.method,\n biggest_earning.amount.dollar(),\n biggest_earning.date,\n ),\n SummaryLargest::new(\n LargestType::Expense,\n biggest_expense.method,\n biggest_expense.amount.dollar(),\n biggest_expense.date,\n ),\n ];\nlet summary_peak = vec![\n SummaryPeak::new(\n PeakType::Earning,\n peak_earning.amount.dollar(),\n peak_earning.date,\n ),\n SummaryPeak::new(\n PeakType::Expense,\n peak_expense.amount.dollar(),\n peak_expense.date,\n ),\n ];\nlet summary_lend_borrows = SummaryLendBorrows::new(\n outstanding_borrows.dollar(),\n outstanding_lends.dollar(),\n mom_yoy_borrows,\n mom_yoy_lends,\n );\nFullSummary {\n methods: method_data,\n net: summary_net,\n largest: summary_largest,\n peak: summary_peak,\n lend_borrows: summary_lend_borrows,\n }\n}", + "body_hash": "ab8513a380f7aa4a971992387f42ba79c4883e8850c7e073ac367b8313747a87" + }, "app/src/views/summary_view.rs|crate::impl SummaryView::generate_summary#free:ed3895a0acef": { "kind": "Free", "file": "app/src/views/summary_view.rs", @@ -572,6 +652,26 @@ "body_text": "let earning_percentage = if method_earning[&method.name].value() == 0 {} else {};\nlet expense_percentage = if method_expense[&method.name] == 0 {} else {};\nlet mut average_earning = if method_earning[&method.name] == 0 {} else {};\nlet mut average_expense = if method_expense[&method.name] == 0 {} else {};\nlet mut mom_yoy_earning = None;\nlet mut mom_yoy_expense = None;\nif let FetchNature::Monthly = self.nature {}\nif let Some(last_summary) = last_summary\n && !no_mom_yoy\n {}\nif !no_mom_yoy && mom_yoy_expense.is_none() && mom_yoy_earning.is_none() {}\nlet method_summary = SummaryMethods::new(\n method.name.clone(),\n method_earning[&method.name].dollar(),\n method_expense[&method.name].dollar(),\n earning_percentage,\n expense_percentage,\n average_earning,\n average_expense,\n mom_yoy_earning,\n mom_yoy_expense,\n );\nmethod_data.push(method_summary);\n}", "body_hash": "44ba4a6249b46f1cf78b278db83e55cb9f6399a3d821cd99e2bc0b7982f8ee3b" }, + "app/src/views/summary_view.rs|crate::impl SummaryView::get_tags_movement_map#free:a232756c6dbc": { + "kind": "Free", + "file": "app/src/views/summary_view.rs", + "item_path": "crate::impl SummaryView::get_tags_movement_map#free:a232756c6dbc", + "comment_kind": "Line", + "comment_text": "// First tag is always the primary tag", + "comment_hash": "a232756c6dbcfcb1af8d0baea3b30eeab733e43c1db4cb07c3b9b60b40e7342d", + "body_text": "let Some(tag) = tx.tags.first() else {};\nmatch tx.tx_type {\n TxType::Income => {}\n TxType::Expense => {}\n TxType::Transfer\n | TxType::Borrow\n | TxType::Lend\n | TxType::BorrowRepay\n | TxType::LendRepay => {}\n }\n}", + "body_hash": "f3c10384e4ca392fd367a12bd4c393999a47ea1e03e7ff71a8e1c9585cea1b61" + }, + "app/src/views/summary_view.rs|crate::impl SummaryView::get_tags_movement_map#free:ffd917e9dd57": { + "kind": "Free", + "file": "app/src/views/summary_view.rs", + "item_path": "crate::impl SummaryView::get_tags_movement_map#free:ffd917e9dd57", + "comment_kind": "Line", + "comment_text": "// Only the primary tag. In case in previous month tag x was secondary but in the current\n// month x is primary tag, the amount will become inaccurate and secondary tag becomes\n// counted. So do not count anything except the primary tag", + "comment_hash": "ffd917e9dd576a9a1b1ca2a6544fe98bf70b50164557b29e300afb5d632f4049", + "body_text": "for tx in &self.txs {}\n(income_tags, expense_tags)\n}", + "body_hash": "db1fff9e5ce806e12aa6988590705f785d07d1cac81b2b346c738300ea2a046e" + }, "app/src/views/tx_view.rs|crate::TxView::balance": { "kind": "Field", "file": "app/src/views/tx_view.rs", @@ -1072,6 +1172,16 @@ "body_text": "assert_eq!(tx_view.get_tx_balance(0)[&cash_id].value(), -50000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", "body_hash": "d95ff84ec5719d13af7dd0fb48531281e8ef440d8cc747155a47363ec599f419" }, + "app/tests/encryption.rs|crate::set_password_makes_the_file_unreadable_without_it#free:06af29ff4d8f": { + "kind": "Free", + "file": "app/tests/encryption.rs", + "item_path": "crate::set_password_makes_the_file_unreadable_without_it#free:06af29ff4d8f", + "comment_kind": "Line", + "comment_text": "// The live connection keeps working", + "comment_hash": "06af29ff4d8f93c014546d2fd4aab37923cefceae6f1845d28db3044fc743f05", + "body_text": "assert_eq!(tx_count(&mut db), 3);\ndrop(db);\nassert!(!starts_with_sqlite_header(file_name));\nassert!(matches!(\n DbConn::open(file_name, None),\n Err(OpenError::NotReadable)\n ));\nassert!(matches!(\n DbConn::open(file_name, Some(\"wrong\")),\n Err(OpenError::NotReadable)\n ));\nlet mut reopened = DbConn::open(file_name, Some(\"correct horse\")).unwrap();\nassert!(reopened.is_encrypted());\nassert_eq!(tx_count(&mut reopened), 3);\nassert_eq!(\n reopened\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.as_str())\n .collect::>(),\n vec![\"Cash\", \"Bank\", \"Other\"]\n );\ndrop(reopened);\ncleanup(file_name);\n}", + "body_hash": "c1165fc2332c19fcc5ed6955c75f29573787991b871198530d7b465bce1dec17" + }, "app/tests/migration_v1.rs|crate::migration_starts_from_the_oldest_tx_even_if_its_balance_rows_are_missing#free:b8e63819cea4": { "kind": "Free", "file": "app/tests/migration_v1.rs", @@ -1972,6 +2082,26 @@ "body_text": "assert_eq!(peak[0][1], \"02-2024\");", "body_hash": "9b2e86281185ab5d2ca14653aec13b698cc175f500475b7c3df00ad42ad33b27" }, + "app/tests/summary.rs|crate::summary_peak_does_not_accumulate_across_months#free:4306e7a0f640": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_does_not_accumulate_across_months#free:4306e7a0f640", + "comment_kind": "Line", + "comment_text": "// Jan is the true peak. Feb and Mar are each lower, but sum to more than Jan", + "comment_hash": "4306e7a0f6406598e82a4f625e983b4bd409c23f7fb15dbdd552f9fd487ff726", + "body_text": "for (date, income, expense) in [\n (\"2024-01-10\", \"1000.00\", \"800.00\"),\n (\"2024-02-10\", \"600.00\", \"500.00\"),\n (\"2024-03-10\", \"600.00\", \"500.00\"),\n ] {}\nlet summary_view = db_conn\n .get_summary_with_str(\"January\", \"2024\", FetchNature::Yearly)\n .unwrap();\nlet full = summary_view.generate_summary(None, &db_conn);\nlet peak = full.peak_array();\nassert_eq!(peak[0][1], \"01-2024\");\nassert_eq!(peak[0][2], \"1000.00\");\nassert_eq!(peak[1][1], \"01-2024\");\nassert_eq!(peak[1][2], \"800.00\");", + "body_hash": "b578c1b28b2e7bac19fc7c8fb223b15ffb4683887459292c23e1358cf44e4329" + }, + "app/tests/summary.rs|crate::summary_peak_does_not_accumulate_across_months#free:e0f75a783cdb": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_does_not_accumulate_across_months#free:e0f75a783cdb", + "comment_kind": "Line", + "comment_text": "// Same for all time", + "comment_hash": "e0f75a783cdbb632cfba8df745a7f8a25863669615d9e93b8d67dd89e9ef393d", + "body_text": "let summary_view = db_conn\n .get_summary_with_str(\"January\", \"2024\", FetchNature::All)\n .unwrap();\nlet peak = summary_view.generate_summary(None, &db_conn).peak_array();\nassert_eq!(peak[0][1], \"01-2024\");\nassert_eq!(peak[0][2], \"1000.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f99bec32ab8d6790f48ce1ca29a97947efae91f5508e1c8e7608d6b036506f30" + }, "app/tests/summary.rs|crate::summary_tags_array_groups_by_tag#free:3cd7abbda88b": { "kind": "Free", "file": "app/tests/summary.rs", @@ -2062,6 +2192,36 @@ "body_text": "assert!(work_row.len() > 6, \"Should have compare columns\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", "body_hash": "4065bd3ef4314b7c9fe7714dbe37c806dd6e7f6f32978e2b58318144469e47a8" }, + "app/tests/summary.rs|crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:1caeff02be79": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:1caeff02be79", + "comment_kind": "Line", + "comment_text": "// September: the same Work income as the primary tag in August", + "comment_hash": "1caeff02be79a4cb62c6064ae7d0c3c8e3d6dcb8cdf08a38c63043a1ea79be35", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-09-01\",\n \"C\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Income\",\n \"Work\",\n );\nlet sep = db_conn\n .get_summary_with_str(\"September\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet aug = db_conn\n .get_summary_with_str(\"August\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet tags = sep.tags_array(Some(&aug), &db_conn);\nlet work_row = tags.iter().find(|r| r[0] == \"Work\").unwrap();\nassert_eq!(work_row[1], \"500.00\");", + "body_hash": "aeae107984240caa239d4f1049cd864ce27b3fab8e1a1ffe37030dbab39dc3a6" + }, + "app/tests/summary.rs|crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:66bedb66b12a": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:66bedb66b12a", + "comment_kind": "Line", + "comment_text": "// August: Work primary on one tx, secondary on another", + "comment_hash": "66bedb66b12ab4a7af832d4d834149da8448d3cdf0912d5c99f9998efe3e23dd", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"A\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Income\",\n \"Work\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-08-02\",\n \"B\",\n \"Cash\",\n \"\",\n \"300.00\",\n \"Income\",\n \"Side, Work\",\n );", + "body_hash": "80f34caa6b56d310f548192e3d615374635e31d1b1083bfe3c051d4ceeffb90b" + }, + "app/tests/summary.rs|crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:bca3fd1e767d": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_compare_counts_primary_tag_only_on_both_sides#free:bca3fd1e767d", + "comment_kind": "Line", + "comment_text": "// 500 against 500, not against the 800 of every tx that merely mentions Work", + "comment_hash": "bca3fd1e767df0af9bfcaa5b59dd17e0070b380d580c82bc9377653cb4cd0ff8", + "body_text": "assert_eq!(work_row[5], \"↑0.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "d1ff16facdefbbcf7798e68b914244d21359308e2c40aa4bf51b8d9da6801d13" + }, "app/tests/summary.rs|crate::summary_with_last_summary_mom_comparison#free:409a9a62de5a": { "kind": "Free", "file": "app/tests/summary.rs", @@ -2892,6 +3052,76 @@ "body_text": "let mut s = \"x\".to_string();\nlet v = db_conn.verify();\nlet result = v.tx_type(&mut s);\nassert!(matches!(result, Err(VerifierError::InvalidTxType)));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", "body_hash": "25de05662893e827788dc54a95438a3183aad3944ba6e1c172619050f3e21220" }, + "db/src/lib.rs|crate::OpenError": { + "kind": "Enum", + "file": "db/src/lib.rs", + "item_path": "crate::OpenError", + "comment_kind": "OuterDoc", + "comment_text": " Why a database could not be opened\n", + "comment_hash": "3601b451eabee39cf26c9c1c0a20f9bb81ffff2f5d3822d8a631d7dcb096b97b", + "body_text": "#[derive(Debug, thiserror::Error)]\npub enum OpenError {\n #[error(\"Wrong password, or the file is not a database\")]\n NotReadable,\n #[error(transparent)]\n Other(#[from] anyhow::Error),\n}", + "body_hash": "28df61a0b2ab6fdad1a71ae80bc137a79f3113ad80b097f3e5db1f5714342d16" + }, + "db/src/lib.rs|crate::OpenError::NotReadable": { + "kind": "Variant", + "file": "db/src/lib.rs", + "item_path": "crate::OpenError::NotReadable", + "comment_kind": "OuterDoc", + "comment_text": " SQLite could not read the file. With an encrypted database this is the only signal a wrong\n\n or missing password gives, and the same goes for a plain database opened with a password\n", + "comment_hash": "5996f3b93e20eabfa91ddf31d4305653c52f061255901350b5acf257add5ad6e", + "body_text": "#[error(\"Wrong password, or the file is not a database\")]\nNotReadable", + "body_hash": "406f1f3edece6329c87d2bf46b0e414c61e8c34d68b045f786eba4753f219ce4" + }, + "db/src/lib.rs|crate::change_key": { + "kind": "Function", + "file": "db/src/lib.rs", + "item_path": "crate::change_key", + "comment_kind": "OuterDoc", + "comment_text": " Changes the key of an already unlocked encrypted database\n", + "comment_hash": "0f31d8bdc5479ad241d1940c97dd04ee85101e034b12c888f3c6e54c00dd9a6d", + "body_text": "pub fn change_key(conn: &mut SqliteConnection, new_key: &str) -> Result<()> {\n conn.batch_execute(&format!(\"PRAGMA rekey = {};\", sql_quote(new_key)))?;\n Ok(())\n}", + "body_hash": "ef1da6c8fbe370fafcf1eb6af497748d2efe3b7a00b57c2edeff3aa4049f27a9" + }, + "db/src/lib.rs|crate::export_encrypted": { + "kind": "Function", + "file": "db/src/lib.rs", + "item_path": "crate::export_encrypted", + "comment_kind": "OuterDoc", + "comment_text": " Copies the whole database into a new encrypted file at `target` using `key`\n", + "comment_hash": "079cbbd335b4cf1fe3e7906fb98cd6123ef21bf8ca6f4b5d8b29411f6171ef03", + "body_text": "pub fn export_encrypted(conn: &mut SqliteConnection, target: &str, key: &str) -> Result<()> {\n conn.batch_execute(&format!(\n \"ATTACH DATABASE {} AS encrypted KEY {};\n SELECT sqlcipher_export('encrypted');\n DETACH DATABASE encrypted;\",\n sql_quote(target),\n sql_quote(key)\n ))?;\n Ok(())\n}", + "body_hash": "f968fbee169986a482ad3f8ad35b2894454d8e5f5f085064d0badfc08fd4ce5f" + }, + "db/src/lib.rs|crate::open_connection": { + "kind": "Function", + "file": "db/src/lib.rs", + "item_path": "crate::open_connection", + "comment_kind": "OuterDoc", + "comment_text": " Opens the database, unlocking it with `key` if given, then runs pending migrations. A wrong\n\n key is reported as [`OpenError::NotReadable`] instead of a panic or a failing later query.\n", + "comment_hash": "777a46aaa7d5174bfeb25d0014b7af3843af723e37b9f6e970fc9afef92d2cc9", + "body_text": "pub fn open_connection(db_url: &str, key: Option<&str>) -> Result {\n let mut conn = SqliteConnection::establish(db_url).map_err(|e| OpenError::Other(e.into()))?;\n\n if let Some(key) = key {\n conn.batch_execute(&format!(\"PRAGMA key = {};\", sql_quote(key)))\n .map_err(|e| OpenError::Other(e.into()))?;\n }\n\n \n if conn\n .batch_execute(\"SELECT count(*) FROM sqlite_master;\")\n .is_err()\n {\n return Err(OpenError::NotReadable);\n }\n\n conn.run_pending_migrations(MIGRATIONS)\n .map_err(|e| OpenError::Other(anyhow!(\"Failed to run database migrations: {e}\")))?;\n\n conn.batch_execute(\"PRAGMA foreign_keys = ON;\")\n .map_err(|e| OpenError::Other(e.into()))?;\n\n Ok(conn)\n}", + "body_hash": "06bc5ea0de7b2fcdfa5e3df042f233ea44ef84f4444b73c893ed5a73ed59a189" + }, + "db/src/lib.rs|crate::open_connection#free:d5518df0626e": { + "kind": "Free", + "file": "db/src/lib.rs", + "item_path": "crate::open_connection#free:d5518df0626e", + "comment_kind": "Line", + "comment_text": "// Opening never reads the file, so a wrong key only shows on the first real query", + "comment_hash": "d5518df0626ebdd1fa3788691570bc738332162d4d49d843b31f492e0988ae09", + "body_text": "if conn\n .batch_execute(\"SELECT count(*) FROM sqlite_master;\")\n .is_err()\n {}\nconn.run_pending_migrations(MIGRATIONS)\n .map_err(|e| OpenError::Other(anyhow!(\"Failed to run database migrations: {e}\")))?;\nconn.batch_execute(\"PRAGMA foreign_keys = ON;\")\n .map_err(|e| OpenError::Other(e.into()))?;\nOk(conn)\n}", + "body_hash": "a1641ccc756487a35f2495e25dd0548973d663105d53b9f6855463254df7b23d" + }, + "db/src/lib.rs|crate::sql_quote": { + "kind": "Function", + "file": "db/src/lib.rs", + "item_path": "crate::sql_quote", + "comment_kind": "OuterDoc", + "comment_text": " Quotes text for use inside a SQL string literal\n", + "comment_hash": "4944b8f65c018536ef388a424ec64caf4333e75754f8d300a8eda75c7f7d9aa3", + "body_text": "fn sql_quote(text: &str) -> String {\n format!(\"'{}'\", text.replace('\\'', \"''\"))\n}", + "body_hash": "fee4074bc37c25aac82b9e8ea320bff7cf2ebb12b386fe4909161b8ce0106e81" + }, "db/src/models/balances.rs|crate::impl Balance::get_last_balance": { "kind": "Function", "file": "db/src/models/balances.rs", @@ -2932,16 +3162,6 @@ "body_text": "pub fn get_due(today: NaiveDate, db_conn: &mut impl ConnCache) -> Result, Error> {\n use crate::schema::recurring_txs::dsl::{is_paused, next_recurring_date, recurring_txs};\n\n recurring_txs\n .filter(is_paused.eq(false))\n .filter(next_recurring_date.le(today))\n .select(Self::as_select())\n .load(db_conn.conn())\n }", "body_hash": "787c2ec8f831e3de78844ad9c2504bebb3b04439cf18b781f713e93b4423a779" }, - "db/src/models/txs.rs|crate::impl Tx::get_highest_date": { - "kind": "Function", - "file": "db/src/models/txs.rs", - "item_path": "crate::impl Tx::get_highest_date", - "comment_kind": "OuterDoc", - "comment_text": " The date of the newest transaction, or `None` if there are no transactions at all\n", - "comment_hash": "3c34fd7e74ff9753ec8d4e41bf9864ef8ec6cd42ed3feae0990cd706977ecfc9", - "body_text": "pub fn get_highest_date(db_conn: &mut impl ConnCache) -> Result, Error> {\n use crate::schema::txs::dsl::{date, txs};\n\n txs.select(max(date)).first(db_conn.conn())\n }", - "body_hash": "c56c372b84855fb970ed903cb8bdc21a82ed8d477bb8aab25153748ef5e98f95" - }, "db/src/models/txs.rs|crate::impl Tx::get_lowest_date": { "kind": "Function", "file": "db/src/models/txs.rs", @@ -2969,8 +3189,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Add Tx page and calls relevant function based on it\n", "comment_hash": "db878b8656e29f8567d1a16a6dfb2b14da231194e61d500369b37db93fa96cb1", - "body_text": "pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.add_tx_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.add_tx()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.add_tx_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", - "body_hash": "4673eaba3fddc3abcdaa5a6db2abb719d57356a075b43bf1a08612adcb8e9994" + "body_text": "pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.add_tx_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.add_tx()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.add_tx_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "1a057d41e223fa2d111f926b37ce51af268ea229984ea8f2784168f5db96e9d0" }, "tui/src/key_checker/chart.rs|crate::chart_keys": { "kind": "Function", @@ -2979,8 +3199,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Chart page and calls relevant function based on it\n", "comment_hash": "5658c0f775955e077532f8b44bc75fbd81512256c3892bab9f509252b8cfe068", - "body_text": "pub fn chart_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('r') => handler.do_chart_hidden_mode(),\n KeyCode::Char('R') => handler.do_chart_lgeneds(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Char(' ') => handler.switch_chart_tx_method_activation()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", - "body_hash": "62fda1f31201f0fd5eaf1f2f4ddd3495a274282d2b017dc43a93d7ec1f2f782f" + "body_text": "pub fn chart_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('r') => handler.do_chart_hidden_mode(),\n KeyCode::Char('R') => handler.do_chart_lgeneds(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Char(' ') => handler.switch_chart_tx_method_activation()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", + "body_hash": "7e1db2f1a4fcdc6c1f4751ec086052f448100806c24b4fada96655f253c5d378" }, "tui/src/key_checker/home.rs|crate::home_keys": { "kind": "Function", @@ -2989,8 +3209,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Homepage and calls relevant function based on it\n", "comment_hash": "3dd962886c4f7cf87fc262fdee1d3e1c44ad6ac90dbc9e00fb3552a4ae38b6f4", - "body_text": "pub fn home_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('e') => handler.home_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char(',') => handler.switch_tx_position_up()?,\n KeyCode::Char('.') => handler.switch_tx_position_down()?,\n KeyCode::Char('v') => handler.show_home_tx_details(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", - "body_hash": "09d3c0dfd4442cdc26a60b49fa3aaf8dd93fcf688b3bfae3435980bce2caa119" + "body_text": "pub fn home_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('e') => handler.home_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char(',') => handler.switch_tx_position_up()?,\n KeyCode::Char('.') => handler.switch_tx_position_down()?,\n KeyCode::Char('v') => handler.show_home_tx_details(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", + "body_hash": "1c8e12d698a96b9454bd0bf19eb914ef3182bc95598d4d0a3ab597bd3bb5d2e9" }, "tui/src/key_checker/initial.rs|crate::initial_keys": { "kind": "Function", @@ -3399,8 +3619,8 @@ "comment_kind": "OuterDoc", "comment_text": " Reload activity data by fetching from the DB\n", "comment_hash": "d4bddec368084f9bb722c73c58c765c5ecbd8a04ad08afb8189a48a6594ce42e", - "body_text": "fn reload_activity_table(&mut self) -> Result<()> {\n *self.activity_view = self.conn.get_activity_view_with_str(\n self.activity_months.get_selected_value(),\n self.activity_years.get_selected_value(),\n )?;\n\n let old_table_position = self.activity_table.state;\n\n *self.activity_table = TableData::new(self.activity_view.get_activity_table());\n self.activity_table.state = old_table_position;\n\n Ok(())\n }", - "body_hash": "fe49119d50c4073b0eac13cd0d9620db0f852ad38a2ff56be4b6d6485d0a8bf8" + "body_text": "fn reload_activity_table(&mut self) -> Result<()> {\n *self.activity_view = self.conn.get_activity_view_with_str(\n self.activity_months.get_selected_value(),\n self.activity_years.get_selected_value(),\n )?;\n\n let old_table_position = self.activity_table.state;\n\n *self.activity_table = TableData::new(self.activity_view.get_activity_table());\n self.activity_table.state = old_table_position;\n\n self.lerp_state.clear_lerp(ACTIVITY_TABLE_ID);\n\n Ok(())\n }", + "body_hash": "50049fb6a1ab0624cba43d710f382ffb084cd424425527b18d34342c87fc1e0e" }, "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_add_tx_balance_data": { "kind": "Function", @@ -3439,8 +3659,8 @@ "comment_kind": "OuterDoc", "comment_text": " Refetches the recurring transaction rules and rebuilds the Recurring page's table\n", "comment_hash": "29657154b9c8097f005930abd9cbbe26a33fb20d43bb255b64308cd87c7bb6a9", - "body_text": "fn reload_recurring_table(&mut self) -> Result<()> {\n *self.recurring_txs = self.conn.get_recurring_txs()?;\n\n let old_table_position = self.recurring_table.state;\n\n let items = self\n .recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect();\n *self.recurring_table = TableData::new(items);\n self.recurring_table.state = old_table_position;\n\n Ok(())\n }", - "body_hash": "88e2f3d6f9cf7978c4a719fe667ff2c2a60818090d9dc325d8ad0131ca4756ab" + "body_text": "fn reload_recurring_table(&mut self) -> Result<()> {\n *self.recurring_txs = self.conn.get_recurring_txs()?;\n\n let old_table_position = self.recurring_table.state;\n\n let items = self\n .recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect();\n *self.recurring_table = TableData::new(items);\n self.recurring_table.state = old_table_position;\n self.lerp_state.clear_lerp(RECURRING_TABLE_ID);\n\n Ok(())\n }", + "body_hash": "388a54b15320da04ca05939b9732a84c55242bbdcb3e6e1d34bdbd01341e40ee" }, "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_summary": { "kind": "Function", @@ -3449,8 +3669,8 @@ "comment_kind": "OuterDoc", "comment_text": " Reset summary table data by recreating it from gathered Summary Data\n", "comment_hash": "e452276c9d00f7e6968657eb87f20ebdb902f11136d23039536cf18cb9cfc8ef", - "body_text": "fn reload_summary(&mut self) -> Result<()> {\n let fetch_nature = match self.summary_modes.index {\n 0 => FetchNature::Monthly,\n 1 => FetchNature::Yearly,\n 2 => FetchNature::All,\n _ => panic!(\"Invalid summary mode\"),\n };\n\n let summary_view = self.conn.get_summary_with_str(\n self.summary_months.get_selected_value(),\n self.summary_years.get_selected_value(),\n fetch_nature,\n )?;\n\n *self.summary_sort = SortingType::Tags;\n\n let (previous_month, previous_year) = self.get_summary_previous_value();\n\n if let Some(month) = previous_month\n && let Some(year) = previous_year\n {\n let last_summary_view = self\n .conn\n .get_summary_with_str(&month, &year, fetch_nature)?;\n\n let last_full_summary = last_summary_view.generate_summary(None, self.conn);\n\n let mut summary_tags_table =\n summary_view.tags_array(Some(&last_summary_view), self.conn);\n\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(Some(&last_full_summary), self.conn);\n\n *self.summary_view = summary_view;\n } else {\n let mut summary_tags_table = summary_view.tags_array(None, self.conn);\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(None, self.conn);\n\n *self.summary_view = summary_view;\n }\n\n Ok(())\n }", - "body_hash": "7b102d70a8d60c64a522bcfc16a34797eb342f2da82b39b13e73bcc30fd3ac41" + "body_text": "fn reload_summary(&mut self) -> Result<()> {\n let fetch_nature = match self.summary_modes.index {\n 0 => FetchNature::Monthly,\n 1 => FetchNature::Yearly,\n 2 => FetchNature::All,\n _ => panic!(\"Invalid summary mode\"),\n };\n\n let summary_view = self.conn.get_summary_with_str(\n self.summary_months.get_selected_value(),\n self.summary_years.get_selected_value(),\n fetch_nature,\n )?;\n\n *self.summary_sort = SortingType::Tags;\n\n let (previous_month, previous_year) = self.get_summary_previous_value();\n\n if let Some(month) = previous_month\n && let Some(year) = previous_year\n {\n let last_summary_view = self\n .conn\n .get_summary_with_str(&month, &year, fetch_nature)?;\n\n let last_full_summary = last_summary_view.generate_summary(None, self.conn);\n\n let mut summary_tags_table =\n summary_view.tags_array(Some(&last_summary_view), self.conn);\n\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(Some(&last_full_summary), self.conn);\n\n *self.summary_view = summary_view;\n } else {\n let mut summary_tags_table = summary_view.tags_array(None, self.conn);\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(None, self.conn);\n\n *self.summary_view = summary_view;\n }\n\n self.lerp_state.clear_lerp(SUMMARY_TABLE_ID);\n\n Ok(())\n }", + "body_hash": "6a3e9021c748e7d5a0b205d91a7496d4081ced7e6bfcfe7e7997a1185b0e6a7e" }, "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reset_search_data": { "kind": "Function", @@ -3729,8 +3949,8 @@ "comment_kind": "OuterDoc", "comment_text": " Handles right arrow key press for multiple pages\n", "comment_hash": "02e274679ed6d519c1e319ebc19c0798b003dd1b529f44da8d95d5a288348623", - "body_text": "pub fn handle_right_arrow(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::Home => match self.home_tab {\n HomeTab::Months => {\n self.home_months.next();\n self.reload_home_table(true)?;\n }\n HomeTab::Years => {\n self.home_years.next_yearly();\n self.home_months.set_index_zero();\n self.reload_home_table(true)?;\n }\n HomeTab::Table => {}\n },\n CurrentUi::AddTx => self.add_tx_data.move_index_right(self.add_tx_tab),\n CurrentUi::Search => {\n self.search_data.move_index_right(self.search_tab);\n self.lerp_state.clear_lerp(SEARCH_TABLE_ID);\n }\n CurrentUi::Recurring => {\n self.recurring_data.move_index_right(self.recurring_tab);\n self.lerp_state.clear_lerp(RECURRING_TABLE_ID);\n }\n CurrentUi::Chart => {\n if !*self.chart_hidden_mode {\n match self.chart_tab {\n ChartTab::ModeSelection => {\n self.lerp_state.clear();\n self.chart_modes.next();\n self.reload_chart_data()?;\n }\n ChartTab::Years => {\n self.lerp_state.clear();\n self.chart_years.next_yearly();\n self.chart_months.set_index_zero();\n self.reload_chart_data()?;\n }\n ChartTab::Months => {\n self.lerp_state.clear();\n self.chart_months.next();\n self.reload_chart_data()?;\n }\n ChartTab::TxMethods => {\n self.chart_tx_methods.next();\n }\n }\n }\n }\n CurrentUi::Summary => {\n if !*self.summary_hidden_mode {\n match self.summary_tab {\n SummaryTab::ModeSelection => {\n self.summary_modes.next();\n self.reload_summary()?;\n }\n SummaryTab::Years => {\n self.summary_months.set_index_zero();\n self.summary_years.next_yearly();\n self.reload_summary()?;\n }\n SummaryTab::Months => {\n self.summary_months.next();\n self.reload_summary()?;\n }\n SummaryTab::Table => {}\n }\n\n self.lerp_state.clear_lerp(SUMMARY_TABLE_ID);\n }\n }\n CurrentUi::Activity => {\n match self.activity_tab {\n ActivityTab::Years => {\n self.activity_months.set_index_zero();\n self.activity_years.next_yearly();\n self.reload_activity_table()?;\n }\n ActivityTab::Months => {\n self.activity_months.next();\n self.reload_activity_table()?;\n }\n ActivityTab::List => {}\n }\n\n self.lerp_state.clear_lerp(ACTIVITY_TABLE_ID);\n }\n CurrentUi::Initial => {}\n }\n\n Ok(())\n }", - "body_hash": "f46da98c19367c0a8a30f887f57e9fc4a359b770eb540000233b9187c628b51b" + "body_text": "pub fn handle_right_arrow(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::Home => match self.home_tab {\n HomeTab::Months => {\n self.home_months.next();\n self.reload_home_table(true)?;\n }\n HomeTab::Years => {\n self.home_years.next_yearly();\n self.home_months.set_index_zero();\n self.reload_home_table(true)?;\n }\n HomeTab::Table => {}\n },\n CurrentUi::AddTx => self.add_tx_data.move_index_right(self.add_tx_tab),\n CurrentUi::Search => {\n self.search_data.move_index_right(self.search_tab);\n }\n CurrentUi::Recurring => {\n self.recurring_data.move_index_right(self.recurring_tab);\n }\n CurrentUi::Chart => {\n if !*self.chart_hidden_mode {\n match self.chart_tab {\n ChartTab::ModeSelection => {\n self.lerp_state.clear();\n self.chart_modes.next();\n self.reload_chart_data()?;\n }\n ChartTab::Years => {\n self.lerp_state.clear();\n self.chart_years.next_yearly();\n self.chart_months.set_index_zero();\n self.reload_chart_data()?;\n }\n ChartTab::Months => {\n self.lerp_state.clear();\n self.chart_months.next();\n self.reload_chart_data()?;\n }\n ChartTab::TxMethods => {\n self.chart_tx_methods.next();\n }\n }\n }\n }\n CurrentUi::Summary => {\n if !*self.summary_hidden_mode {\n match self.summary_tab {\n SummaryTab::ModeSelection => {\n self.summary_modes.next();\n self.reload_summary()?;\n }\n SummaryTab::Years => {\n self.summary_months.set_index_zero();\n self.summary_years.next_yearly();\n self.reload_summary()?;\n }\n SummaryTab::Months => {\n self.summary_months.next();\n self.reload_summary()?;\n }\n SummaryTab::Table => {}\n }\n }\n }\n CurrentUi::Activity => match self.activity_tab {\n ActivityTab::Years => {\n self.activity_months.set_index_zero();\n self.activity_years.next_yearly();\n self.reload_activity_table()?;\n }\n ActivityTab::Months => {\n self.activity_months.next();\n self.reload_activity_table()?;\n }\n ActivityTab::List => {}\n },\n CurrentUi::Initial => {}\n }\n\n Ok(())\n }", + "body_hash": "ed74fa4cf977d9b90e2bd71ab463df95bdcba1008e285328273d364323fbdc0f" }, "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_tags": { "kind": "Function", @@ -3842,6 +4062,26 @@ "body_text": "pub fn recurring_toggle_pause(&mut self) -> Result<()> {\n if let Some(index) = self.recurring_table.state.selected() {\n let target = &self.recurring_txs[index];\n let new_paused = !target.is_paused;\n let id = target.id;\n\n self.conn.set_recurring_paused(id, new_paused)?;\n self.reload_recurring_table()?;\n }\n\n Ok(())\n }", "body_hash": "9fe0837664f13b1e2c182265c8fe2d147c295dc3bf631785b276852395b1834d" }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::save_password": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::save_password", + "comment_kind": "OuterDoc", + "comment_text": " Encrypts the database with the password, or changes it if there already is one\n", + "comment_hash": "84ab2f5a2e292ae48ab058894323a4261d1c8beef4f00c0dee22d3f4e5e42466", + "body_text": "fn save_password(&mut self, password: &str) -> Result<()> {\n let changing = self.conn.is_encrypted();\n\n if let Err(e) = self.conn.set_password(password) {\n self.popup_status.password_failed(&e.to_string());\n return Ok(());\n }\n\n self.config.set_db_encrypted(true)?;\n\n \n \n self.config\n .save_backup(&PathBuf::from(self.conn.location()));\n\n let notice = if changing {\n \"Password changed. The new password will be asked the next time Rex starts\"\n } else {\n \"Database encrypted. The password will be asked every time Rex starts. A forgotten password cannot be recovered\"\n };\n\n *self.popup_status = PopupType::new_info(InfoPopupState::Notice(notice.to_string()));\n\n Ok(())\n }", + "body_hash": "df1aaa5786201c90a98b6a1761deb60744eb2515ffd9119cead8ee9e37a8b32f" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::save_password#free:062a652d6a46": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::save_password#free:062a652d6a46", + "comment_kind": "Line", + "comment_text": "// Backups and the default location hold copies from before the password existed, so\n// replace them right away instead of waiting for the app to close", + "comment_hash": "062a652d6a4696ca44096846691aa15c851072b7247e0e9261c223d7be9fd463", + "body_text": "self.config\n .save_backup(&PathBuf::from(self.conn.location()));\nlet notice = if changing {} else {};\n*self.popup_status = PopupType::new_info(InfoPopupState::Notice(notice.to_string()));\nOk(())\n}", + "body_hash": "c185e270c7c63f21c65ffb42f5d07e4c2816185f64fdbf22c787287d8c34fea6" + }, "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::save_recurring_tx": { "kind": "Function", "file": "tui/src/key_checker/key_handler.rs", @@ -3952,35 +4192,15 @@ "body_text": "if self.home_table.items.len() <= 1 || index == 0 {}\nlet reload_stuff = self\n .conn\n .swap_tx_position(index, index - 1, self.home_txs)?;\nif reload_stuff {}\n}", "body_hash": "7e8247d5c31a420e0f987e4b1f7382f7a55ca06d7433006e2f72a24dfae9ff8e" }, - "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable#free:251b69c070ca": { - "kind": "Free", - "file": "tui/src/key_checker/key_handler.rs", - "item_path": "crate::number_jump_tests::income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable#free:251b69c070ca", - "comment_kind": "Line", - "comment_text": "// No 10th field in this combination, so key 0 has nothing to jump to", - "comment_hash": "251b69c070ca554881da214c9f17bb088a9a223c16fd3700d5211161e50943f0", - "body_text": "assert!(tabs.get(digit_to_tab_index(0)).is_none());\n}", - "body_hash": "121bd454df481cb285b519788f00367fd92668647cee0d4ddcd3402bccd9fe31" - }, - "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::income_expense_yearly_has_eleven_fields_tags_has_no_key_left#free:bd7a09583570": { - "kind": "Free", - "file": "tui/src/key_checker/key_handler.rs", - "item_path": "crate::number_jump_tests::income_expense_yearly_has_eleven_fields_tags_has_no_key_left#free:bd7a09583570", - "comment_kind": "Line", - "comment_text": "// Tags is the 11th field - out of digits (1-9 then 0 only covers 10)", - "comment_hash": "bd7a09583570793270f664ef53fe3fad141e417c9b7b9033463187d98b5b3cc9", - "body_text": "assert!(tabs.get(10).is_some());\nassert_eq!(tabs[10], TxTab::Tags);\n}", - "body_hash": "71d2597d81e2b5d65a3e41ea13edb030a7191d7067ca128673b7d1280145a6f8" - }, - "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left#free:f45b37ddbf57": { - "kind": "Free", + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::toggle_privacy": { + "kind": "Function", "file": "tui/src/key_checker/key_handler.rs", - "item_path": "crate::number_jump_tests::transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left#free:f45b37ddbf57", - "comment_kind": "Line", - "comment_text": "// End Date and Tags are the 11th/12th fields here - genuinely out of digits,\n// only reachable via sequential Enter in this one combination", - "comment_hash": "f45b37ddbf5729d5b03dc2ebd3f0be97f6d2114b55dc49f97d6989107de387a1", - "body_text": "assert_eq!(tabs[10], TxTab::EndDate);\nassert_eq!(tabs[11], TxTab::Tags);\n}", - "body_hash": "35d304c7e6f385ff28a1e2de9f7cf50dae3703c18477654fc032ded33ecbc647" + "item_path": "crate::impl InputKeyHandler<'a>::toggle_privacy", + "comment_kind": "OuterDoc", + "comment_text": " Turns privacy mode on or off and saves it so it carries over to the next launch\n", + "comment_hash": "3d1d9068f54ba9e50a225a4ac22f98a40a22ce63889aa4e1e64ff28e4e57b3ab", + "body_text": "pub fn toggle_privacy(&mut self) -> Result<()> {\n let enabled = !self.config.privacy_mode.unwrap_or(false);\n self.config.set_privacy_mode(enabled)\n }", + "body_hash": "23499b59b88dbbe8f5311df2c8b5c229c301ee0d8b23769f9ae205b7308d796d" }, "tui/src/key_checker/recurring.rs|crate::recurring_keys": { "kind": "Function", @@ -3989,8 +4209,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Recurring Transactions page and calls relevant function based on it\n", "comment_hash": "c64636d0356bd322054412731c82c4966475fe7e08bc041c077b8541f4b4e094", - "body_text": "pub fn recurring_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.recurring_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.save_recurring_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('e') => handler.recurring_edit_selected(),\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('p') => handler.recurring_toggle_pause()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.recurring_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => handler.handle_recurring_field()?,\n TxTab::Nothing => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", - "body_hash": "58dfe1f15a6cbf8cc065456347234e693f3460c500606dbd504b95d5b2f8d3b8" + "body_text": "pub fn recurring_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.recurring_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.save_recurring_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('e') => handler.recurring_edit_selected(),\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('p') => handler.recurring_toggle_pause()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.recurring_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => handler.handle_recurring_field()?,\n TxTab::Nothing => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "3ba4019f84e9be77779a31de84641d874f08bb130b13581799bd7c2aa6a516c8" }, "tui/src/key_checker/search.rs|crate::search_keys": { "kind": "Function", @@ -3999,8 +4219,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Search page and calls relevant function based on it\n", "comment_hash": "5244faa2dc4cdf6c4551eb8a9cea60acac11b0d9a60628674ba785e727606cde", - "body_text": "pub fn search_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.search_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.search_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('x') => handler.change_search_date_type(),\n KeyCode::Char('e') => handler.search_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.search_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", - "body_hash": "3cb2016c8633d2b961a6ffd878d61b90543ac45572507fbd4042c12c8df90063" + "body_text": "pub fn search_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.search_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.search_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('x') => handler.change_search_date_type(),\n KeyCode::Char('e') => handler.search_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.search_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "808e9cc5d1c823dc757bdf3f3ac029f67282ca5e70e01bc3495ac0dc0b454caf" }, "tui/src/key_checker/summary.rs|crate::summary_keys": { "kind": "Function", @@ -4009,8 +4229,8 @@ "comment_kind": "OuterDoc", "comment_text": " Tracks the keys of the Summary page and calls relevant function based on it\n", "comment_hash": "3e6c02f8015d16674a3d5b3612ccdf543cfcb799749f905bc2a293481d0d6878", - "body_text": "pub fn summary_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.do_summary_hidden_mode(),\n KeyCode::Char('x') => handler.change_summary_sort(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.search_tag()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", - "body_hash": "9cd48930010c8e13751a4833d9d4e5cf86dc85d119155a1bc1677122b28b8cd2" + "body_text": "pub fn summary_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.do_summary_hidden_mode(),\n KeyCode::Char('x') => handler.change_summary_sort(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Char('P') => handler.toggle_privacy()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.search_tag()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "64d854b368c5f0bac2b7afe773a09a709b0e24e91f8a76eb5788105e229c6163" }, "tui/src/main.rs|crate::main#free:09c90db4bfe2": { "kind": "Free", @@ -4049,8 +4269,28 @@ "comment_kind": "OuterDoc", "comment_text": " Sets up a terminal, the migrated database and config plus a background\n\n version check, then runs the TUI loop until the user quits\n", "comment_hash": "9d5717332a7c7e4fe9b0d2ccf958816a18ee85a7f296b805030858e51db94474", - "body_text": "pub fn initialize_app(\n old_db_path: &PathBuf,\n migrated_db_path: &Path,\n original_dir: &PathBuf,\n) -> Result<()> {\n if !atty::is(Stream::Stdout) {\n if already_relaunched() || !start_terminal(original_dir) {\n let mut error_location = PathBuf::from(&original_dir);\n error_location.push(\"Error.txt\");\n\n let mut open = File::create(error_location)?;\n let to_write = \"Failed to start a terminal. Please open one manually\".to_string();\n open.write_all(to_write.as_bytes())?;\n process::exit(1);\n }\n\n process::exit(0);\n }\n\n let result =\n migrate_to_new_schema(old_db_path, migrated_db_path.display().to_string().as_str());\n\n match result {\n Ok(result) => {\n if result {\n println!(\"Restart required after migration. Exiting\");\n process::exit(0);\n }\n }\n Err(e) => {\n println!(\"Failed to migrate to new schema. Error: {e:?}\");\n fs::remove_file(migrated_db_path)?;\n process::exit(1);\n }\n }\n\n let new_update = Arc::new(Mutex::new(None));\n\n let update_clone = new_update.clone();\n\n spawn(move || {\n let new_version = check_version().unwrap_or_default().unwrap_or_default();\n\n let mut lock = update_clone.lock().unwrap();\n *lock = Some(new_version);\n });\n\n if let Err(e) = migrate_config(old_db_path) {\n println!(\"Failed to migrate config. Error: {e:?}\");\n process::exit(1);\n }\n\n let mut config = Config::get_config(&migrated_db_path.to_path_buf())?;\n\n let new_db_path = if let Some(mut location) = config.new_location.clone() {\n let result = set_current_dir(&location);\n\n if let Err(e) = result {\n println!(\n \"Failed to set the new path. Does the path exists? Exiting program. Error: {e}\"\n );\n process::exit(1);\n } else {\n location.push(\"rex.sqlite\");\n location.clone()\n }\n } else {\n migrated_db_path.to_path_buf()\n };\n\n let mut conn = get_conn(new_db_path.display().to_string().as_str());\n\n let v1_migration_done = config.migration_v1_done.unwrap_or(false);\n\n if !v1_migration_done {\n if let Err(e) = conn.initiate_v1_migration() {\n println!(\"Failed to perform balance v1 migration. Error: {e:?}\");\n process::exit(1);\n }\n\n config.set_migration_v1_done()?;\n }\n\n loop {\n let mut terminal = enter_tui_interface()?;\n let result = start_app(&mut terminal, new_update.clone(), &mut config, &mut conn);\n exit_tui_interface()?;\n\n match result {\n Ok(output) => match output {\n HandlingOutput::QuitUi => {\n drop(conn);\n config.save_backup(&new_db_path.clone());\n break;\n }\n HandlingOutput::PrintNewUpdate => println!(\n \"Could not open browser.\\n\\nLatest Version Link: https://github.com/TheRustyPickle/Rex/releases/latest\"\n ),\n },\n Err(error) => {\n println!(\"{error}\");\n process::exit(1);\n }\n }\n }\n\n Ok(())\n}", - "body_hash": "5327bce0b038858860eb93503d4469c8e2d6fcb17a22880f3ba153e9c0605b07" + "body_text": "pub fn initialize_app(\n old_db_path: &PathBuf,\n migrated_db_path: &Path,\n original_dir: &PathBuf,\n) -> Result<()> {\n if !atty::is(Stream::Stdout) {\n if already_relaunched() || !start_terminal(original_dir) {\n let mut error_location = PathBuf::from(&original_dir);\n error_location.push(\"Error.txt\");\n\n let mut open = File::create(error_location)?;\n let to_write = \"Failed to start a terminal. Please open one manually\".to_string();\n open.write_all(to_write.as_bytes())?;\n process::exit(1);\n }\n\n process::exit(0);\n }\n\n let result =\n migrate_to_new_schema(old_db_path, migrated_db_path.display().to_string().as_str());\n\n match result {\n Ok(result) => {\n if result {\n println!(\"Restart required after migration. Exiting\");\n process::exit(0);\n }\n }\n Err(e) => {\n println!(\"Failed to migrate to new schema. Error: {e:?}\");\n fs::remove_file(migrated_db_path)?;\n process::exit(1);\n }\n }\n\n let new_update = Arc::new(Mutex::new(None));\n\n let update_clone = new_update.clone();\n\n spawn(move || {\n let new_version = check_version().unwrap_or_default().unwrap_or_default();\n\n let mut lock = update_clone.lock().unwrap();\n *lock = Some(new_version);\n });\n\n if let Err(e) = migrate_config(old_db_path) {\n println!(\"Failed to migrate config. Error: {e:?}\");\n process::exit(1);\n }\n\n let mut config = Config::get_config(&migrated_db_path.to_path_buf())?;\n\n let new_db_path = if let Some(mut location) = config.new_location.clone() {\n let result = set_current_dir(&location);\n\n if let Err(e) = result {\n println!(\n \"Failed to set the new path. Does the path exists? Exiting program. Error: {e}\"\n );\n process::exit(1);\n } else {\n location.push(\"rex.sqlite\");\n location.clone()\n }\n } else {\n migrated_db_path.to_path_buf()\n };\n\n let Some(mut conn) = open_database(new_db_path.display().to_string().as_str(), &mut config)?\n else {\n \n return Ok(());\n };\n\n let v1_migration_done = config.migration_v1_done.unwrap_or(false);\n\n if !v1_migration_done {\n if let Err(e) = conn.initiate_v1_migration() {\n println!(\"Failed to perform balance v1 migration. Error: {e:?}\");\n process::exit(1);\n }\n\n config.set_migration_v1_done()?;\n }\n\n loop {\n let mut terminal = enter_tui_interface()?;\n let result = start_app(&mut terminal, new_update.clone(), &mut config, &mut conn);\n exit_tui_interface()?;\n\n match result {\n Ok(output) => match output {\n HandlingOutput::QuitUi => {\n drop(conn);\n config.save_backup(&new_db_path.clone());\n break;\n }\n HandlingOutput::PrintNewUpdate => println!(\n \"Could not open browser.\\n\\nLatest Version Link: https://github.com/TheRustyPickle/Rex/releases/latest\"\n ),\n },\n Err(error) => {\n println!(\"{error}\");\n process::exit(1);\n }\n }\n }\n\n Ok(())\n}", + "body_hash": "17cff280370a57ed144dc835727151221fbb4ba49de3e11b1244a2d6cda8713e" + }, + "tui/src/page_handler/initializer.rs|crate::initialize_app#free:2d537662084b": { + "kind": "Free", + "file": "tui/src/page_handler/initializer.rs", + "item_path": "crate::initialize_app#free:2d537662084b", + "comment_kind": "Line", + "comment_text": "// Quit from the password screen", + "comment_hash": "2d537662084b17afc1f4792a499f3ddb2a685dd6192ee2a8d342193460ba801d", + "body_text": "return Ok(());\n}", + "body_hash": "e8f683342a975edf8150d0828fe21003dfffcd52e96c4ccdbdaae28fd5eb4778" + }, + "tui/src/page_handler/initializer.rs|crate::open_database": { + "kind": "Function", + "file": "tui/src/page_handler/initializer.rs", + "item_path": "crate::open_database", + "comment_kind": "OuterDoc", + "comment_text": " Opens the database, asking for the password first if it is encrypted. Returns `None` if the\n\n user quits instead of entering one.\n", + "comment_hash": "eef67a8a19b4d9cb165cb23f6c3b600e4f06dd5cc664e298bbe3773dd8dc7f14", + "body_text": "fn open_database(path: &str, config: &mut Config) -> Result> {\n match DbConn::open(path, None) {\n Ok(conn) => {\n if config.db_encrypted.unwrap_or(false) {\n config.set_db_encrypted(false)?;\n }\n\n return Ok(Some(conn));\n }\n Err(OpenError::NotReadable) => {}\n Err(e) => return Err(e.into()),\n }\n\n let theme = Theme::new_index(config.theme_index.unwrap_or(0));\n let mut error = None;\n\n loop {\n let Some(password) = ask_password(&theme, error)? else {\n return Ok(None);\n };\n\n match DbConn::open(path, Some(&password)) {\n Ok(conn) => {\n if !config.db_encrypted.unwrap_or(false) {\n config.set_db_encrypted(true)?;\n }\n\n return Ok(Some(conn));\n }\n Err(OpenError::NotReadable) => error = Some(\"Wrong password\"),\n Err(e) => return Err(e.into()),\n }\n }\n}", + "body_hash": "f4076b54d67a97e5662052ec5992a4a40f1f7bccb59b65560d3a9d06b143d18d" }, "tui/src/page_handler/ui_handler.rs|crate::start_app": { "kind": "Function", @@ -4059,8 +4299,8 @@ "comment_kind": "OuterDoc", "comment_text": " Starts the interface and runs the app\n", "comment_hash": "3700e72df31d9454f3f9d6571ecf421e65469dcb570f26886c09ee4b004fae43", - "body_text": "pub fn start_app(\n terminal: &mut Terminal,\n new_version_data: Arc>>>,\n config: &mut Config,\n conn: &mut DbConn,\n) -> Result {\n \n let mut popup_status = PopupType::Nothing;\n\n let recurring_added = match conn.process_due_recurring_txs() {\n Ok(count) => count,\n Err(e) => {\n let state = InfoPopupState::Error(format!(\"Failed to process recurring TXs: {e}\"));\n popup_status = PopupType::new_info(state);\n 0\n }\n };\n\n let mut theme = Theme::new_index(config.theme_index.unwrap_or(0));\n\n \n \n \n let mut home_months = IndexedData::new_monthly();\n let mut home_years = IndexedData::new_yearly();\n let mut chart_months = IndexedData::new_monthly_no_local();\n let mut chart_years = IndexedData::new_yearly_no_local();\n let mut chart_modes = IndexedData::new_modes();\n let mut chart_tx_methods = IndexedData::new_tx_methods_cumulative(conn);\n let mut summary_months = IndexedData::new_monthly_no_local();\n let mut summary_years = IndexedData::new_yearly_no_local();\n let mut summary_modes = IndexedData::new_modes();\n let mut activity_months = IndexedData::new_monthly();\n let mut activity_years = IndexedData::new_yearly();\n\n \n let mut home_tab = HomeTab::Months;\n\n \n let mut summary_sort = SortingType::Tags;\n\n \n let mut search_txs = SearchView::new_empty();\n\n \n let mut home_txs = conn\n .fetch_txs_with_str(\n home_months.get_selected_value(),\n home_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut home_table = TableData::new(home_txs.tx_array());\n\n \n let mut page = CurrentUi::Initial;\n\n \n let mut add_tx_tab = TxTab::Nothing;\n \n let mut chart_tab = ChartTab::ModeSelection;\n \n let mut summary_tab = SummaryTab::ModeSelection;\n \n let mut search_tab = TxTab::Nothing;\n \n let mut search_date_type = DateType::Exact;\n \n let mut activity_tab = ActivityTab::Years;\n \n let mut recurring_tab = TxTab::Nothing;\n\n \n let mut add_tx_data = TxData::new();\n \n let mut search_data = TxData::new_empty();\n \n let mut recurring_data = TxData::new_empty();\n\n \n let mut chart_view = conn\n .get_chart_view_with_str(\n chart_months.get_selected_value(),\n chart_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut summary_view = conn\n .get_summary_with_str(\n summary_months.get_selected_value(),\n summary_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut activity_view = conn\n .get_activity_view_with_str(\n activity_months.get_selected_value(),\n activity_years.get_selected_value(),\n )\n .unwrap();\n\n \n let mut full_summary = summary_view.generate_summary(None, conn);\n\n \n let mut summary_table = TableData::new(summary_view.tags_array(None, conn));\n\n \n let mut search_table = TableData::new(Vec::new());\n\n \n let mut activity_table = TableData::new(activity_view.get_activity_table());\n\n \n let mut recurring_txs = conn.get_recurring_txs().unwrap();\n\n \n let initial_data = InitialData::load(conn, &recurring_txs, recurring_added);\n\n \n let mut recurring_table = TableData::new(\n recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect(),\n );\n\n \n let mut starter_index = 0;\n\n \n let mut chart_hidden_mode = false;\n\n \n let mut chart_hidden_legends = false;\n\n \n let mut summary_hidden_mode = false;\n\n \n let mut chart_activated_methods = conn\n .get_tx_methods_cumulative()\n .into_iter()\n .map(|s| (s, true))\n .collect();\n\n \n let mut add_tx_balance = Vec::new();\n\n let mut lerp_state = LerpState::new(1.0);\n\n let mut version_checked = false;\n\n \n \n \n \n \n \n \n loop {\n if !version_checked {\n let update_lock = new_version_data.lock().unwrap();\n if let Some(data) = update_lock.as_ref() {\n if !data.is_empty() {\n let state = InfoPopupState::NewUpdate(data.clone());\n popup_status = PopupType::new_info(state);\n }\n version_checked = true;\n }\n }\n \n if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {\n popup_status = PopupType::new_choice_config_forced(&theme);\n }\n\n \n terminal\n .draw(|f| {\n match page {\n CurrentUi::Home => home_ui(\n f,\n &home_months,\n &home_years,\n &mut home_table,\n &home_tab,\n &mut lerp_state,\n &mut home_txs,\n &theme,\n conn,\n ),\n\n CurrentUi::AddTx => add_tx_ui(\n f,\n &mut add_tx_balance,\n &add_tx_data,\n &add_tx_tab,\n &mut lerp_state,\n &theme,\n conn,\n ),\n\n CurrentUi::Initial => initial_ui(f, starter_index, &initial_data, &theme),\n\n CurrentUi::Chart => chart_ui(\n f,\n &chart_months,\n &chart_years,\n &chart_modes,\n &chart_tx_methods,\n &chart_tab,\n chart_hidden_mode,\n chart_hidden_legends,\n &chart_activated_methods,\n &mut lerp_state,\n &chart_view,\n &theme,\n conn,\n ),\n\n CurrentUi::Summary => summary_ui(\n f,\n &summary_months,\n &summary_years,\n &summary_modes,\n &mut summary_table,\n &summary_tab,\n summary_hidden_mode,\n &summary_sort,\n &mut lerp_state,\n &full_summary,\n &theme,\n conn,\n ),\n CurrentUi::Search => search_ui(\n f,\n &search_data,\n &search_tab,\n &mut search_table,\n search_date_type,\n &mut lerp_state,\n &theme,\n ),\n CurrentUi::Activity => activity_ui(\n f,\n &activity_months,\n &activity_years,\n &activity_tab,\n &activity_view,\n &mut activity_table,\n &mut lerp_state,\n &theme,\n ),\n CurrentUi::Recurring => recurring_ui(\n f,\n &recurring_data,\n &recurring_tab,\n &mut recurring_table,\n &mut lerp_state,\n &theme,\n ),\n }\n\n popup_status.show_ui(f, &theme);\n })\n .map_err(|e| {\n let e = e.to_string();\n UiHandlingError::Drawing(e)\n })?;\n\n \n match page {\n CurrentUi::Initial => {\n \n if !poll(Duration::from_millis(40)).map_err(UiHandlingError::Polling)? {\n starter_index = (starter_index + 1) % 27;\n continue;\n }\n }\n CurrentUi::Home\n | CurrentUi::AddTx\n | CurrentUi::Summary\n | CurrentUi::Search\n | CurrentUi::Chart\n | CurrentUi::Activity\n | CurrentUi::Recurring => {\n \n if lerp_state.has_active_lerps()\n && !poll(Duration::from_millis(2)).map_err(UiHandlingError::Polling)?\n {\n continue;\n }\n }\n }\n\n \n if let Event::Key(key) = event::read().map_err(UiHandlingError::Polling)? {\n if key.kind != KeyEventKind::Press {\n continue;\n }\n\n let mut handler = InputKeyHandler::new(\n key,\n &mut page,\n &mut add_tx_balance,\n &mut popup_status,\n &mut add_tx_tab,\n &mut chart_tab,\n &mut summary_tab,\n &mut home_tab,\n &mut add_tx_data,\n &mut chart_view,\n &mut summary_view,\n &mut full_summary,\n &mut home_table,\n &mut home_txs,\n &mut summary_table,\n &mut home_months,\n &mut home_years,\n &mut chart_months,\n &mut chart_years,\n &mut chart_modes,\n &mut chart_tx_methods,\n &mut summary_months,\n &mut summary_years,\n &mut summary_modes,\n &mut summary_sort,\n &mut search_data,\n &mut search_date_type,\n &mut search_tab,\n &mut search_table,\n &mut search_txs,\n &mut activity_months,\n &mut activity_years,\n &mut activity_tab,\n &mut activity_view,\n &mut activity_table,\n &mut recurring_data,\n &mut recurring_tab,\n &mut recurring_table,\n &mut recurring_txs,\n &mut chart_hidden_mode,\n &mut chart_hidden_legends,\n &mut summary_hidden_mode,\n &mut chart_activated_methods,\n &mut lerp_state,\n config,\n &mut theme,\n conn,\n );\n\n let status = match handler.page {\n CurrentUi::Initial => initial_keys(&mut handler),\n CurrentUi::Home => home_keys(&mut handler),\n CurrentUi::AddTx => add_tx_keys(&mut handler),\n CurrentUi::Chart => chart_keys(&mut handler),\n CurrentUi::Summary => summary_keys(&mut handler),\n CurrentUi::Search => search_keys(&mut handler),\n CurrentUi::Activity => activity_keys(&mut handler),\n CurrentUi::Recurring => recurring_keys(&mut handler),\n };\n\n match status {\n Ok(output) => {\n if let Some(output) = output {\n return Ok(output);\n }\n }\n Err(e) => {\n let state = InfoPopupState::Error(e.to_string());\n popup_status = PopupType::new_info(state);\n }\n }\n }\n }\n}", - "body_hash": "0d9840d0aa0af42dba353f6c9dbd2a426caeb9e26f8d55e11ed40a7ac0004a36" + "body_text": "pub fn start_app(\n terminal: &mut Terminal,\n new_version_data: Arc>>>,\n config: &mut Config,\n conn: &mut DbConn,\n) -> Result {\n \n let mut popup_status = PopupType::Nothing;\n\n let recurring_added = match conn.process_due_recurring_txs() {\n Ok(count) => count,\n Err(e) => {\n let state = InfoPopupState::Error(format!(\"Failed to process recurring TXs: {e}\"));\n popup_status = PopupType::new_info(state);\n 0\n }\n };\n\n let mut theme = Theme::new_index(config.theme_index.unwrap_or(0));\n\n \n \n \n let mut home_months = IndexedData::new_monthly();\n let mut home_years = IndexedData::new_yearly();\n let mut chart_months = IndexedData::new_monthly_no_local();\n let mut chart_years = IndexedData::new_yearly_no_local();\n let mut chart_modes = IndexedData::new_modes();\n let mut chart_tx_methods = IndexedData::new_tx_methods_cumulative(conn);\n let mut summary_months = IndexedData::new_monthly_no_local();\n let mut summary_years = IndexedData::new_yearly_no_local();\n let mut summary_modes = IndexedData::new_modes();\n let mut activity_months = IndexedData::new_monthly();\n let mut activity_years = IndexedData::new_yearly();\n\n \n let mut home_tab = HomeTab::Months;\n\n \n let mut summary_sort = SortingType::Tags;\n\n \n let mut search_txs = SearchView::new_empty();\n\n \n let mut home_txs = conn\n .fetch_txs_with_str(\n home_months.get_selected_value(),\n home_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut home_table = TableData::new(home_txs.tx_array());\n\n \n let mut page = CurrentUi::Initial;\n\n \n let mut add_tx_tab = TxTab::Nothing;\n \n let mut chart_tab = ChartTab::ModeSelection;\n \n let mut summary_tab = SummaryTab::ModeSelection;\n \n let mut search_tab = TxTab::Nothing;\n \n let mut search_date_type = DateType::Exact;\n \n let mut activity_tab = ActivityTab::Years;\n \n let mut recurring_tab = TxTab::Nothing;\n\n \n let mut add_tx_data = TxData::new();\n \n let mut search_data = TxData::new_empty();\n \n let mut recurring_data = TxData::new_empty();\n\n \n let mut chart_view = conn\n .get_chart_view_with_str(\n chart_months.get_selected_value(),\n chart_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut summary_view = conn\n .get_summary_with_str(\n summary_months.get_selected_value(),\n summary_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut activity_view = conn\n .get_activity_view_with_str(\n activity_months.get_selected_value(),\n activity_years.get_selected_value(),\n )\n .unwrap();\n\n \n let mut full_summary = summary_view.generate_summary(None, conn);\n\n \n let mut summary_table = TableData::new(summary_view.tags_array(None, conn));\n\n \n let mut search_table = TableData::new(Vec::new());\n\n \n let mut activity_table = TableData::new(activity_view.get_activity_table());\n\n \n let mut recurring_txs = conn.get_recurring_txs().unwrap();\n\n \n let initial_data = InitialData::load(conn, &recurring_txs, recurring_added);\n\n \n let mut recurring_table = TableData::new(\n recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect(),\n );\n\n \n let mut starter_index = 0;\n\n \n let mut chart_hidden_mode = false;\n\n \n let mut chart_hidden_legends = false;\n\n \n let mut summary_hidden_mode = false;\n\n \n let mut chart_activated_methods = conn\n .get_tx_methods_cumulative()\n .into_iter()\n .map(|s| (s, true))\n .collect();\n\n \n let mut add_tx_balance = Vec::new();\n\n let mut lerp_state = LerpState::new(1.0);\n\n let mut version_checked = false;\n\n \n \n \n \n \n \n \n loop {\n if !version_checked {\n let update_lock = new_version_data.lock().unwrap();\n if let Some(data) = update_lock.as_ref() {\n if !data.is_empty() {\n let state = InfoPopupState::NewUpdate(data.clone());\n popup_status = PopupType::new_info(state);\n }\n version_checked = true;\n }\n }\n \n if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {\n popup_status = PopupType::new_choice_config_forced(&theme);\n }\n\n let privacy = config.privacy_mode.unwrap_or(false);\n\n \n terminal\n .draw(|f| {\n match page {\n CurrentUi::Home => home_ui(\n f,\n &home_months,\n &home_years,\n &mut home_table,\n &home_tab,\n &mut lerp_state,\n &mut home_txs,\n &theme,\n privacy,\n conn,\n ),\n\n CurrentUi::AddTx => add_tx_ui(\n f,\n &mut add_tx_balance,\n &add_tx_data,\n &add_tx_tab,\n &mut lerp_state,\n &theme,\n privacy,\n conn,\n ),\n\n CurrentUi::Initial => initial_ui(f, starter_index, &initial_data, &theme),\n\n CurrentUi::Chart => chart_ui(\n f,\n &chart_months,\n &chart_years,\n &chart_modes,\n &chart_tx_methods,\n &chart_tab,\n chart_hidden_mode,\n chart_hidden_legends,\n &chart_activated_methods,\n &mut lerp_state,\n &chart_view,\n &theme,\n privacy,\n conn,\n ),\n\n CurrentUi::Summary => summary_ui(\n f,\n &summary_months,\n &summary_years,\n &summary_modes,\n &mut summary_table,\n &summary_tab,\n summary_hidden_mode,\n &summary_sort,\n &mut lerp_state,\n &full_summary,\n &theme,\n privacy,\n conn,\n ),\n CurrentUi::Search => search_ui(\n f,\n &search_data,\n &search_tab,\n &mut search_table,\n search_date_type,\n &mut lerp_state,\n &theme,\n privacy,\n ),\n CurrentUi::Activity => activity_ui(\n f,\n &activity_months,\n &activity_years,\n &activity_tab,\n &activity_view,\n &mut activity_table,\n &mut lerp_state,\n &theme,\n privacy,\n ),\n CurrentUi::Recurring => recurring_ui(\n f,\n &recurring_data,\n &recurring_tab,\n &mut recurring_table,\n &mut lerp_state,\n &theme,\n privacy,\n ),\n }\n\n popup_status.show_ui(f, &theme);\n })\n .map_err(|e| {\n let e = e.to_string();\n UiHandlingError::Drawing(e)\n })?;\n\n \n match page {\n CurrentUi::Initial => {\n \n if !poll(Duration::from_millis(40)).map_err(UiHandlingError::Polling)? {\n starter_index = (starter_index + 1) % 27;\n continue;\n }\n }\n CurrentUi::Home\n | CurrentUi::AddTx\n | CurrentUi::Summary\n | CurrentUi::Search\n | CurrentUi::Chart\n | CurrentUi::Activity\n | CurrentUi::Recurring => {\n \n if lerp_state.has_active_lerps()\n && !poll(Duration::from_millis(2)).map_err(UiHandlingError::Polling)?\n {\n continue;\n }\n }\n }\n\n \n if let Event::Key(key) = event::read().map_err(UiHandlingError::Polling)? {\n if key.kind != KeyEventKind::Press {\n continue;\n }\n\n let mut handler = InputKeyHandler::new(\n key,\n &mut page,\n &mut add_tx_balance,\n &mut popup_status,\n &mut add_tx_tab,\n &mut chart_tab,\n &mut summary_tab,\n &mut home_tab,\n &mut add_tx_data,\n &mut chart_view,\n &mut summary_view,\n &mut full_summary,\n &mut home_table,\n &mut home_txs,\n &mut summary_table,\n &mut home_months,\n &mut home_years,\n &mut chart_months,\n &mut chart_years,\n &mut chart_modes,\n &mut chart_tx_methods,\n &mut summary_months,\n &mut summary_years,\n &mut summary_modes,\n &mut summary_sort,\n &mut search_data,\n &mut search_date_type,\n &mut search_tab,\n &mut search_table,\n &mut search_txs,\n &mut activity_months,\n &mut activity_years,\n &mut activity_tab,\n &mut activity_view,\n &mut activity_table,\n &mut recurring_data,\n &mut recurring_tab,\n &mut recurring_table,\n &mut recurring_txs,\n &mut chart_hidden_mode,\n &mut chart_hidden_legends,\n &mut summary_hidden_mode,\n &mut chart_activated_methods,\n &mut lerp_state,\n config,\n &mut theme,\n conn,\n );\n\n let status = match handler.page {\n CurrentUi::Initial => initial_keys(&mut handler),\n CurrentUi::Home => home_keys(&mut handler),\n CurrentUi::AddTx => add_tx_keys(&mut handler),\n CurrentUi::Chart => chart_keys(&mut handler),\n CurrentUi::Summary => summary_keys(&mut handler),\n CurrentUi::Search => search_keys(&mut handler),\n CurrentUi::Activity => activity_keys(&mut handler),\n CurrentUi::Recurring => recurring_keys(&mut handler),\n };\n\n match status {\n Ok(output) => {\n if let Some(output) = output {\n return Ok(output);\n }\n }\n Err(e) => {\n let state = InfoPopupState::Error(e.to_string());\n popup_status = PopupType::new_info(state);\n }\n }\n }\n }\n}", + "body_hash": "2995b0c3757aceec934faf06796d4c8ae784d3b12c54a0a02914d2473b00807b" }, "tui/src/page_handler/ui_handler.rs|crate::start_app#free:04d7c4ebf9a2": { "kind": "Free", @@ -4439,8 +4679,8 @@ "comment_kind": "Line", "comment_text": "// With no TX method at all, keep asking the user to create one", "comment_hash": "e7903176c3f2d6ecdbe040b7f1d1f1ea54336ac95c35118bf9ff98865ba948bb", - "body_text": "if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {}", - "body_hash": "f620cd15d4771925b3605a040093a3a0fbe56cfecbf0bba2f2420d7db176f190" + "body_text": "if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {}\nlet privacy = config.privacy_mode.unwrap_or(false);", + "body_hash": "9b5c56b13f04495906ef8fdd12fe8d4fbfa9cb450eb2b7d64708baed7f2fe651" }, "tui/src/page_handler/ui_handler.rs|crate::start_app#free:ebaef3caed6c": { "kind": "Free", @@ -4712,6 +4952,26 @@ "body_text": "pub fn previous(&mut self) {\n let i = match self.state.selected() {\n Some(i) => {\n if i == 0 {\n self.items.len() - 1\n } else {\n i - 1\n }\n }\n None => 0,\n };\n self.state.select(Some(i));\n }", "body_hash": "973484f626aea0f3f048b6bd1dafc01e9a375d6f8e7c34aec27f0e21b5d7639d" }, + "tui/src/page_handler/unlock.rs|crate::ask_password": { + "kind": "Function", + "file": "tui/src/page_handler/unlock.rs", + "item_path": "crate::ask_password", + "comment_kind": "OuterDoc", + "comment_text": " Asks for the database password on a screen of its own, before the app has a connection.\n\n Returns `None` if the user quits instead of answering.\n", + "comment_hash": "3bdff1f83e92d43bf1ac5fb2c2d9c15b01c21636b80b1f9e8cf775d19aa92477", + "body_text": "pub fn ask_password(theme: &Theme, error: Option<&str>) -> Result> {\n let mut terminal = enter_tui_interface()?;\n let mut password = String::new();\n\n let answer = loop {\n terminal.draw(|f| show_unlock_ui(f, theme, password.chars().count(), error))?;\n\n let Event::Key(key) = event::read()? else {\n continue;\n };\n\n if key.kind != KeyEventKind::Press {\n continue;\n }\n\n match key.code {\n KeyCode::Enter if !password.is_empty() => break Some(password),\n KeyCode::Esc => break None,\n KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break None,\n KeyCode::Char(c) => password.push(c),\n KeyCode::Backspace => {\n password.pop();\n }\n _ => {}\n }\n };\n\n exit_tui_interface()?;\n\n Ok(answer)\n}", + "body_hash": "ac12ef59521b18aebb774f81deb7266f8aad33d312a632ce5c4935d1b7d529b6" + }, + "tui/src/page_handler/unlock.rs|crate::show_unlock_ui": { + "kind": "Function", + "file": "tui/src/page_handler/unlock.rs", + "item_path": "crate::show_unlock_ui", + "comment_kind": "OuterDoc", + "comment_text": " Only the length of the password is ever given here, so nothing typed can end up on screen\n", + "comment_hash": "9cb3b0616a4df7c4ffa5468376aff20988f934715ebb0f6ec95899832cd0bb68", + "body_text": "fn show_unlock_ui(f: &mut Frame, theme: &Theme, password_len: usize, error: Option<&str>) {\n let area = centered_rect_exact(50, 10, f.area());\n\n f.render_widget(\n main_block(theme)\n .border_type(BorderType::Rounded)\n .borders(Borders::ALL)\n .title(Span::styled(\n \"Rex - Locked\",\n Style::default().add_modifier(Modifier::BOLD),\n )),\n area,\n );\n\n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(1)\n .constraints([\n Constraint::Length(3),\n Constraint::Length(1),\n Constraint::Length(1),\n Constraint::Length(1),\n ])\n .split(area);\n\n let masked = \"*\".repeat(password_len);\n\n f.render_widget(\n Paragraph::new(masked)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Password\", theme)),\n chunks[0],\n );\n\n f.set_cursor_position(Position {\n x: chunks[0].x + password_len as u16 + 1,\n y: chunks[0].y + 1,\n });\n\n let (status, color) = match error {\n Some(error) => (error.to_string(), theme.negative()),\n None => (String::new(), theme.text()),\n };\n\n f.render_widget(\n Paragraph::new(status)\n .style(\n Style::default()\n .bg(theme.background())\n .fg(color)\n .add_modifier(Modifier::BOLD),\n )\n .alignment(Alignment::Center),\n chunks[1],\n );\n\n f.render_widget(\n Paragraph::new(\"Enter to unlock. Esc to quit\")\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .alignment(Alignment::Center),\n chunks[3],\n );\n}", + "body_hash": "c2f20452a468bf5212b4b777a324faf43815094bb404989758d2055ad10fbbc9" + }, "tui/src/pages/activity_ui.rs|crate::activity_ui#free:042daf211246": { "kind": "Free", "file": "tui/src/pages/activity_ui.rs", @@ -4729,8 +4989,18 @@ "comment_kind": "Line", "comment_text": "// The first column is the date, which gets no separators. A yearly search\n// would otherwise end up with a comma in the year", "comment_hash": "081e9d50c8f48801307f3f42239afb314fa298eb8c63dee16b8c91647503e07d", - "body_text": "let mut first_index_passed = false;\nlet cells = item.iter().map(|c| {});\nRow::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n}", - "body_hash": "99e4b6647f764cd64af9018bc970ced08be10fa73de2370820467a620af9e230" + "body_text": "let mut first_index_passed = false;\nlet cells = item.iter().enumerate().map(|(index, c)| {});\nRow::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n}", + "body_hash": "1bc2b0a23e54ca6ac8efc4636d54956a8f77b1aca67779d104730bc60e3aa5f1" + }, + "tui/src/pages/activity_ui.rs|crate::activity_ui#free:9a52fccaf3aa": { + "kind": "Free", + "file": "tui/src/pages/activity_ui.rs", + "item_path": "crate::activity_ui#free:9a52fccaf3aa", + "comment_kind": "Line", + "comment_text": "// Index 3 is the Amount column", + "comment_hash": "9a52fccaf3aac1a82c405e82ac85ab74e0a5d3b2518e86114cc7b5788cd11c44", + "body_text": "if privacy && index == 3 {}\nif first_index_passed {} else {}\n}", + "body_hash": "835ddbb624da6fbf7cbc58793d206068a0632e34a8d682cea049c6b8b797b560" }, "tui/src/pages/activity_ui.rs|crate::activity_ui#free:d4b4b487d811": { "kind": "Free", @@ -4749,8 +5019,8 @@ "comment_kind": "OuterDoc", "comment_text": " The function draws the Add Transaction page of the interface.\n", "comment_hash": "d79562eebc8b0481e14b1028f3ce2ec68bc3ba4332cbcac738782c1d1759efa2", - "body_text": "pub fn add_tx_ui(\n f: &mut Frame,\n balance: &mut [Vec],\n add_tx_data: &TxData,\n add_tx_tab: &TxTab,\n lerp_state: &mut LerpState,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n \n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let status_data = add_tx_data.get_tx_status();\n \n \n \n let input_data = add_tx_data.get_all_texts();\n \n let current_index = add_tx_data.get_current_index();\n\n let size = f.area();\n\n let tx_type = add_tx_data.get_tx_type();\n\n let from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\n\n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(5),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Percentage(100),\n ])\n .split(size);\n\n \n \n let input_chunk = {\n match tx_type {\n TxType::IncomeExpense => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n TxType::Transfer => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n }\n };\n\n \n f.render_widget(main_block(theme), size);\n\n let bal_data = balance.iter().map(|item| {\n let height = 1;\n\n let row_type = HomeRow::get_row(item);\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow && ![\"Balance\", \"Changes\"].contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n index += 1;\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\").separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance Change\", theme))\n .style(Style::default().fg(theme.border()));\n\n let mut status_text = vec![];\n\n \n \n for i in status_data.iter().rev() {\n let (initial, rest) = i.text.split_once(':').unwrap();\n\n match i.log_type {\n LogType::Info => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.positive())),\n ]));\n }\n LogType::Error => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.negative())),\n ]));\n }\n }\n }\n\n \n \n let date_text = Line::from(format!(\"{} \", input_data[0]));\n\n let mut details_text = Line::from(format!(\"{} \", input_data[1]));\n\n let mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\n\n let mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\n\n let amount_text = Line::from(format!(\"{} \", input_data[4]));\n\n let mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\n\n let mut tags_text = Line::from(format!(\"{} \", input_data[6]));\n\n match add_tx_tab {\n TxTab::Details => {\n details_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[1])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::FromMethod => {\n from_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[2])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::ToMethod => {\n to_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[3])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::Tags => {\n tags_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[6])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::TxType => {\n tx_type_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[5])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n _ => {}\n }\n\n let status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\n\n let date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Date\", theme))\n .alignment(Alignment::Left);\n\n let from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\n\n let to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\n\n let amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\n\n let tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\n\n let details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\n\n let tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);\n\n \n \n match add_tx_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: input_chunk[0].x + current_index as u16 + 1,\n y: input_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: input_chunk[1].x + current_index as u16 + 1,\n y: input_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: input_chunk[2].x + current_index as u16 + 1,\n y: input_chunk[2].y + 1,\n }),\n _ => {}\n }\n\n match tx_type {\n TxType::IncomeExpense => match add_tx_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match add_tx_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[5].x + current_index as u16 + 1,\n y: input_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\n\n f.render_widget(balance_area, chunks[0]);\n f.render_widget(details_sec, chunks[2]);\n f.render_widget(status_sec, chunks[3]);\n f.render_widget(date_sec, input_chunk[0]);\n f.render_widget(tx_type_sec, input_chunk[1]);\n f.render_widget(from_method_sec, input_chunk[2]);\n\n match tx_type {\n TxType::IncomeExpense => {\n f.render_widget(amount_sec, input_chunk[3]);\n f.render_widget(tags_sec, input_chunk[4]);\n }\n TxType::Transfer => {\n f.render_widget(to_method_sec, input_chunk[3]);\n f.render_widget(amount_sec, input_chunk[4]);\n f.render_widget(tags_sec, input_chunk[5]);\n }\n }\n}", - "body_hash": "6bbef9826487d25267ddf970197874a86e7b8b4134c5da039ca26874adb740db" + "body_text": "pub fn add_tx_ui(\n f: &mut Frame,\n balance: &mut [Vec],\n add_tx_data: &TxData,\n add_tx_tab: &TxTab,\n lerp_state: &mut LerpState,\n theme: &Theme,\n privacy: bool,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n \n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let status_data = add_tx_data.get_tx_status();\n \n \n \n let input_data = add_tx_data.get_all_texts();\n \n let current_index = add_tx_data.get_current_index();\n\n let size = f.area();\n\n let tx_type = add_tx_data.get_tx_type();\n\n let from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\n\n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(5),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Percentage(100),\n ])\n .split(size);\n\n \n \n let input_chunk = {\n match tx_type {\n TxType::IncomeExpense => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n TxType::Transfer => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n }\n };\n\n \n f.render_widget(main_block(theme), size);\n\n let bal_data = balance.iter().map(|item| {\n let height = 1;\n\n let row_type = HomeRow::get_row(item);\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow && ![\"Balance\", \"Changes\"].contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if privacy {\n return Cell::from(PRIVACY_MASK);\n }\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n index += 1;\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\").separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance Change\", theme))\n .style(Style::default().fg(theme.border()));\n\n let mut status_text = vec![];\n\n \n \n for i in status_data.iter().rev() {\n let (initial, rest) = i.text.split_once(':').unwrap();\n\n match i.log_type {\n LogType::Info => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.positive())),\n ]));\n }\n LogType::Error => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.negative())),\n ]));\n }\n }\n }\n\n \n \n let date_text = Line::from(format!(\"{} \", input_data[0]));\n\n let mut details_text = Line::from(format!(\"{} \", input_data[1]));\n\n let mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\n\n let mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\n\n let amount_text = Line::from(format!(\"{} \", input_data[4]));\n\n let mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\n\n let mut tags_text = Line::from(format!(\"{} \", input_data[6]));\n\n match add_tx_tab {\n TxTab::Details => {\n details_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[1])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::FromMethod => {\n from_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[2])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::ToMethod => {\n to_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[3])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::Tags => {\n tags_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[6])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::TxType => {\n tx_type_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[5])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n _ => {}\n }\n\n let status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\n\n let date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Date\", theme))\n .alignment(Alignment::Left);\n\n let from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\n\n let to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\n\n let amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\n\n let tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\n\n let details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\n\n let tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);\n\n \n \n match add_tx_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: input_chunk[0].x + current_index as u16 + 1,\n y: input_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: input_chunk[1].x + current_index as u16 + 1,\n y: input_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: input_chunk[2].x + current_index as u16 + 1,\n y: input_chunk[2].y + 1,\n }),\n _ => {}\n }\n\n match tx_type {\n TxType::IncomeExpense => match add_tx_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match add_tx_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[5].x + current_index as u16 + 1,\n y: input_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\n\n f.render_widget(balance_area, chunks[0]);\n f.render_widget(details_sec, chunks[2]);\n f.render_widget(status_sec, chunks[3]);\n f.render_widget(date_sec, input_chunk[0]);\n f.render_widget(tx_type_sec, input_chunk[1]);\n f.render_widget(from_method_sec, input_chunk[2]);\n\n match tx_type {\n TxType::IncomeExpense => {\n f.render_widget(amount_sec, input_chunk[3]);\n f.render_widget(tags_sec, input_chunk[4]);\n }\n TxType::Transfer => {\n f.render_widget(to_method_sec, input_chunk[3]);\n f.render_widget(amount_sec, input_chunk[4]);\n f.render_widget(tags_sec, input_chunk[5]);\n }\n }\n}", + "body_hash": "6b88334d939bbc885a7f3b177b16f2a2edf13c0f2e040e639e54e10cb438c17a" }, "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:37e26499b6b0": { "kind": "Free", @@ -4859,8 +5129,8 @@ "comment_kind": "Line", "comment_text": "// Changes row can contain arrow symbols", "comment_hash": "ed98a63512e55d3edabf0fff7754033ff310882cf5f07260d73c736b77407047", - "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};", - "body_hash": "b42a79c23a7d289c1cac9ab7e6aa00b2ea3e94fe23db4716bd5189b299c04503" + "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};\nif privacy {}", + "body_hash": "0fc659b1d9e09f69fd0ea89bf3ed918feb93d802935b73625befba6a34b2259c" }, "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:ff561b0d9be3": { "kind": "Free", @@ -4879,8 +5149,8 @@ "comment_kind": "OuterDoc", "comment_text": " Creates the balance chart from the transactions\n", "comment_hash": "44b650027cfb510bc8903182c7ef916705e758de54b81dd9fa200b7d5c4bb749", - "body_text": "pub fn chart_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n chart_tx_methods: &IndexedData,\n current_page: &ChartTab,\n chart_hidden_mode: bool,\n chart_hidden_legends: bool,\n chart_activated_methods: &HashMap,\n lerp_state: &mut LerpState,\n chart_view: &ChartView,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let size = f.area();\n\n \n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n\n \n if chart_hidden_mode {\n main_layout = main_layout.constraints([Constraint::Min(0)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n let mut tx_method_selection_tab = create_tab_activation(\n chart_tx_methods,\n \"Tx Method Selection\",\n chart_activated_methods,\n theme,\n );\n\n let tx_methods = conn.get_tx_methods_sorted();\n\n let mut all_tx_methods: Vec<&str> = tx_methods.iter().map(|t| t.name.as_str()).collect();\n all_tx_methods.push(\"Cumulative\");\n\n \n let mut datasets: Vec> = Vec::new();\n\n let mut last_balances = Vec::new();\n\n \n if chart_view.is_empty() {\n for _ in 0..all_tx_methods.len() {\n datasets.push(vec![(0.0, 0.0)]);\n last_balances.push(0.0);\n }\n }\n\n let mut lowest_balance = 0.0;\n let mut highest_balance = 0.0;\n\n let mut date_labels: Vec = vec![];\n\n let mut current_axis = 0.0;\n\n \n if !chart_view.is_empty() {\n let mut checking_date = chart_view.start_date();\n\n \n let final_date = chart_view.end_date();\n\n \n let total_loop = final_date.signed_duration_since(checking_date).num_days() as f64;\n\n let lerp_id = \"chart_loop_size\";\n let mut to_loop = lerp_state.lerp(lerp_id, total_loop, None);\n\n \n date_labels.push(checking_date.to_string());\n date_labels.push(final_date.to_string());\n\n \n \n \n\n let mut to_add_again = false;\n let mut data_num = 0;\n loop {\n if chart_view.contains_date(&checking_date) {\n let current_balances = chart_view.get_balance(data_num);\n\n \n let mut next_date = NaiveDate::default();\n\n if chart_view.len() > data_num + 1 {\n next_date = chart_view.get_tx(data_num + 1).date.date();\n }\n \n \n \n last_balances = Vec::new();\n\n let mut cumulative_balance = 0.0;\n\n for method_index in 0..all_tx_methods.len() {\n \n let current_balance = if method_index == all_tx_methods.len() - 1 {\n cumulative_balance\n } else {\n let method_at_index = tx_methods[method_index];\n let balance = current_balances.get(&method_at_index.id).unwrap().dollar();\n\n cumulative_balance += balance.value();\n\n balance.value()\n };\n\n \n \n \n \n if chart_activated_methods[all_tx_methods[method_index]] {\n if current_balance > highest_balance {\n highest_balance = current_balance;\n } else if current_balance < lowest_balance {\n lowest_balance = current_balance;\n }\n }\n\n if to_add_again {\n \n \n \n \n\n let (position, _balance) = datasets[method_index].pop().unwrap();\n let to_push = vec![(position, current_balance)];\n datasets[method_index].extend(to_push);\n } else {\n let to_push = vec![(current_axis, current_balance)];\n\n if datasets.get(method_index).is_some() {\n datasets[method_index].extend(to_push);\n } else {\n datasets.push(to_push);\n }\n }\n\n last_balances.push(current_balance);\n }\n\n if next_date == checking_date {\n \n to_add_again = true;\n } else {\n to_add_again = false;\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n \n data_num += 1;\n } else {\n \n for method_index in 0..all_tx_methods.len() {\n let to_push = vec![(current_axis, last_balances[method_index])];\n datasets[method_index].extend(to_push);\n }\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n if !to_add_again {\n \n if to_loop <= 0.0 {\n date_labels.pop().unwrap();\n date_labels.push(checking_date.to_string());\n break;\n }\n to_loop += -1.0;\n }\n\n if checking_date == NaiveDate::default() {\n break;\n }\n }\n }\n\n \n highest_balance += highest_balance * 5.0 / 100.0;\n lowest_balance -= lowest_balance * 5.0 / 100.0;\n\n let diff = (highest_balance - lowest_balance) / 10.0;\n\n let mut to_add = lowest_balance;\n\n \n \n let mut labels = vec![lowest_balance.to_string()];\n\n for _i in 0..10 {\n to_add += diff;\n labels.push(format!(\"{to_add:.2}\"));\n }\n\n let mut color_list = vec![\n Color::LightRed,\n Color::LightBlue,\n Color::LightYellow,\n Color::Gray,\n Color::Black,\n Color::Yellow,\n Color::Green,\n Color::Red,\n Color::Blue,\n Color::Magenta,\n ];\n\n let mut final_dataset = vec![];\n\n \n for i in 0..all_tx_methods.len() {\n \n if color_list.is_empty() {\n color_list.push(Color::Cyan);\n }\n\n if !chart_activated_methods[all_tx_methods[i]] {\n continue;\n }\n\n let mut dataset = Dataset::default()\n .marker(Marker::Braille)\n .graph_type(GraphType::Line)\n .style(\n Style::default()\n .fg(color_list.pop().unwrap())\n .bg(theme.background()),\n )\n .data(&datasets[i]);\n\n if !chart_hidden_legends {\n dataset = dataset.name(all_tx_methods[i]);\n }\n\n final_dataset.push(dataset);\n }\n\n let chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\n\n match current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n ChartTab::TxMethods => {\n tx_method_selection_tab =\n tx_method_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n }\n\n if chart_hidden_mode {\n f.render_widget(chart, chunks[0]);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(tx_method_selection_tab, chunks[3]);\n f.render_widget(chart, chunks[4]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(tx_method_selection_tab, chunks[2]);\n f.render_widget(chart, chunks[3]);\n }\n 2 => {\n f.render_widget(tx_method_selection_tab, chunks[1]);\n f.render_widget(chart, chunks[2]);\n }\n _ => {}\n }\n }\n}", - "body_hash": "43d103f0764d9a78e7a9d28c99c01e000210dbb6aa8282f3880555ce6a1b37e1" + "body_text": "pub fn chart_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n chart_tx_methods: &IndexedData,\n current_page: &ChartTab,\n chart_hidden_mode: bool,\n chart_hidden_legends: bool,\n chart_activated_methods: &HashMap,\n lerp_state: &mut LerpState,\n chart_view: &ChartView,\n theme: &Theme,\n privacy: bool,\n conn: &mut DbConn,\n) {\n let size = f.area();\n\n \n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n\n \n if chart_hidden_mode {\n main_layout = main_layout.constraints([Constraint::Min(0)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n let mut tx_method_selection_tab = create_tab_activation(\n chart_tx_methods,\n \"Tx Method Selection\",\n chart_activated_methods,\n theme,\n );\n\n let tx_methods = conn.get_tx_methods_sorted();\n\n let mut all_tx_methods: Vec<&str> = tx_methods.iter().map(|t| t.name.as_str()).collect();\n all_tx_methods.push(\"Cumulative\");\n\n \n let mut datasets: Vec> = Vec::new();\n\n let mut last_balances = Vec::new();\n\n \n if chart_view.is_empty() {\n for _ in 0..all_tx_methods.len() {\n datasets.push(vec![(0.0, 0.0)]);\n last_balances.push(0.0);\n }\n }\n\n let mut lowest_balance = 0.0;\n let mut highest_balance = 0.0;\n\n let mut date_labels: Vec = vec![];\n\n let mut current_axis = 0.0;\n\n \n if !chart_view.is_empty() {\n let mut checking_date = chart_view.start_date();\n\n \n let final_date = chart_view.end_date();\n\n \n let total_loop = final_date.signed_duration_since(checking_date).num_days() as f64;\n\n let lerp_id = \"chart_loop_size\";\n let mut to_loop = lerp_state.lerp(lerp_id, total_loop, None);\n\n \n date_labels.push(checking_date.to_string());\n date_labels.push(final_date.to_string());\n\n \n \n \n\n let mut to_add_again = false;\n let mut data_num = 0;\n loop {\n if chart_view.contains_date(&checking_date) {\n let current_balances = chart_view.get_balance(data_num);\n\n \n let mut next_date = NaiveDate::default();\n\n if chart_view.len() > data_num + 1 {\n next_date = chart_view.get_tx(data_num + 1).date.date();\n }\n \n \n \n last_balances = Vec::new();\n\n let mut cumulative_balance = 0.0;\n\n for method_index in 0..all_tx_methods.len() {\n \n let current_balance = if method_index == all_tx_methods.len() - 1 {\n cumulative_balance\n } else {\n let method_at_index = tx_methods[method_index];\n let balance = current_balances.get(&method_at_index.id).unwrap().dollar();\n\n cumulative_balance += balance.value();\n\n balance.value()\n };\n\n \n \n \n \n if chart_activated_methods[all_tx_methods[method_index]] {\n if current_balance > highest_balance {\n highest_balance = current_balance;\n } else if current_balance < lowest_balance {\n lowest_balance = current_balance;\n }\n }\n\n if to_add_again {\n \n \n \n \n\n let (position, _balance) = datasets[method_index].pop().unwrap();\n let to_push = vec![(position, current_balance)];\n datasets[method_index].extend(to_push);\n } else {\n let to_push = vec![(current_axis, current_balance)];\n\n if datasets.get(method_index).is_some() {\n datasets[method_index].extend(to_push);\n } else {\n datasets.push(to_push);\n }\n }\n\n last_balances.push(current_balance);\n }\n\n if next_date == checking_date {\n \n to_add_again = true;\n } else {\n to_add_again = false;\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n \n data_num += 1;\n } else {\n \n for method_index in 0..all_tx_methods.len() {\n let to_push = vec![(current_axis, last_balances[method_index])];\n datasets[method_index].extend(to_push);\n }\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n if !to_add_again {\n \n if to_loop <= 0.0 {\n date_labels.pop().unwrap();\n date_labels.push(checking_date.to_string());\n break;\n }\n to_loop += -1.0;\n }\n\n if checking_date == NaiveDate::default() {\n break;\n }\n }\n }\n\n \n highest_balance += highest_balance * 5.0 / 100.0;\n lowest_balance -= lowest_balance * 5.0 / 100.0;\n\n let diff = (highest_balance - lowest_balance) / 10.0;\n\n let mut to_add = lowest_balance;\n\n \n \n let mut labels = vec![lowest_balance.to_string()];\n\n for _i in 0..10 {\n to_add += diff;\n labels.push(format!(\"{to_add:.2}\"));\n }\n\n let mut color_list = vec![\n Color::LightRed,\n Color::LightBlue,\n Color::LightYellow,\n Color::Gray,\n Color::Black,\n Color::Yellow,\n Color::Green,\n Color::Red,\n Color::Blue,\n Color::Magenta,\n ];\n\n let mut final_dataset = vec![];\n\n \n for i in 0..all_tx_methods.len() {\n \n if color_list.is_empty() {\n color_list.push(Color::Cyan);\n }\n\n if !chart_activated_methods[all_tx_methods[i]] {\n continue;\n }\n\n let mut dataset = Dataset::default()\n .marker(Marker::Braille)\n .graph_type(GraphType::Line)\n .style(\n Style::default()\n .fg(color_list.pop().unwrap())\n .bg(theme.background()),\n )\n .data(&datasets[i]);\n\n if !chart_hidden_legends {\n dataset = dataset.name(all_tx_methods[i]);\n }\n\n final_dataset.push(dataset);\n }\n\n let chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\n\n match current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n ChartTab::TxMethods => {\n tx_method_selection_tab =\n tx_method_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n }\n\n \n let chart_chunk = chunks[chunks.len() - 1];\n\n if chart_hidden_mode {\n render_chart(f, chart, chart_chunk, privacy, theme);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(tx_method_selection_tab, chunks[3]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(tx_method_selection_tab, chunks[2]);\n }\n 2 => {\n f.render_widget(tx_method_selection_tab, chunks[1]);\n }\n _ => {}\n }\n\n render_chart(f, chart, chart_chunk, privacy, theme);\n }\n}", + "body_hash": "515f5d2e412d6a93fc660d64135ce1a6f428cafbf331b117d143a383a582795f" }, "tui/src/pages/chart_ui.rs|crate::chart_ui#free:07f5d316839a": { "kind": "Free", @@ -4902,6 +5172,16 @@ "body_text": "data_num += 1;\n}", "body_hash": "8e25ed185b9dbb54b79e94988b366c4d86117cc20b7d0bc462e99f6f53211fe2" }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:1518cd3bc47b": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:1518cd3bc47b", + "comment_kind": "Line", + "comment_text": "// The chart is always the last chunk", + "comment_hash": "1518cd3bc47b3f3ee39dbab6da12456d21561096fe06b85845390936f92acd17", + "body_text": "let chart_chunk = chunks[chunks.len() - 1];\nif chart_hidden_mode {} else {}\n}", + "body_hash": "785f70a76567878aebd79fbba9050c0e772c4a2cc2140b20d7597106e1ee226c" + }, "tui/src/pages/chart_ui.rs|crate::chart_ui#free:3e932f6000f7": { "kind": "Free", "file": "tui/src/pages/chart_ui.rs", @@ -4919,8 +5199,8 @@ "comment_kind": "Line", "comment_text": "// Loop through the data that was added for each tx_method and turn them into chart data", "comment_hash": "3f9ce259931dc4fa701d8e30de8b639c95d65ce118a6f4a7aed83fe37cae77b5", - "body_text": "for i in 0..all_tx_methods.len() {}\nlet chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\nmatch current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {}\n ChartTab::TxMethods => {}\n }\nif chart_hidden_mode {} else {}\n}", - "body_hash": "eb3046dc234e8cf86f86ca7666a124d5f1c640efc99ecb00afd1a2f34b90a88a" + "body_text": "for i in 0..all_tx_methods.len() {}\nlet chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\nmatch current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {}\n ChartTab::TxMethods => {}\n }", + "body_hash": "7a10cdc2ef4b0c91eb572caa4042fe4153158eccd77d289ddbf1679ce854fe8a" }, "tui/src/pages/chart_ui.rs|crate::chart_ui#free:4625f66030de": { "kind": "Free", @@ -5119,8 +5399,8 @@ "comment_kind": "OuterDoc", "comment_text": " The function draws the Homepage of the interface.\n", "comment_hash": "16e5c1896fecde18d6224c6bc9003ff717ba503e198fe1d4d7968e46e662301e", - "body_text": "pub fn home_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n home_table: &mut TableData,\n current_tab: &HomeTab,\n lerp_state: &mut LerpState,\n view_group: &mut TxViewGroup,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let size = f.area();\n\n \n let selected_style_income = Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::REVERSED);\n let selected_style_expense = Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::REVERSED);\n\n let tx_count = home_table.items.len();\n let lerp_id = \"home_tx_count\";\n let lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\n\n let lerp_tx_row = lerp_state.lerp(HOME_TABLE_ID, tx_count as f64, Some(0.50)) as i64;\n\n let table_name = format!(\"Transactions: {lerp_tx_count}\");\n\n \n let header_cells = [\"Date\", \"Details\", \"TX Method\", \"Amount\", \"Type\", \"Tags\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n \n let rows = home_table\n .items\n .iter()\n .take(lerp_tx_row as usize)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n let Ok(parsed_num) = c.parse::() else {\n return Cell::from(c.separate_with_commas());\n };\n\n let lerp_id = format!(\"home_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n });\n\n \n \n \n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(9),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n ])\n .split(size);\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n \n \n \n\n let mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(13),\n Constraint::Percentage(35),\n Constraint::Percentage(13),\n Constraint::Percentage(13),\n Constraint::Percentage(8),\n Constraint::Percentage(18),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));\n\n let balance_array = view_group\n .balance_array(home_table.state.selected(), conn)\n .unwrap();\n \n let bal_data = balance_array.into_iter().map(|item| {\n let height = 1_u16;\n\n let row_type = HomeRow::get_row(&item);\n\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow\n && ![\n \"Balance\",\n \"Changes\",\n \"Income\",\n \"Expense\",\n \"Daily Income\",\n \"Daily Expense\",\n ]\n .contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n index += 1;\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\").separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n \n \n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_tab {\n \n \n HomeTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n HomeTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n \n HomeTab::Table => {\n if let Some(a) = home_table.state.selected() {\n table_area = table_area.highlight_symbol(\">> \");\n\n let income_strings = [\"Income\", \"Borrow\", \"Lend Repay\"];\n let expense_strings = [\"Expense\", \"Lend\", \"Borrow Repay\"];\n\n let target_string = &home_table.items[a][4].as_str();\n\n if expense_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_expense);\n } else if income_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_income);\n } else if home_table.items[a][4] == \"Transfer\" {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area.row_highlight_style(style);\n }\n }\n }\n }\n\n \n if let Some(index) = home_table.state.selected()\n && index > 10\n {\n *home_table.state.offset_mut() = index - 10;\n }\n\n \n \n f.render_widget(balance_area, chunks[0]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(year_tab, chunks[1]);\n\n \n f.render_stateful_widget(table_area, chunks[3], &mut home_table.state);\n}", - "body_hash": "293b44ba7bd49896febd1e822c44e130909f6d964879896bbc16690d84d01308" + "body_text": "pub fn home_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n home_table: &mut TableData,\n current_tab: &HomeTab,\n lerp_state: &mut LerpState,\n view_group: &mut TxViewGroup,\n theme: &Theme,\n privacy: bool,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let size = f.area();\n\n \n let selected_style_income = Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::REVERSED);\n let selected_style_expense = Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::REVERSED);\n\n let tx_count = home_table.items.len();\n let lerp_id = \"home_tx_count\";\n let lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\n\n let lerp_tx_row = lerp_state.lerp(HOME_TABLE_ID, tx_count as f64, Some(0.50)) as i64;\n\n let table_name = format!(\"Transactions: {lerp_tx_count}\");\n\n \n let header_cells = [\"Date\", \"Details\", \"TX Method\", \"Amount\", \"Type\", \"Tags\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n \n let rows = home_table\n .items\n .iter()\n .take(lerp_tx_row as usize)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n \n if privacy && index == 3 {\n return Cell::from(PRIVACY_MASK);\n }\n\n let Ok(parsed_num) = c.parse::() else {\n return Cell::from(c.separate_with_commas());\n };\n\n let lerp_id = format!(\"home_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n });\n\n \n \n \n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(9),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n ])\n .split(size);\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n \n \n \n\n let mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(13),\n Constraint::Percentage(35),\n Constraint::Percentage(13),\n Constraint::Percentage(13),\n Constraint::Percentage(8),\n Constraint::Percentage(18),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));\n\n let balance_array = view_group\n .balance_array(home_table.state.selected(), conn)\n .unwrap();\n \n let bal_data = balance_array.into_iter().map(|item| {\n let height = 1_u16;\n\n let row_type = HomeRow::get_row(&item);\n\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow\n && ![\n \"Balance\",\n \"Changes\",\n \"Income\",\n \"Expense\",\n \"Daily Income\",\n \"Daily Expense\",\n ]\n .contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n index += 1;\n\n if privacy {\n return Cell::from(PRIVACY_MASK);\n }\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\").separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n \n \n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_tab {\n \n \n HomeTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n HomeTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n \n HomeTab::Table => {\n if let Some(a) = home_table.state.selected() {\n table_area = table_area.highlight_symbol(\">> \");\n\n let income_strings = [\"Income\", \"Borrow\", \"Lend Repay\"];\n let expense_strings = [\"Expense\", \"Lend\", \"Borrow Repay\"];\n\n let target_string = &home_table.items[a][4].as_str();\n\n if expense_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_expense);\n } else if income_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_income);\n } else if home_table.items[a][4] == \"Transfer\" {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area.row_highlight_style(style);\n }\n }\n }\n }\n\n \n if let Some(index) = home_table.state.selected()\n && index > 10\n {\n *home_table.state.offset_mut() = index - 10;\n }\n\n \n \n f.render_widget(balance_area, chunks[0]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(year_tab, chunks[1]);\n\n \n f.render_stateful_widget(table_area, chunks[3], &mut home_table.state);\n}", + "body_hash": "98b0a0bb8eac393eb0e0b11ff9c15833878e96f5aa08607ef4bccf98a755ed6b" }, "tui/src/pages/home_ui.rs|crate::home_ui#free:041abc10d72c": { "kind": "Free", @@ -5182,6 +5462,16 @@ "body_text": "let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(9),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n ])\n .split(size);\nf.render_widget(main_block(theme), size);\nlet mut month_tab = create_tab(months, \"Months\", theme);\nlet mut year_tab = create_tab(years, \"Years\", theme);", "body_hash": "67a1b36d0ad4850508724e3c0f2ede20ef5f1bbb35b9d451dd4bc449441f9e21" }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:9a52fccaf3aa": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:9a52fccaf3aa", + "comment_kind": "Line", + "comment_text": "// Index 3 is the Amount column", + "comment_hash": "9a52fccaf3aac1a82c405e82ac85ab74e0a5d3b2518e86114cc7b5788cd11c44", + "body_text": "if privacy && index == 3 {}\nlet Ok(parsed_num) = c.parse::() else {};\nlet lerp_id = format!(\"home_table:{index}:{row_index}\");\nlet new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\nCell::from(format!(\"{new_c:.2}\").separate_with_commas())\n}", + "body_hash": "9584a5b038fe30fab69cbac262e483dc04aea918035eebb21e08c4faff4b251f" + }, "tui/src/pages/home_ui.rs|crate::home_ui#free:b801cf2e87a3": { "kind": "Free", "file": "tui/src/pages/home_ui.rs", @@ -5229,8 +5519,8 @@ "comment_kind": "Line", "comment_text": "// Changes row can contain arrow symbols", "comment_hash": "ed98a63512e55d3edabf0fff7754033ff310882cf5f07260d73c736b77407047", - "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};\nindex += 1;", - "body_hash": "d808b8c5f50fcde387ba69e15805c657d0c227296be44dc19ce62fec20fc0e54" + "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};\nindex += 1;\nif privacy {}", + "body_hash": "5106b1e7602a689f8a9a09689bd3f97d7bdd91e6c3bb9026efc61a74dc8125f4" }, "tui/src/pages/home_ui.rs|crate::home_ui#free:f4dbdead76d3": { "kind": "Free", @@ -5422,6 +5712,56 @@ "body_text": "fn row_style(theme: &Theme) -> Style {\n Style::default().bg(theme.background()).fg(theme.text())\n}", "body_hash": "41ef0294fe87b7f07d95c588155041ca18312f4f7205ce3d164cdd83c9a5becc" }, + "tui/src/pages/popups/models.rs|crate::ConfigChoices::SetPassword": { + "kind": "Variant", + "file": "tui/src/pages/popups/models.rs", + "item_path": "crate::ConfigChoices::SetPassword", + "comment_kind": "OuterDoc", + "comment_text": " The text changes to match whether a password exists already\n", + "comment_hash": "22ef1d8b5d0e547158ce5621f0934b616c21680cbee591bea3a9729de4ebf6b8", + "body_text": "#[strum(to_string = \"Set a password for app data\")]\nSetPassword", + "body_hash": "045ca257d01a8b8766c9e8913787fc8fc761ed2f409829b2d34b90ab6ed8dd01" + }, + "tui/src/pages/popups/models.rs|crate::PasswordOutcome::Accepted": { + "kind": "Variant", + "file": "tui/src/pages/popups/models.rs", + "item_path": "crate::PasswordOutcome::Accepted", + "comment_kind": "OuterDoc", + "comment_text": " Entered twice, identical and long enough\n", + "comment_hash": "5af2353cc7a8a09db552270541fa9978c1da33e6f40889843af555b19bdca282", + "body_text": "Accepted(String)", + "body_hash": "6ad55d637b716fff3374a393749057e6d4985a2583544dc2249fd6bf529e9ec2" + }, + "tui/src/pages/popups/models.rs|crate::PasswordOutcome::Pending": { + "kind": "Variant", + "file": "tui/src/pages/popups/models.rs", + "item_path": "crate::PasswordOutcome::Pending", + "comment_kind": "OuterDoc", + "comment_text": " More input needed. The status explains why when something was wrong\n", + "comment_hash": "23467f8dc537239cb83c71c0e76c2bf84e0db651060c238aad41eecdb88011a6", + "body_text": "Pending", + "body_hash": "3eae7d749562f89f07bbc7e280dc225cc0c1f250dcdc5165695ba919f49bac46" + }, + "tui/src/pages/popups/models.rs|crate::PasswordPopup::changing": { + "kind": "Field", + "file": "tui/src/pages/popups/models.rs", + "item_path": "crate::PasswordPopup::changing", + "comment_kind": "OuterDoc", + "comment_text": " The database already has a password, so this replaces it instead of creating one\n", + "comment_hash": "e9f77e8dedf594a24c29e30b3faf5a138892150c2e4a03fe1a84b04e2e7ab2e0", + "body_text": "pub changing: bool", + "body_hash": "95fb38a30ad243e233f4beeb322e099f68b61d4eb5ba637ae96532864d784301" + }, + "tui/src/pages/popups/models.rs|crate::impl PopupType::password_failed": { + "kind": "Function", + "file": "tui/src/pages/popups/models.rs", + "item_path": "crate::impl PopupType::password_failed", + "comment_kind": "OuterDoc", + "comment_text": " Shows why saving the password failed and starts over\n", + "comment_hash": "f21f54621ad7d35a86a3842d6c8b35b4a8fb23ed3d1b93f832c980290f696b3b", + "body_text": "pub fn password_failed(&mut self, error: &str) {\n if let PopupType::Password(password) = self {\n password.text.clear();\n password.first.clear();\n password.stage = PasswordStage::Enter;\n password.status = format!(\"Failed: {error}\");\n }\n }", + "body_hash": "c4c012a48e4b913f332075b6912c42ea97b1579ac25f58de56bdacded5964917" + }, "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:092b6e90c7f6": { "kind": "Free", "file": "tui/src/pages/recurring_ui.rs", @@ -5452,6 +5792,16 @@ "body_text": "let current_index = recurring_data.get_current_index();\nlet tx_type = recurring_data.get_tx_type();\nlet frequency = recurring_data.get_frequency();\nlet size = f.area();\nlet from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\nlet tx_count = recurring_table.items.len();\nlet lerp_id = \"recurring_count\";\nlet lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\nlet lerp_row = lerp_state.lerp(RECURRING_TABLE_ID, tx_count as f64, Some(0.50)) as usize;\nlet table_name = format!(\"Recurring Transactions: {lerp_tx_count}\");\nlet header_cells = [\n \"Next Date\",\n \"Frequency\",\n \"Details\",\n \"Method\",\n \"Amount\",\n \"Type\",\n \"Tags\",\n \"Paused\",\n ]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet rows = recurring_table.items.iter().take(lerp_row).map(|item| {});\nlet mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(12),\n Constraint::Percentage(13),\n Constraint::Percentage(25),\n Constraint::Percentage(13),\n Constraint::Percentage(11),\n Constraint::Percentage(8),\n Constraint::Percentage(11),\n Constraint::Percentage(7),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));", "body_hash": "44d4d8f170b966156fb9987b4b45ec49086e4834773d6315de230dbac49155b1" }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:815e7c0db1a1": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:815e7c0db1a1", + "comment_kind": "Line", + "comment_text": "// Index 4 is the Amount column", + "comment_hash": "815e7c0db1a10850824a68a3833999ad3bf4e1590678ed0fa79d4805294af7fd", + "body_text": "if privacy && index == 4 {} else if index == 4 {} else {}\n}", + "body_hash": "a902e6d27a3d00e6aa6f0318efcbc6554a22f7470a42729ee32ccb25bc2ae244" + }, "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:8a18521a9ae1": { "kind": "Free", "file": "tui/src/pages/recurring_ui.rs", @@ -5482,26 +5832,6 @@ "body_text": "let recur_box_count = match frequency {\n RecurrenceFrequency::Daily => 3,\n RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => 4,\n RecurrenceFrequency::Yearly => 5,\n };\nlet recur_percent = 100 / recur_box_count;\nlet recur_chunk = Layout::default()\n .direction(Direction::Horizontal)\n .constraints(vec![\n Constraint::Percentage(recur_percent);\n recur_box_count as usize\n ])\n .split(chunks[1]);\nf.render_widget(main_block(theme), size);\nlet mut status_text = vec![];\nfor i in status_data.iter().rev() {}\nlet date_text = Line::from(format!(\"{} \", input_data[0]));\nlet mut details_text = Line::from(format!(\"{} \", input_data[1]));\nlet mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\nlet mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\nlet amount_text = Line::from(format!(\"{} \", input_data[4]));\nlet mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\nlet mut tags_text = Line::from(format!(\"{} \", input_data[6]));\nlet mut frequency_text = Line::from(format!(\"{} \", recur_data[0]));\nlet recur_interval_text = Line::from(format!(\"{} \", recur_data[1]));\nlet mut recur_value_text = Line::from(format!(\"{} \", recur_data[2]));\nlet mut recur_month_text = Line::from(format!(\"{} \", recur_data[3]));\nlet end_date_text = Line::from(format!(\"{} \", recur_data[4]));\nmatch recurring_tab {\n TxTab::Details => {}\n TxTab::FromMethod => {}\n TxTab::ToMethod => {}\n TxTab::Tags => {}\n TxTab::Frequency => {}\n TxTab::RecurValue => {}\n TxTab::RecurMonth => {}\n TxTab::TxType => {}\n _ => {}\n }\nlet status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\nlet date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Start Date\", theme))\n .alignment(Alignment::Left);\nlet from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\nlet to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\nlet amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\nlet tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\nlet details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\nlet tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);\nlet frequency_sec = Paragraph::new(frequency_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Frequency\", theme))\n .alignment(Alignment::Left);\nlet recur_interval_title = match frequency {\n RecurrenceFrequency::Daily => \"Every N Days\",\n RecurrenceFrequency::Weekly => \"Every N Weeks\",\n RecurrenceFrequency::Monthly => \"Every N Months\",\n RecurrenceFrequency::Yearly => \"Every N Years\",\n };\nlet recur_interval_sec = Paragraph::new(recur_interval_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(recur_interval_title, theme))\n .alignment(Alignment::Left);\nlet recur_value_title = match frequency {\n RecurrenceFrequency::Weekly => \"Day of Week\",\n _ => \"Day of Month\",\n };\nlet recur_value_sec = Paragraph::new(recur_value_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(recur_value_title, theme))\n .alignment(Alignment::Left);\nlet recur_month_sec = Paragraph::new(recur_month_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Month\", theme))\n .alignment(Alignment::Left);\nlet end_date_sec = Paragraph::new(end_date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"End Date (Optional)\", theme))\n .alignment(Alignment::Left);\nmatch recurring_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: core_chunk[0].x + current_index as u16 + 1,\n y: core_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: core_chunk[1].x + current_index as u16 + 1,\n y: core_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: core_chunk[2].x + current_index as u16 + 1,\n y: core_chunk[2].y + 1,\n }),\n _ => {}\n }\nmatch tx_type {\n TxType::IncomeExpense => match recurring_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: core_chunk[3].x + current_index as u16 + 1,\n y: core_chunk[3].y + 1,\n }),\n TxTab::Tags => f.set_cursor_position(Position {\n x: core_chunk[4].x + current_index as u16 + 1,\n y: core_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match recurring_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: core_chunk[3].x + current_index as u16 + 1,\n y: core_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: core_chunk[4].x + current_index as u16 + 1,\n y: core_chunk[4].y + 1,\n }),\n TxTab::Tags => f.set_cursor_position(Position {\n x: core_chunk[5].x + current_index as u16 + 1,\n y: core_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\nmatch recurring_tab {\n TxTab::Frequency => f.set_cursor_position(Position {\n x: recur_chunk[0].x + current_index as u16 + 1,\n y: recur_chunk[0].y + 1,\n }),\n TxTab::RecurInterval => f.set_cursor_position(Position {\n x: recur_chunk[1].x + current_index as u16 + 1,\n y: recur_chunk[1].y + 1,\n }),\n TxTab::RecurValue => f.set_cursor_position(Position {\n x: recur_chunk[2].x + current_index as u16 + 1,\n y: recur_chunk[2].y + 1,\n }),\n TxTab::RecurMonth if recur_box_count == 5 => f.set_cursor_position(Position {\n x: recur_chunk[3].x + current_index as u16 + 1,\n y: recur_chunk[3].y + 1,\n }),\n TxTab::EndDate => {}\n _ => {}\n }\nf.render_widget(details_sec, chunks[2]);\nf.render_widget(status_sec, chunks[3]);\nf.render_widget(date_sec, core_chunk[0]);\nf.render_widget(tx_type_sec, core_chunk[1]);\nf.render_widget(from_method_sec, core_chunk[2]);\nmatch tx_type {\n TxType::IncomeExpense => {}\n TxType::Transfer => {}\n }\nf.render_widget(frequency_sec, recur_chunk[0]);\nf.render_widget(recur_interval_sec, recur_chunk[1]);\nmatch frequency {\n RecurrenceFrequency::Daily => {}\n RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => {}\n RecurrenceFrequency::Yearly => {}\n }\nif recurring_table.state.selected().is_some() {}\nif let Some(index) = recurring_table.state.selected()\n && index > 10\n {}\nf.render_stateful_widget(table_area, chunks[4], &mut recurring_table.state);\n}", "body_hash": "cdbdb121ca296d7053aee314a749b4fd900a1c722b22ca0e33d04d97e58f8f49" }, - "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:ab5b6ff9f743": { - "kind": "Free", - "file": "tui/src/pages/recurring_ui.rs", - "item_path": "crate::recurring_ui#free:ab5b6ff9f743", - "comment_kind": "Line", - "comment_text": "// Index 4 is the Amount column - the only one that should get thousands\n// separators", - "comment_hash": "ab5b6ff9f74391ac7829a60064af7160c419876e89307880cf0c849169771793", - "body_text": "if index == 4 {} else {}\n}", - "body_hash": "33f61081048c6e996d25253715055524f8f42208bd186b979c38f6f05d5e278b" - }, - "tui/src/pages/recurring_ui.rs|crate::tests::suggestion_shows_next_to_the_selected_recurrence_field_only#free:23c33b247f32": { - "kind": "Free", - "file": "tui/src/pages/recurring_ui.rs", - "item_path": "crate::tests::suggestion_shows_next_to_the_selected_recurrence_field_only#free:23c33b247f32", - "comment_kind": "Line", - "comment_text": "// Yearly is the only frequency that shows all three boxes at once", - "comment_hash": "23c33b247f3210a5c97398ef89177ea494d2b16f008a92c5f37a32d18275edcf", - "body_text": "data.frequency = \"y\".to_string();\ndata.recur_value = \"mon\".to_string();\ndata.recur_month = \"sep\".to_string();\ndata.autofill = \"SUGGESTION\".to_string();\nfor tab in [TxTab::Frequency, TxTab::RecurValue, TxTab::RecurMonth] {}\nlet screen = render(&data, &TxTab::Nothing);\nassert!(!screen.contains(\"SUGGESTION\"));\n}", - "body_hash": "e0945efb724da0913f67a05abb7cee5e67debb92d69cab336a924a0b88322bdd" - }, "tui/src/pages/search_ui.rs|crate::search_ui#free:055eae7029c0": { "kind": "Free", "file": "tui/src/pages/search_ui.rs", @@ -5532,6 +5862,16 @@ "body_text": "let input_chunk = {};", "body_hash": "f5386f9fc17dadfaabbbb0dd05055e60c81076507060de6a1316b5de81d7f6df" }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:9a52fccaf3aa": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:9a52fccaf3aa", + "comment_kind": "Line", + "comment_text": "// Index 3 is the Amount column", + "comment_hash": "9a52fccaf3aac1a82c405e82ac85ab74e0a5d3b2518e86114cc7b5788cd11c44", + "body_text": "if privacy && index == 3 {}\nlet Ok(parsed_num) = c.parse::() else {};\nlet lerp_id = format!(\"search_table:{index}:{row_index}\");\nlet new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\nCell::from(format!(\"{new_c:.2}\").separate_with_commas())\n}", + "body_hash": "2ff0c4b5b63e3de432177f9c88c343df1fcc1bfdb382ab40d1c80e83a87a667e" + }, "tui/src/pages/search_ui.rs|crate::search_ui#free:9c1666ab54c2": { "kind": "Free", "file": "tui/src/pages/search_ui.rs", @@ -5619,8 +5959,8 @@ "comment_kind": "OuterDoc", "comment_text": " The function draws the Summary page of the interface.\n", "comment_hash": "298cd9d5427f6be27f6c515fe0949d815b411d67bfa387ddfdaea4f0d0779fe0", - "body_text": "pub fn summary_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n table_data: &mut TableData,\n current_page: &SummaryTab,\n summary_hidden_mode: bool,\n summary_sort: &SortingType,\n lerp_state: &mut LerpState,\n full_summary: &FullSummary,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let size = f.area();\n\n let tags_count = table_data.items.len();\n let lerp_id = \"summary_tags_count\";\n let lerp_tags_count = lerp_state.lerp(lerp_id, tags_count as f64, None) as i64;\n\n let lerp_row = lerp_state.lerp(SUMMARY_TABLE_ID, tags_count as f64, Some(0.50)) as usize;\n\n let tag_header = if let SortingType::Tags = summary_sort {\n \"Tags ↑\"\n } else {\n \"Tags\"\n };\n\n let total_income_header = if let SortingType::Income = summary_sort {\n \"Total Income ↑\"\n } else {\n \"Total Income\"\n };\n\n let total_expense_header = if let SortingType::Expense = summary_sort {\n \"Total Expense ↑\"\n } else {\n \"Total Expense\"\n };\n\n let mut tag_table_headers = vec![\n tag_header,\n total_income_header,\n total_expense_header,\n \"Income %\",\n \"Expense %\",\n ];\n\n let mut lend_borrow_headers = vec![\"Borrow\", \"Lend\"];\n\n if mode_selection.index == 0 {\n tag_table_headers.push(\"MoM Income %\");\n tag_table_headers.push(\"MoM Expense %\");\n\n lend_borrow_headers.push(\"MoM Borrow %\");\n lend_borrow_headers.push(\"MoM Lend %\");\n } else if mode_selection.index == 1 {\n tag_table_headers.push(\"YoY Income %\");\n tag_table_headers.push(\"YoY Expense %\");\n\n lend_borrow_headers.push(\"YoY Borrow %\");\n lend_borrow_headers.push(\"YoY Lend %\");\n }\n\n tag_table_headers.push(\"Borrow\");\n tag_table_headers.push(\"Lend\");\n\n let mut method_headers = vec![\"Method\", \"Total Income\", \"Total Expense\"];\n\n if mode_selection.index != 0 {\n method_headers.push(\"Average Income\");\n method_headers.push(\"Average Expense\");\n }\n\n method_headers.push(\"Income %\");\n method_headers.push(\"Expense %\");\n\n if mode_selection.index == 0 {\n method_headers.push(\"MoM Income %\");\n method_headers.push(\"MoM Expense %\");\n } else if mode_selection.index == 1 {\n method_headers.push(\"YoY Income %\");\n method_headers.push(\"YoY Expense %\");\n }\n\n let method_header_cells = method_headers\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let tag_table_header_cells = tag_table_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let lend_borrow_header_cells = lend_borrow_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let tag_table_header = Row::new(tag_table_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_header = Row::new(method_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let lend_borrow_header = Row::new(lend_borrow_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_len = conn.get_tx_methods().len() as u16;\n\n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n let mut summary_layout = Layout::default().direction(Direction::Horizontal);\n\n if summary_hidden_mode {\n main_layout = main_layout.constraints([\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout =\n summary_layout.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n \n let summary_chunk = if summary_hidden_mode {\n summary_layout.split(chunks[3])\n } else {\n summary_layout.split(chunks[6 - mode_selection.index])\n };\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n \n let rows = table_data\n .items\n .iter()\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n if index == 0 {\n return Cell::from(c.clone());\n }\n\n let lerp_id = format!(\"summary_table_main:{index}:{row_index}\");\n\n let Ok(parsed_num) = c.parse::() else {\n if c == \"∞\" {\n let new_c = lerp_state.lerp(&lerp_id, 0.0, None);\n return Cell::from(format!(\"{new_c:.2}\").separate_with_commas());\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n return Cell::from(c.separate_with_commas());\n };\n\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let table_width = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(15),\n Constraint::Percentage(15),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n ]\n } else {\n vec![\n Constraint::Percentage(12),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n ]\n };\n\n let tags_text = format!(\"Tags: {lerp_tags_count}\");\n\n let mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_largest =\n full_summary\n .largest_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_largest:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_peak =\n full_summary\n .peak_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let height = 1;\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_peak:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n let text = if index == 1 && row_index == 2 {\n \n let new_c = new_c as i64;\n format!(\"{new_c}\").separate_with_commas()\n } else {\n format!(\"{new_c:.2}\").separate_with_commas()\n };\n\n Cell::from(text)\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let method_rows =\n full_summary\n .method_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(\n format!(\"{sym}{new_c:.2}\").separate_with_commas(),\n );\n }\n }\n\n if c == \"∞\" {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let method_widths = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n } else if mode_selection.index == 1 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n ]\n } else {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n };\n\n let method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let lerp_id = format!(\"lend_borrow_table:{index}:{row_index}\");\n\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n\n if c == \"∞\" {\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n }\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let lend_borrow_widths = if mode_selection.index == 2 {\n vec![Constraint::Percentage(50), Constraint::Percentage(50)]\n } else {\n vec![\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ]\n };\n\n let lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let net_row = full_summary\n .net_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n if c == \"∞\" {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_net_rows:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n SummaryTab::Table => {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area\n .row_highlight_style(style)\n .highlight_symbol(\">> \");\n }\n }\n\n \n if let Some(index) = table_data.state.selected()\n && index > 7\n {\n *table_data.state.offset_mut() = index - 7;\n }\n\n f.render_widget(summary_area_largest, summary_chunk[1]);\n f.render_widget(summary_area_peak, summary_chunk[0]);\n\n if summary_hidden_mode {\n f.render_stateful_widget(table_area, chunks[4], &mut table_data.state);\n f.render_widget(lend_borrow_area, chunks[2]);\n f.render_widget(net_area, chunks[1]);\n f.render_widget(method_area, chunks[0]);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_stateful_widget(table_area, chunks[7], &mut table_data.state);\n f.render_widget(net_area, chunks[4]);\n f.render_widget(lend_borrow_area, chunks[5]);\n f.render_widget(method_area, chunks[3]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_stateful_widget(table_area, chunks[6], &mut table_data.state);\n f.render_widget(net_area, chunks[3]);\n f.render_widget(lend_borrow_area, chunks[4]);\n f.render_widget(method_area, chunks[2]);\n }\n 2 => {\n f.render_stateful_widget(table_area, chunks[5], &mut table_data.state);\n f.render_widget(net_area, chunks[2]);\n f.render_widget(lend_borrow_area, chunks[3]);\n f.render_widget(method_area, chunks[1]);\n }\n _ => {}\n }\n }\n}", - "body_hash": "ffc8bdd10cd63b6cedfdec042e85020b2bd99beca97f307438ab49b37adb4ca9" + "body_text": "pub fn summary_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n table_data: &mut TableData,\n current_page: &SummaryTab,\n summary_hidden_mode: bool,\n summary_sort: &SortingType,\n lerp_state: &mut LerpState,\n full_summary: &FullSummary,\n theme: &Theme,\n privacy: bool,\n conn: &mut DbConn,\n) {\n \n let mask_row = |row: Vec| {\n if privacy {\n mask_numeric_cells(row, true)\n } else {\n row\n }\n };\n\n let size = f.area();\n\n let tags_count = table_data.items.len();\n let lerp_id = \"summary_tags_count\";\n let lerp_tags_count = lerp_state.lerp(lerp_id, tags_count as f64, None) as i64;\n\n let lerp_row = lerp_state.lerp(SUMMARY_TABLE_ID, tags_count as f64, Some(0.50)) as usize;\n\n let tag_header = if let SortingType::Tags = summary_sort {\n \"Tags ↑\"\n } else {\n \"Tags\"\n };\n\n let total_income_header = if let SortingType::Income = summary_sort {\n \"Total Income ↑\"\n } else {\n \"Total Income\"\n };\n\n let total_expense_header = if let SortingType::Expense = summary_sort {\n \"Total Expense ↑\"\n } else {\n \"Total Expense\"\n };\n\n let mut tag_table_headers = vec![\n tag_header,\n total_income_header,\n total_expense_header,\n \"Income %\",\n \"Expense %\",\n ];\n\n let mut lend_borrow_headers = vec![\"Borrow\", \"Lend\"];\n\n if mode_selection.index == 0 {\n tag_table_headers.push(\"MoM Income %\");\n tag_table_headers.push(\"MoM Expense %\");\n\n lend_borrow_headers.push(\"MoM Borrow %\");\n lend_borrow_headers.push(\"MoM Lend %\");\n } else if mode_selection.index == 1 {\n tag_table_headers.push(\"YoY Income %\");\n tag_table_headers.push(\"YoY Expense %\");\n\n lend_borrow_headers.push(\"YoY Borrow %\");\n lend_borrow_headers.push(\"YoY Lend %\");\n }\n\n tag_table_headers.push(\"Borrow\");\n tag_table_headers.push(\"Lend\");\n\n let mut method_headers = vec![\"Method\", \"Total Income\", \"Total Expense\"];\n\n if mode_selection.index != 0 {\n method_headers.push(\"Average Income\");\n method_headers.push(\"Average Expense\");\n }\n\n method_headers.push(\"Income %\");\n method_headers.push(\"Expense %\");\n\n if mode_selection.index == 0 {\n method_headers.push(\"MoM Income %\");\n method_headers.push(\"MoM Expense %\");\n } else if mode_selection.index == 1 {\n method_headers.push(\"YoY Income %\");\n method_headers.push(\"YoY Expense %\");\n }\n\n let method_header_cells = method_headers\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let tag_table_header_cells = tag_table_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let lend_borrow_header_cells = lend_borrow_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let tag_table_header = Row::new(tag_table_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_header = Row::new(method_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let lend_borrow_header = Row::new(lend_borrow_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_len = conn.get_tx_methods().len() as u16;\n\n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n let mut summary_layout = Layout::default().direction(Direction::Horizontal);\n\n if summary_hidden_mode {\n main_layout = main_layout.constraints([\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout =\n summary_layout.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n \n let summary_chunk = if summary_hidden_mode {\n summary_layout.split(chunks[3])\n } else {\n summary_layout.split(chunks[6 - mode_selection.index])\n };\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n \n let rows = table_data\n .items\n .iter()\n .cloned()\n .map(mask_row)\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n if index == 0 {\n return Cell::from(c.clone());\n }\n\n let lerp_id = format!(\"summary_table_main:{index}:{row_index}\");\n\n let Ok(parsed_num) = c.parse::() else {\n if c == \"∞\" {\n let new_c = lerp_state.lerp(&lerp_id, 0.0, None);\n return Cell::from(format!(\"{new_c:.2}\").separate_with_commas());\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n return Cell::from(c.separate_with_commas());\n };\n\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let table_width = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(15),\n Constraint::Percentage(15),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n ]\n } else {\n vec![\n Constraint::Percentage(12),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n ]\n };\n\n let tags_text = format!(\"Tags: {lerp_tags_count}\");\n\n let mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_largest = full_summary\n .largest_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_largest:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_peak = full_summary\n .peak_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {\n let height = 1;\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_peak:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n let text = if index == 1 && row_index == 2 {\n \n let new_c = new_c as i64;\n format!(\"{new_c}\").separate_with_commas()\n } else {\n format!(\"{new_c:.2}\").separate_with_commas()\n };\n\n Cell::from(text)\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let method_rows = full_summary\n .method_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n\n if c == \"∞\" {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let method_widths = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n } else if mode_selection.index == 1 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n ]\n } else {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n };\n\n let method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .map(|row| {\n if privacy {\n mask_numeric_cells(row, false)\n } else {\n row\n }\n })\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let lerp_id = format!(\"lend_borrow_table:{index}:{row_index}\");\n\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n\n if c == \"∞\" {\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n }\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let lend_borrow_widths = if mode_selection.index == 2 {\n vec![Constraint::Percentage(50), Constraint::Percentage(50)]\n } else {\n vec![\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ]\n };\n\n let lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let net_row = full_summary\n .net_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n if c == \"∞\" {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_net_rows:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n SummaryTab::Table => {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area\n .row_highlight_style(style)\n .highlight_symbol(\">> \");\n }\n }\n\n \n if let Some(index) = table_data.state.selected()\n && index > 7\n {\n *table_data.state.offset_mut() = index - 7;\n }\n\n f.render_widget(summary_area_largest, summary_chunk[1]);\n f.render_widget(summary_area_peak, summary_chunk[0]);\n\n if summary_hidden_mode {\n f.render_stateful_widget(table_area, chunks[4], &mut table_data.state);\n f.render_widget(lend_borrow_area, chunks[2]);\n f.render_widget(net_area, chunks[1]);\n f.render_widget(method_area, chunks[0]);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_stateful_widget(table_area, chunks[7], &mut table_data.state);\n f.render_widget(net_area, chunks[4]);\n f.render_widget(lend_borrow_area, chunks[5]);\n f.render_widget(method_area, chunks[3]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_stateful_widget(table_area, chunks[6], &mut table_data.state);\n f.render_widget(net_area, chunks[3]);\n f.render_widget(lend_borrow_area, chunks[4]);\n f.render_widget(method_area, chunks[2]);\n }\n 2 => {\n f.render_stateful_widget(table_area, chunks[5], &mut table_data.state);\n f.render_widget(net_area, chunks[2]);\n f.render_widget(lend_borrow_area, chunks[3]);\n f.render_widget(method_area, chunks[1]);\n }\n _ => {}\n }\n }\n}", + "body_hash": "05f640b7d07a892fe248a1ee06d7cfc6a648d422aecf3acd2583a69e54637518" }, "tui/src/pages/summary_ui.rs|crate::summary_ui#free:34babd4dfed9": { "kind": "Free", @@ -5639,8 +5979,8 @@ "comment_kind": "Line", "comment_text": "// Goes through all tags provided and creates row for the table", "comment_hash": "bb1db8402aa18ebafd3016fdc3f7ef56028e8da4eb1f856e1397334d70de8880", - "body_text": "let rows = table_data\n .items\n .iter()\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet table_width = if mode_selection.index == 2 {} else {};\nlet tags_text = format!(\"Tags: {lerp_tags_count}\");\nlet mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_largest =\n full_summary\n .largest_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_peak =\n full_summary\n .peak_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet method_rows =\n full_summary\n .method_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet method_widths = if mode_selection.index == 2 {} else if mode_selection.index == 1 {} else {};\nlet method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet lend_borrow_widths = if mode_selection.index == 2 {} else {};\nlet lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet net_row = full_summary\n .net_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\nmatch current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {}\n SummaryTab::Table => {}\n }", - "body_hash": "19509cd3cd46a182cef66682f1de2d2040d8f66e09e08fcc122a86fe70642e84" + "body_text": "let rows = table_data\n .items\n .iter()\n .cloned()\n .map(mask_row)\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet table_width = if mode_selection.index == 2 {} else {};\nlet tags_text = format!(\"Tags: {lerp_tags_count}\");\nlet mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_largest = full_summary\n .largest_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_peak = full_summary\n .peak_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet method_rows = full_summary\n .method_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet method_widths = if mode_selection.index == 2 {} else if mode_selection.index == 1 {} else {};\nlet method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .map(|row| {})\n .enumerate()\n .map(|(row_index, item)| {});\nlet lend_borrow_widths = if mode_selection.index == 2 {} else {};\nlet lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet net_row = full_summary\n .net_array()\n .into_iter()\n .map(mask_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\nmatch current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {}\n SummaryTab::Table => {}\n }", + "body_hash": "662712f22c77e2c13cb2ffad7f3aa58c68c6f1333d6e96ac4c960d7701196136" }, "tui/src/pages/summary_ui.rs|crate::summary_ui#free:f37c8defa895": { "kind": "Free", @@ -5662,6 +6002,16 @@ "body_text": "if let Some(index) = table_data.state.selected()\n && index > 7\n {}\nf.render_widget(summary_area_largest, summary_chunk[1]);\nf.render_widget(summary_area_peak, summary_chunk[0]);\nif summary_hidden_mode {} else {}\n}", "body_hash": "b32f67ba1897d4375205abd641086cdada6d26226a33d61667c6648213a4a54b" }, + "tui/src/pages/summary_ui.rs|crate::summary_ui#free:f59a9e64203d": { + "kind": "Free", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui#free:f59a9e64203d", + "comment_kind": "Line", + "comment_text": "// While privacy mode is on, every number of every table gets masked", + "comment_hash": "f59a9e64203d665e2a05310bd6fc78d8d9fd56c0b5d7066462620c00bab0d139", + "body_text": "let mask_row = |row: Vec| {};\nlet size = f.area();\nlet tags_count = table_data.items.len();\nlet lerp_id = \"summary_tags_count\";\nlet lerp_tags_count = lerp_state.lerp(lerp_id, tags_count as f64, None) as i64;\nlet lerp_row = lerp_state.lerp(SUMMARY_TABLE_ID, tags_count as f64, Some(0.50)) as usize;\nlet tag_header = if let SortingType::Tags = summary_sort {} else {};\nlet total_income_header = if let SortingType::Income = summary_sort {} else {};\nlet total_expense_header = if let SortingType::Expense = summary_sort {} else {};\nlet mut tag_table_headers = vec![\n tag_header,\n total_income_header,\n total_expense_header,\n \"Income %\",\n \"Expense %\",\n ];\nlet mut lend_borrow_headers = vec![\"Borrow\", \"Lend\"];\nif mode_selection.index == 0 {} else if mode_selection.index == 1 {}\ntag_table_headers.push(\"Borrow\");\ntag_table_headers.push(\"Lend\");\nlet mut method_headers = vec![\"Method\", \"Total Income\", \"Total Expense\"];\nif mode_selection.index != 0 {}\nmethod_headers.push(\"Income %\");\nmethod_headers.push(\"Expense %\");\nif mode_selection.index == 0 {} else if mode_selection.index == 1 {}\nlet method_header_cells = method_headers\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet tag_table_header_cells = tag_table_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\nlet lend_borrow_header_cells = lend_borrow_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\nlet tag_table_header = Row::new(tag_table_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet method_header = Row::new(method_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet lend_borrow_header = Row::new(lend_borrow_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet method_len = conn.get_tx_methods().len() as u16;\nlet mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\nlet mut summary_layout = Layout::default().direction(Direction::Horizontal);\nif summary_hidden_mode {} else {}\nlet chunks = main_layout.split(size);", + "body_hash": "16d3bc183e4672b367b0e9e9fac9ad7b267db5962df7554bbe409073126f2866" + }, "tui/src/tx_handler/tx_data.rs|crate::TxData": { "kind": "Struct", "file": "tui/src/tx_handler/tx_data.rs", @@ -6472,26 +6822,6 @@ "body_text": "#[must_use]\npub fn start_terminal(work_dir: &Path) -> bool {\n let exe = env::current_exe().unwrap_or_else(|_| PathBuf::from(\"rex\"));\n let terminal_env = env::var(\"TERMINAL\").ok();\n let desktop = env::var(\"XDG_CURRENT_DESKTOP\").unwrap_or_default();\n\n candidates(\n Platform::current(),\n &exe,\n work_dir,\n terminal_env.as_deref(),\n &desktop,\n )\n .iter()\n .any(|launcher| try_launch(launcher, work_dir))\n}", "body_hash": "9dcc6cbd6db0bd9a22a1bf854d6f8d149e075fc7c089e9e59ba90f4ee8943c0a" }, - "tui/src/utility/terminal_launcher.rs|crate::tests::terminal_env_with_a_known_name_uses_that_terminals_flags#free:82424a146901": { - "kind": "Free", - "file": "tui/src/utility/terminal_launcher.rs", - "item_path": "crate::tests::terminal_env_with_a_known_name_uses_that_terminals_flags#free:82424a146901", - "comment_kind": "Line", - "comment_text": "// kitty doesn't take -e, so it must use its own recipe rather than the generic one", - "comment_hash": "82424a146901c44ed9b7565c95a7f93cbd81a31e9f4a95187c8f3b697d251594", - "body_text": "assert_eq!(launchers[0].program, \"kitty\");\nassert_eq!(\n launchers[0].args,\n os(&[\"--directory\", \"/home/user/projects\", \"/opt/rex/rex\"])\n );", - "body_hash": "bab81d6b96eedf722b0a452a8e21d2fd4135ac70cb12e279c9ae3ec9661d658d" - }, - "tui/src/utility/terminal_launcher.rs|crate::tests::terminal_env_with_a_known_name_uses_that_terminals_flags#free:9b364e83b15d": { - "kind": "Free", - "file": "tui/src/utility/terminal_launcher.rs", - "item_path": "crate::tests::terminal_env_with_a_known_name_uses_that_terminals_flags#free:9b364e83b15d", - "comment_kind": "Line", - "comment_text": "// Nothing is duplicated", - "comment_hash": "9b364e83b15d8abb2be76114a48c57892aacf3642c20685692200c733300fd39", - "body_text": "assert_eq!(launchers.iter().filter(|l| l.program == \"kitty\").count(), 1);\n}", - "body_hash": "997db8626a9de98688a5e358dfb59768f8976a99964140f4c8095c7bd535797a" - }, "tui/src/utility/utils.rs|crate::add_char_to": { "kind": "Function", "file": "tui/src/utility/utils.rs", From d9c010b92f7c58ac3822a3364c8d64af01e5a7f8 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Tue, 20 Oct 2026 14:57:24 +0600 Subject: [PATCH 2/4] Update reade --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a6e970e..be7b304 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ percentage distribution. * Find transactions quickly using partial or specific information. * Organize transactions with custom tags for easy filtering. * Add recurring transactions with flexible interval rules. +* Privacy mode for hiding balances and amounts. +* Password protection option for the DB with SQLite cipher. * Works fully offline.

Installation

From d5249d22f9e2a1c9f9c19ca7da70f636f3b8b3c7 Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Tue, 20 Oct 2026 14:58:23 +0600 Subject: [PATCH 3/4] Add P key handler for privacy mode --- tui/src/config.rs | 41 +++-------- tui/src/key_checker/activity.rs | 1 + tui/src/key_checker/add_tx.rs | 1 + tui/src/key_checker/chart.rs | 1 + tui/src/key_checker/home.rs | 1 + tui/src/key_checker/key_handler.rs | 112 ++--------------------------- tui/src/key_checker/recurring.rs | 1 + tui/src/key_checker/search.rs | 1 + tui/src/key_checker/summary.rs | 1 + tui/src/page_handler/ui_handler.rs | 9 +++ 10 files changed, 30 insertions(+), 139 deletions(-) diff --git a/tui/src/config.rs b/tui/src/config.rs index 6f81595..0f66768 100644 --- a/tui/src/config.rs +++ b/tui/src/config.rs @@ -23,8 +23,8 @@ pub struct Config { pub new_location: Option, pub theme_index: Option, pub migration_v1_done: Option, - /// Whether the database is encrypted and a password is asked on startup pub db_encrypted: Option, + pub privacy_mode: Option, } impl Config { @@ -42,6 +42,7 @@ impl Config { theme_index: Some(0), migration_v1_done: None, db_encrypted: None, + privacy_mode: None, }); } @@ -168,6 +169,11 @@ impl Config { self.save_config() } + pub fn set_privacy_mode(&mut self, privacy_mode: bool) -> Result<()> { + self.privacy_mode = Some(privacy_mode); + self.save_config() + } + pub fn set_migration_v1_done(&mut self) -> Result<()> { self.migration_v1_done = Some(true); self.save_config() @@ -182,6 +188,7 @@ pub fn migrate_config(config_path: &PathBuf) -> Result<()> { theme_index: Some(0), migration_v1_done: None, db_encrypted: None, + privacy_mode: None, }; let mut backup_path = config_path.to_owned(); @@ -246,35 +253,3 @@ pub fn migrate_config(config_path: &PathBuf) -> Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn config_from_before_encryption_loads_as_not_encrypted() { - let config: Config = serde_json::from_str( - r#"{"backup_db_path":null,"new_location":null,"theme_index":1,"migration_v1_done":true}"#, - ) - .unwrap(); - - assert_eq!(config.db_encrypted, None); - assert_eq!(config.migration_v1_done, Some(true)); - } - - #[test] - fn encrypted_flag_is_saved_and_loaded() { - let dir = std::env::temp_dir().join("rex_config_encrypted_test"); - fs::create_dir_all(&dir).unwrap(); - - let mut config = Config::get_config(&dir.join("rex.sqlite")).unwrap(); - assert_eq!(config.db_encrypted, None); - - config.set_db_encrypted(true).unwrap(); - - let loaded = Config::get_config(&dir.join("rex.sqlite")).unwrap(); - assert_eq!(loaded.db_encrypted, Some(true)); - - fs::remove_dir_all(dir).unwrap(); - } -} diff --git a/tui/src/key_checker/activity.rs b/tui/src/key_checker/activity.rs index 88f2e6e..7cfa9d3 100644 --- a/tui/src/key_checker/activity.rs +++ b/tui/src/key_checker/activity.rs @@ -19,6 +19,7 @@ pub fn activity_keys(handler: &mut InputKeyHandler) -> Result handler.go_recurring(), KeyCode::Char('v') => handler.show_activity_tx_details()?, KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, KeyCode::Up => handler.handle_up_arrow(), diff --git a/tui/src/key_checker/add_tx.rs b/tui/src/key_checker/add_tx.rs index fd39bcf..4796ed6 100644 --- a/tui/src/key_checker/add_tx.rs +++ b/tui/src/key_checker/add_tx.rs @@ -24,6 +24,7 @@ pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result handler.go_activity(), KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Enter => handler.select_date_field(), KeyCode::Char(c) if c.is_numeric() => { handler.handle_number_press(); diff --git a/tui/src/key_checker/chart.rs b/tui/src/key_checker/chart.rs index c1cce82..c54440d 100644 --- a/tui/src/key_checker/chart.rs +++ b/tui/src/key_checker/chart.rs @@ -21,6 +21,7 @@ pub fn chart_keys(handler: &mut InputKeyHandler) -> Result handler.go_activity(), KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, KeyCode::Up => handler.handle_up_arrow(), diff --git a/tui/src/key_checker/home.rs b/tui/src/key_checker/home.rs index bd774b3..2cfbcc1 100644 --- a/tui/src/key_checker/home.rs +++ b/tui/src/key_checker/home.rs @@ -24,6 +24,7 @@ pub fn home_keys(handler: &mut InputKeyHandler) -> Result KeyCode::Char('.') => handler.switch_tx_position_down()?, KeyCode::Char('v') => handler.show_home_tx_details(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, KeyCode::Up => handler.handle_up_arrow(), diff --git a/tui/src/key_checker/key_handler.rs b/tui/src/key_checker/key_handler.rs index e804fce..5abccfc 100644 --- a/tui/src/key_checker/key_handler.rs +++ b/tui/src/key_checker/key_handler.rs @@ -1332,6 +1332,12 @@ impl<'a> InputKeyHandler<'a> { self.popup_status.next(); } + /// Turns privacy mode on or off and saves it so it carries over to the next launch + pub fn toggle_privacy(&mut self) -> Result<()> { + let enabled = !self.config.privacy_mode.unwrap_or(false); + self.config.set_privacy_mode(enabled) + } + pub fn next_theme(&mut self) -> Result<()> { let new_index = self.theme.next(); self.config.theme_index = Some(new_index); @@ -2976,109 +2982,3 @@ fn recurring_number_jump_tabs(tx_type: TxType, frequency: RecurrenceFrequency) - fn digit_to_tab_index(digit: u32) -> usize { if digit == 0 { 9 } else { digit as usize - 1 } } - -#[cfg(test)] -mod number_jump_tests { - use super::*; - - #[test] - fn digit_to_tab_index_maps_1_through_9_to_0_through_8() { - for digit in 1..=9 { - assert_eq!(digit_to_tab_index(digit), digit as usize - 1); - } - } - - #[test] - fn digit_to_tab_index_maps_0_to_the_tenth_field() { - assert_eq!(digit_to_tab_index(0), 9); - } - - #[test] - fn income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable() { - let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Daily); - assert_eq!( - tabs, - vec![ - TxTab::Date, - TxTab::Details, - TxTab::TxType, - TxTab::FromMethod, - TxTab::Amount, - TxTab::Frequency, - TxTab::RecurInterval, - TxTab::EndDate, - TxTab::Tags, - ] - ); - assert_eq!(tabs[digit_to_tab_index(8)], TxTab::EndDate); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::Tags); - // No 10th field in this combination, so key 0 has nothing to jump to - assert!(tabs.get(digit_to_tab_index(0)).is_none()); - } - - #[test] - fn income_expense_weekly_or_monthly_has_ten_fields_both_end_date_and_tags_reachable() { - let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Weekly); - assert_eq!(tabs.len(), 10); - assert_eq!(tabs[digit_to_tab_index(8)], TxTab::RecurValue); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::EndDate); - assert_eq!(tabs[digit_to_tab_index(0)], TxTab::Tags); - } - - #[test] - fn income_expense_yearly_has_eleven_fields_tags_has_no_key_left() { - let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Yearly); - assert_eq!(tabs.len(), 11); - assert_eq!(tabs[digit_to_tab_index(8)], TxTab::RecurValue); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurMonth); - assert_eq!(tabs[digit_to_tab_index(0)], TxTab::EndDate); - // Tags is the 11th field - out of digits (1-9 then 0 only covers 10) - assert!(tabs.get(10).is_some()); - assert_eq!(tabs[10], TxTab::Tags); - } - - #[test] - fn transfer_daily_has_ten_fields_both_end_date_and_tags_reachable() { - let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Daily); - assert_eq!(tabs.len(), 10); - assert_eq!(tabs[4], TxTab::ToMethod); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::EndDate); - assert_eq!(tabs[digit_to_tab_index(0)], TxTab::Tags); - } - - #[test] - fn transfer_weekly_or_monthly_has_eleven_fields_tags_has_no_key_left() { - let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Monthly); - assert_eq!(tabs.len(), 11); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurValue); - assert_eq!(tabs[digit_to_tab_index(0)], TxTab::EndDate); - assert_eq!(tabs[10], TxTab::Tags); - } - - #[test] - fn transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left() { - let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Yearly); - assert_eq!(tabs.len(), 12); - assert_eq!( - &tabs[..10], - [ - TxTab::Date, - TxTab::Details, - TxTab::TxType, - TxTab::FromMethod, - TxTab::ToMethod, - TxTab::Amount, - TxTab::Frequency, - TxTab::RecurInterval, - TxTab::RecurValue, - TxTab::RecurMonth, - ] - ); - assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurValue); - assert_eq!(tabs[digit_to_tab_index(0)], TxTab::RecurMonth); - // End Date and Tags are the 11th/12th fields here - genuinely out of digits, - // only reachable via sequential Enter in this one combination - assert_eq!(tabs[10], TxTab::EndDate); - assert_eq!(tabs[11], TxTab::Tags); - } -} diff --git a/tui/src/key_checker/recurring.rs b/tui/src/key_checker/recurring.rs index af9c306..f250c10 100644 --- a/tui/src/key_checker/recurring.rs +++ b/tui/src/key_checker/recurring.rs @@ -27,6 +27,7 @@ pub fn recurring_keys(handler: &mut InputKeyHandler) -> Result handler.go_search(), KeyCode::Char('y') => handler.go_activity(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Up => handler.handle_up_arrow(), KeyCode::Down => handler.handle_down_arrow(), KeyCode::Enter => handler.select_date_field(), diff --git a/tui/src/key_checker/search.rs b/tui/src/key_checker/search.rs index 7abe563..26807b9 100644 --- a/tui/src/key_checker/search.rs +++ b/tui/src/key_checker/search.rs @@ -27,6 +27,7 @@ pub fn search_keys(handler: &mut InputKeyHandler) -> Result handler.go_activity(), KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Up => handler.handle_up_arrow(), KeyCode::Down => handler.handle_down_arrow(), KeyCode::Enter => handler.select_date_field(), diff --git a/tui/src/key_checker/summary.rs b/tui/src/key_checker/summary.rs index 0ab3860..8955185 100644 --- a/tui/src/key_checker/summary.rs +++ b/tui/src/key_checker/summary.rs @@ -21,6 +21,7 @@ pub fn summary_keys(handler: &mut InputKeyHandler) -> Result handler.go_activity(), KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Char('P') => handler.toggle_privacy()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, KeyCode::Up => handler.handle_up_arrow(), diff --git a/tui/src/page_handler/ui_handler.rs b/tui/src/page_handler/ui_handler.rs index 2d305c9..278a2e3 100644 --- a/tui/src/page_handler/ui_handler.rs +++ b/tui/src/page_handler/ui_handler.rs @@ -210,6 +210,8 @@ pub fn start_app( popup_status = PopupType::new_choice_config_forced(&theme); } + let privacy = config.privacy_mode.unwrap_or(false); + // Passing out relevant data to the UI function terminal .draw(|f| { @@ -223,6 +225,7 @@ pub fn start_app( &mut lerp_state, &mut home_txs, &theme, + privacy, conn, ), @@ -233,6 +236,7 @@ pub fn start_app( &add_tx_tab, &mut lerp_state, &theme, + privacy, conn, ), @@ -251,6 +255,7 @@ pub fn start_app( &mut lerp_state, &chart_view, &theme, + privacy, conn, ), @@ -266,6 +271,7 @@ pub fn start_app( &mut lerp_state, &full_summary, &theme, + privacy, conn, ), CurrentUi::Search => search_ui( @@ -276,6 +282,7 @@ pub fn start_app( search_date_type, &mut lerp_state, &theme, + privacy, ), CurrentUi::Activity => activity_ui( f, @@ -286,6 +293,7 @@ pub fn start_app( &mut activity_table, &mut lerp_state, &theme, + privacy, ), CurrentUi::Recurring => recurring_ui( f, @@ -294,6 +302,7 @@ pub fn start_app( &mut recurring_table, &mut lerp_state, &theme, + privacy, ), } From cbc5f6c2673732cfe5c5fc1e365c330508261e6c Mon Sep 17 00:00:00 2001 From: Rusty Pickle Date: Tue, 20 Oct 2026 14:58:38 +0600 Subject: [PATCH 4/4] Add privacy mode on ui --- tui/src/page_handler/unlock.rs | 51 ------ tui/src/pages/activity_ui.rs | 12 +- tui/src/pages/add_tx_ui.rs | 7 +- tui/src/pages/chart_ui.rs | 26 ++- tui/src/pages/home_ui.rs | 14 +- tui/src/pages/initial_ui.rs | 144 ---------------- tui/src/pages/popups/help_text.rs | 8 + tui/src/pages/popups/models.rs | 120 -------------- tui/src/pages/recurring_ui.rs | 61 +------ tui/src/pages/search_ui.rs | 8 +- tui/src/pages/summary_ui.rs | 236 ++++++++++++++------------- tui/src/tx_handler/tx_data.rs | 41 ----- tui/src/utility/terminal_launcher.rs | 215 ------------------------ tui/src/utility/utils.rs | 19 +++ 14 files changed, 216 insertions(+), 746 deletions(-) diff --git a/tui/src/page_handler/unlock.rs b/tui/src/page_handler/unlock.rs index 8efdabc..3923782 100644 --- a/tui/src/page_handler/unlock.rs +++ b/tui/src/page_handler/unlock.rs @@ -109,54 +109,3 @@ fn show_unlock_ui(f: &mut Frame, theme: &Theme, password_len: usize, error: Opti chunks[3], ); } - -#[cfg(test)] -mod tests { - use ratatui::Terminal; - use ratatui::backend::TestBackend; - - use super::*; - use crate::theme::ThemeVariant; - - fn render(password_len: usize, error: Option<&str>) -> String { - let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap(); - let theme = Theme::new(ThemeVariant::Dracula); - - terminal - .draw(|f| show_unlock_ui(f, &theme, password_len, error)) - .unwrap(); - - terminal - .backend() - .buffer() - .content() - .iter() - .map(|cell| cell.symbol()) - .collect() - } - - #[test] - fn typed_characters_are_masked() { - let screen = render(6, None); - - assert!(screen.contains("******")); - assert!(!screen.contains("*******")); - assert!(screen.contains("Rex - Locked")); - } - - #[test] - fn wrong_password_message_is_shown() { - assert!(render(0, Some("Wrong password")).contains("Wrong password")); - assert!(!render(0, None).contains("Wrong password")); - } - - #[test] - fn fits_a_small_terminal_without_panicking() { - let mut terminal = Terminal::new(TestBackend::new(30, 8)).unwrap(); - let theme = Theme::new(ThemeVariant::Light); - - terminal - .draw(|f| show_unlock_ui(f, &theme, 40, Some("Wrong password"))) - .unwrap(); - } -} diff --git a/tui/src/pages/activity_ui.rs b/tui/src/pages/activity_ui.rs index c6db7ff..5d74dd4 100644 --- a/tui/src/pages/activity_ui.rs +++ b/tui/src/pages/activity_ui.rs @@ -7,7 +7,9 @@ use thousands::Separable; use crate::page_handler::{ActivityTab, IndexedData, TableData}; use crate::theme::Theme; -use crate::utility::{LerpState, create_tab, main_block, styled_block, tab_highlight_style}; +use crate::utility::{ + LerpState, PRIVACY_MASK, create_tab, main_block, styled_block, tab_highlight_style, +}; pub const ACTIVITY_TABLE_ID: &str = "activity_table_row"; @@ -20,6 +22,7 @@ pub fn activity_ui( table_data: &mut TableData, lerp_state: &mut LerpState, theme: &Theme, + privacy: bool, ) { let activity_txs_data = activity_view.get_activity_txs_table(table_data.state.selected()); let mut activity_txs_table = TableData::new(activity_txs_data); @@ -122,7 +125,12 @@ pub fn activity_ui( // The first column is the date, which gets no separators. A yearly search // would otherwise end up with a comma in the year let mut first_index_passed = false; - let cells = item.iter().map(|c| { + let cells = item.iter().enumerate().map(|(index, c)| { + // Index 3 is the Amount column + if privacy && index == 3 { + return Cell::from(PRIVACY_MASK); + } + if first_index_passed { Cell::from(c.separate_with_commas()) } else { diff --git a/tui/src/pages/add_tx_ui.rs b/tui/src/pages/add_tx_ui.rs index 4e771fb..60e0ac7 100644 --- a/tui/src/pages/add_tx_ui.rs +++ b/tui/src/pages/add_tx_ui.rs @@ -11,7 +11,7 @@ use crate::page_handler::{HomeRow, LogType, TxTab}; use crate::pages::BALANCE_BOLD; use crate::theme::Theme; use crate::tx_handler::TxData; -use crate::utility::{LerpState, main_block, styled_block}; +use crate::utility::{LerpState, PRIVACY_MASK, main_block, styled_block}; /// The function draws the Add Transaction page of the interface. pub fn add_tx_ui( @@ -21,6 +21,7 @@ pub fn add_tx_ui( add_tx_tab: &TxTab, lerp_state: &mut LerpState, theme: &Theme, + privacy: bool, conn: &mut DbConn, ) { let all_methods: Vec = conn @@ -118,6 +119,10 @@ pub fn add_tx_ui( None }; + if privacy { + return Cell::from(PRIVACY_MASK); + } + // If loading was complete then this value is to be shown let actual_data: f64 = if row_type != HomeRow::Changes { c.parse().unwrap() diff --git a/tui/src/pages/chart_ui.rs b/tui/src/pages/chart_ui.rs index 1f4e3a3..bfaef70 100644 --- a/tui/src/pages/chart_ui.rs +++ b/tui/src/pages/chart_ui.rs @@ -1,10 +1,10 @@ use chrono::{Duration, naive::NaiveDate}; use ratatui::Frame; -use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Style}; use ratatui::symbols::Marker; use ratatui::text::Span; -use ratatui::widgets::{Axis, Block, Chart, Dataset, GraphType}; +use ratatui::widgets::{Axis, Block, Chart, Dataset, GraphType, Paragraph}; use rex_app::conn::DbConn; use rex_app::views::ChartView; use std::collections::HashMap; @@ -29,6 +29,7 @@ pub fn chart_ui( lerp_state: &mut LerpState, chart_view: &ChartView, theme: &Theme, + privacy: bool, conn: &mut DbConn, ) { let size = f.area(); @@ -350,8 +351,11 @@ pub fn chart_ui( } } + // The chart is always the last chunk + let chart_chunk = chunks[chunks.len() - 1]; + if chart_hidden_mode { - f.render_widget(chart, chunks[0]); + render_chart(f, chart, chart_chunk, privacy, theme); } else { f.render_widget(mode_selection_tab, chunks[0]); @@ -360,18 +364,28 @@ pub fn chart_ui( f.render_widget(year_tab, chunks[1]); f.render_widget(month_tab, chunks[2]); f.render_widget(tx_method_selection_tab, chunks[3]); - f.render_widget(chart, chunks[4]); } 1 => { f.render_widget(year_tab, chunks[1]); f.render_widget(tx_method_selection_tab, chunks[2]); - f.render_widget(chart, chunks[3]); } 2 => { f.render_widget(tx_method_selection_tab, chunks[1]); - f.render_widget(chart, chunks[2]); } _ => {} } + + render_chart(f, chart, chart_chunk, privacy, theme); + } +} + +fn render_chart(f: &mut Frame, chart: Chart, area: Rect, privacy: bool, theme: &Theme) { + if privacy { + let placeholder = Paragraph::new("Privacy mode is on. Press P to show the chart") + .alignment(Alignment::Center) + .style(Style::default().bg(theme.background()).fg(theme.border())); + f.render_widget(placeholder, area); + } else { + f.render_widget(chart, area); } } diff --git a/tui/src/pages/home_ui.rs b/tui/src/pages/home_ui.rs index 59369e0..b8d9425 100644 --- a/tui/src/pages/home_ui.rs +++ b/tui/src/pages/home_ui.rs @@ -8,7 +8,9 @@ use thousands::Separable; use crate::page_handler::{HomeRow, HomeTab, IndexedData, TableData}; use crate::theme::Theme; -use crate::utility::{LerpState, create_tab, main_block, styled_block, tab_highlight_style}; +use crate::utility::{ + LerpState, PRIVACY_MASK, create_tab, main_block, styled_block, tab_highlight_style, +}; pub const HOME_TABLE_ID: &str = "home_table_row"; @@ -32,6 +34,7 @@ pub fn home_ui( lerp_state: &mut LerpState, view_group: &mut TxViewGroup, theme: &Theme, + privacy: bool, conn: &mut DbConn, ) { let all_methods: Vec = conn @@ -84,6 +87,11 @@ pub fn home_ui( .enumerate() .map(|(row_index, item)| { let cells = item.iter().enumerate().map(|(index, c)| { + // Index 3 is the Amount column + if privacy && index == 3 { + return Cell::from(PRIVACY_MASK); + } + let Ok(parsed_num) = c.parse::() else { return Cell::from(c.separate_with_commas()); }; @@ -170,6 +178,10 @@ pub fn home_ui( index += 1; + if privacy { + return Cell::from(PRIVACY_MASK); + } + // If loading was complete then this value is to be shown let actual_data: f64 = if row_type != HomeRow::Changes { c.parse().unwrap() diff --git a/tui/src/pages/initial_ui.rs b/tui/src/pages/initial_ui.rs index e0da493..3859ee8 100644 --- a/tui/src/pages/initial_ui.rs +++ b/tui/src/pages/initial_ui.rs @@ -414,147 +414,3 @@ fn render_recurring(f: &mut Frame, area: ratatui::layout::Rect, data: &InitialDa f.render_widget(table, sections[1]); } - -#[cfg(test)] -mod tests { - use super::*; - - fn date(y: i32, m: u32, d: u32) -> NaiveDate { - NaiveDate::from_ymd_opt(y, m, d).unwrap() - } - - #[test] - fn upcoming_includes_today_and_the_last_day_of_the_window() { - let today = date(2026, 9, 29); - assert!(is_upcoming(today, None, false, today, 7)); - assert!(is_upcoming(date(2026, 10, 6), None, false, today, 7)); - } - - #[test] - fn upcoming_excludes_outside_the_window() { - let today = date(2026, 9, 29); - assert!(!is_upcoming(date(2026, 10, 7), None, false, today, 7)); - assert!(!is_upcoming(date(2026, 9, 28), None, false, today, 7)); - } - - #[test] - fn upcoming_excludes_paused_rules() { - let today = date(2026, 9, 29); - assert!(!is_upcoming(date(2026, 9, 30), None, true, today, 7)); - } - - #[test] - fn upcoming_excludes_rules_past_their_end_date() { - let today = date(2026, 9, 29); - let next = date(2026, 10, 2); - assert!(!is_upcoming(next, Some(date(2026, 10, 1)), false, today, 7)); - assert!(is_upcoming(next, Some(date(2026, 10, 2)), false, today, 7)); - } - - #[test] - fn format_cents_adds_separators_and_keeps_sign() { - assert_eq!(format_cents(123_456_789), "1,234,567.89"); - assert_eq!(format_cents(-250), "-2.50"); - assert_eq!(format_cents(0), "0.00"); - } - - fn sample_data() -> InitialData { - InitialData { - recurring_added: 3, - balances: vec![ - ("Cash".to_string(), "1,200.50".to_string(), false), - ("Bank".to_string(), "-40.00".to_string(), true), - ], - total: Some(("1,160.50".to_string(), false)), - upcoming: vec![UpcomingRow { - date: "Wed Sep 30".to_string(), - details: "Rent".to_string(), - amount: "800.00".to_string(), - sign: Sign::Negative, - }], - paused_count: 1, - } - } - - fn render(width: u16, height: u16, data: &InitialData) -> String { - use ratatui::Terminal; - use ratatui::backend::TestBackend; - - let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); - let theme = Theme::new_index(0); - terminal.draw(|f| initial_ui(f, 5, data, &theme)).unwrap(); - - let buffer = terminal.backend().buffer().clone(); - (0..height) - .map(|y| { - (0..width) - .map(|x| buffer[(x, y)].symbol().to_string()) - .collect::() - }) - .collect::>() - .join("\n") - } - - #[test] - fn renders_balances_recurring_and_keys() { - let screen = render(120, 40, &sample_data()); - - assert!(screen.contains("Balances")); - assert!(screen.contains("1,200.50")); - assert!(screen.contains("Total")); - assert!(screen.contains("3 recurring transactions were added on startup")); - assert!(screen.contains("1 paused")); - assert!(screen.contains("Rent")); - assert!(screen.contains("U: Recurring Transactions Page")); - } - - #[test] - fn renders_empty_states() { - let data = InitialData { - recurring_added: 0, - balances: Vec::new(), - total: None, - upcoming: Vec::new(), - paused_count: 0, - }; - let screen = render(120, 40, &data); - - assert!(screen.contains("No transaction methods yet")); - assert!(screen.contains("No recurring transactions were due on startup")); - assert!(screen.contains("Nothing coming up")); - } - - #[test] - fn does_not_panic_on_tiny_terminals() { - for (w, h) in [(1, 1), (20, 5), (40, 12), (80, 24)] { - render(w, h, &sample_data()); - } - } - - #[test] - fn table_text_uses_the_text_colour_not_the_border_colour() { - use ratatui::Terminal; - use ratatui::backend::TestBackend; - - for idx in 0..12 { - let theme = Theme::new_index(idx); - let mut terminal = Terminal::new(TestBackend::new(120, 36)).unwrap(); - let data = sample_data(); - terminal.draw(|f| initial_ui(f, 5, &data, &theme)).unwrap(); - let buffer = terminal.backend().buffer().clone(); - - for word in ["Cash", "Total", "Rent", "Wed Sep 30"] { - let (x, y) = (0..36u16) - .find_map(|y| { - let row: String = (0..120u16) - .map(|x| buffer[(x, y)].symbol().to_string()) - .collect(); - row.find(word).map(|x| (x as u16, y)) - }) - .unwrap(); - - assert_eq!(buffer[(x, y)].fg, theme.text(), "{word} in theme {idx}"); - } - } - } -} diff --git a/tui/src/pages/popups/help_text.rs b/tui/src/pages/popups/help_text.rs index 4eaad1c..580044e 100644 --- a/tui/src/pages/popups/help_text.rs +++ b/tui/src/pages/popups/help_text.rs @@ -10,6 +10,7 @@ pub const H: &str = "H: Show help"; pub const V: &str = "V: Show selected transaction details"; pub const J: &str = "J: Configuration"; pub const T: &str = "T: Cycle through themes"; +pub const PRIVACY: &str = "Shift+P: Hide or show balances and amounts"; pub fn new_update_text(data: &[String]) -> String { format!( @@ -54,6 +55,7 @@ Example amount: 100 + b, b + b, 5 * b, 1.2k + 1m {F} {T} +{PRIVACY} {R} {Z} {Y} @@ -81,6 +83,7 @@ Arrow Left/Right: Move value of the widget {F} {T} +{PRIVACY} {A} {Z} {Y} @@ -108,6 +111,7 @@ Arrow Left/Right: Move value of the widget {F} {T} +{PRIVACY} {A} {R} {Y} @@ -138,6 +142,7 @@ Swapping transaction location will only work if they are on the same date. {A} {T} +{PRIVACY} {R} {Z} {Y} @@ -194,6 +199,7 @@ Example amount : <1000, >=10000 {F} {T} +{PRIVACY} {A} {R} {Z} @@ -220,6 +226,7 @@ Arrow Left/Right: Move value of the widget {F} {T} +{PRIVACY} {A} {R} {Z} @@ -271,6 +278,7 @@ Calculation: Amount field supports simple calculation with +, -, *, / {F} {T} +{PRIVACY} {A} {R} {Z} diff --git a/tui/src/pages/popups/models.rs b/tui/src/pages/popups/models.rs index 8e0f3e1..a76b3e6 100644 --- a/tui/src/pages/popups/models.rs +++ b/tui/src/pages/popups/models.rs @@ -797,123 +797,3 @@ impl PopupType { } } } - -#[cfg(test)] -mod password_tests { - use super::*; - - fn type_text(popup: &mut PopupType, text: &str) { - for c in text.chars() { - popup.password_char(Some(c)); - } - } - - fn status(popup: &PopupType) -> &str { - let PopupType::Password(password) = popup else { - panic!("not a password popup"); - }; - &password.status - } - - fn stage(popup: &PopupType) -> PasswordStage { - let PopupType::Password(password) = popup else { - panic!("not a password popup"); - }; - password.stage - } - - #[test] - fn matching_passwords_are_accepted_after_confirming() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "correct horse"); - assert!(matches!(popup.accept_password(), PasswordOutcome::Pending)); - assert_eq!(stage(&popup), PasswordStage::Confirm); - - type_text(&mut popup, "correct horse"); - let PasswordOutcome::Accepted(password) = popup.accept_password() else { - panic!("should have been accepted"); - }; - assert_eq!(password, "correct horse"); - } - - #[test] - fn short_passwords_are_refused_and_stay_on_the_first_step() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "1234567"); - assert!(matches!(popup.accept_password(), PasswordOutcome::Pending)); - - assert_eq!(stage(&popup), PasswordStage::Enter); - assert!(status(&popup).contains("at least 8")); - } - - #[test] - fn exactly_the_minimum_length_is_enough() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "12345678"); - popup.accept_password(); - - assert_eq!(stage(&popup), PasswordStage::Confirm); - } - - #[test] - fn length_counts_characters_not_bytes() { - let mut popup = PopupType::new_password(false); - - // 4 characters, 8 bytes - type_text(&mut popup, "éééé"); - popup.accept_password(); - - assert_eq!(stage(&popup), PasswordStage::Enter); - } - - #[test] - fn a_mismatch_starts_over_and_clears_both_entries() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "correct horse"); - popup.accept_password(); - type_text(&mut popup, "correct horsf"); - assert!(matches!(popup.accept_password(), PasswordOutcome::Pending)); - - assert_eq!(stage(&popup), PasswordStage::Enter); - assert!(status(&popup).contains("did not match")); - - // The first attempt must not linger as the thing to match - type_text(&mut popup, "another one"); - popup.accept_password(); - type_text(&mut popup, "correct horse"); - assert!(matches!(popup.accept_password(), PasswordOutcome::Pending)); - } - - #[test] - fn backspace_removes_the_last_character() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "abc"); - popup.password_char(None); - popup.password_char(None); - popup.password_char(None); - // Nothing left to remove - popup.password_char(None); - - let PopupType::Password(password) = &popup else { - panic!(); - }; - assert!(password.text.is_empty()); - } - - #[test] - fn a_failure_shows_its_reason_and_starts_over() { - let mut popup = PopupType::new_password(false); - - type_text(&mut popup, "correct horse"); - popup.accept_password(); - popup.password_failed("disk is full"); - - assert_eq!(stage(&popup), PasswordStage::Enter); - assert!(status(&popup).contains("disk is full")); - } -} diff --git a/tui/src/pages/recurring_ui.rs b/tui/src/pages/recurring_ui.rs index 24e8145..7adc793 100644 --- a/tui/src/pages/recurring_ui.rs +++ b/tui/src/pages/recurring_ui.rs @@ -10,7 +10,7 @@ use crate::outputs::TxType; use crate::page_handler::{LogType, TableData, TxTab}; use crate::theme::Theme; use crate::tx_handler::TxData; -use crate::utility::{LerpState, main_block, styled_block}; +use crate::utility::{LerpState, PRIVACY_MASK, main_block, styled_block}; pub const RECURRING_TABLE_ID: &str = "recurring_table_row"; @@ -21,6 +21,7 @@ pub fn recurring_ui( recurring_table: &mut TableData, lerp_state: &mut LerpState, theme: &Theme, + privacy: bool, ) { let status_data = recurring_data.get_tx_status(); // date, details, from method, to method, amount, tx type, tags, autofill @@ -69,9 +70,10 @@ pub fn recurring_ui( let rows = recurring_table.items.iter().take(lerp_row).map(|item| { let height = 1; let cells = item.iter().enumerate().map(|(index, c)| { - // Index 4 is the Amount column - the only one that should get thousands - // separators - if index == 4 { + // Index 4 is the Amount column + if privacy && index == 4 { + Cell::from(PRIVACY_MASK) + } else if index == 4 { Cell::from(c.separate_with_commas()) } else { Cell::from(c.clone()) @@ -472,54 +474,3 @@ pub fn recurring_ui( f.render_stateful_widget(table_area, chunks[4], &mut recurring_table.state); } - -#[cfg(test)] -mod tests { - use super::*; - use ratatui::Terminal; - use ratatui::backend::TestBackend; - - fn render(data: &TxData, tab: &TxTab) -> String { - let (width, height) = (140, 40); - let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); - let theme = Theme::new_index(0); - let mut table = TableData::new(Vec::new()); - let mut lerp = LerpState::new(1.0); - - terminal - .draw(|f| recurring_ui(f, data, tab, &mut table, &mut lerp, &theme)) - .unwrap(); - - let buffer = terminal.backend().buffer().clone(); - (0..height) - .map(|y| { - (0..width) - .map(|x| buffer[(x, y)].symbol().to_string()) - .collect::() - }) - .collect::>() - .join("\n") - } - - #[test] - fn suggestion_shows_next_to_the_selected_recurrence_field_only() { - let mut data = TxData::new_empty(); - // Yearly is the only frequency that shows all three boxes at once - data.frequency = "y".to_string(); - data.recur_value = "mon".to_string(); - data.recur_month = "sep".to_string(); - data.autofill = "SUGGESTION".to_string(); - - for tab in [TxTab::Frequency, TxTab::RecurValue, TxTab::RecurMonth] { - let screen = render(&data, &tab); - assert_eq!( - screen.matches("SUGGESTION").count(), - 1, - "exactly one box should show the suggestion for {tab:?}" - ); - } - - let screen = render(&data, &TxTab::Nothing); - assert!(!screen.contains("SUGGESTION")); - } -} diff --git a/tui/src/pages/search_ui.rs b/tui/src/pages/search_ui.rs index f5f578f..b3a7629 100644 --- a/tui/src/pages/search_ui.rs +++ b/tui/src/pages/search_ui.rs @@ -10,7 +10,7 @@ use crate::outputs::TxType; use crate::page_handler::{LogType, TableData, TxTab}; use crate::theme::Theme; use crate::tx_handler::TxData; -use crate::utility::{LerpState, main_block, styled_block}; +use crate::utility::{LerpState, PRIVACY_MASK, main_block, styled_block}; pub const SEARCH_TABLE_ID: &str = "search_table_row"; @@ -22,6 +22,7 @@ pub fn search_ui( date_type: DateType, lerp_state: &mut LerpState, theme: &Theme, + privacy: bool, ) { // Get the data to insert into the Status widget of this page @@ -59,6 +60,11 @@ pub fn search_ui( .map(|(row_index, item)| { let height = 1; let cells = item.iter().enumerate().map(|(index, c)| { + // Index 3 is the Amount column + if privacy && index == 3 { + return Cell::from(PRIVACY_MASK); + } + let Ok(parsed_num) = c.parse::() else { if index == 0 { return Cell::from(c.clone()); diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index fe352e1..9e23c88 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -9,8 +9,8 @@ use thousands::Separable; use crate::page_handler::{IndexedData, SortingType, SummaryTab, TableData}; use crate::theme::Theme; use crate::utility::{ - LerpState, create_tab, main_block, styled_block, styled_block_no_bottom, styled_block_no_top, - tab_highlight_style, + LerpState, create_tab, main_block, mask_numeric_cells, styled_block, styled_block_no_bottom, + styled_block_no_top, tab_highlight_style, }; pub const SUMMARY_TABLE_ID: &str = "summary_table_row"; @@ -28,8 +28,18 @@ pub fn summary_ui( lerp_state: &mut LerpState, full_summary: &FullSummary, theme: &Theme, + privacy: bool, conn: &mut DbConn, ) { + // While privacy mode is on, every number of every table gets masked + let mask_row = |row: Vec| { + if privacy { + mask_numeric_cells(row, true) + } else { + row + } + }; + let size = f.area(); let tags_count = table_data.items.len(); @@ -210,6 +220,8 @@ pub fn summary_ui( let rows = table_data .items .iter() + .cloned() + .map(mask_row) .take(lerp_row) .enumerate() .map(|(row_index, item)| { @@ -284,36 +296,36 @@ pub fn summary_ui( .block(styled_block(&tags_text, theme)) .style(Style::default().fg(theme.border())); - let summary_rows_largest = - full_summary - .largest_array() - .into_iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.into_iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_largest:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); - - Cell::from(format!("{new_c:.2}").separate_with_commas()) - } else { - Cell::from(c.separate_with_commas()) - }; + let summary_rows_largest = full_summary + .largest_array() + .into_iter() + .map(mask_row) + .enumerate() + .map(|(row_index, item)| { + let cells = item.into_iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_table_largest:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); - if index == 0 { - cell = cell.style( - Style::default() - .fg(theme.text()) - .add_modifier(Modifier::BOLD), - ); - } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(theme.text())) + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + Cell::from(c.separate_with_commas()) + }; + + if index == 0 { + cell = cell.style( + Style::default() + .fg(theme.text()) + .add_modifier(Modifier::BOLD), + ); + } + cell }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(theme.text())) + }); let summary_area_largest = Table::new( summary_rows_largest, @@ -327,45 +339,45 @@ pub fn summary_ui( .block(styled_block("", theme)) .style(Style::default().fg(theme.border())); - let summary_rows_peak = - full_summary - .peak_array() - .into_iter() - .enumerate() - .map(|(row_index, item)| { - let height = 1; - let cells = item.into_iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_peak:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); - - let text = if index == 1 && row_index == 2 { - // Month count, so whole numbers are enough here - let new_c = new_c as i64; - format!("{new_c}").separate_with_commas() - } else { - format!("{new_c:.2}").separate_with_commas() - }; - - Cell::from(text) + let summary_rows_peak = full_summary + .peak_array() + .into_iter() + .map(mask_row) + .enumerate() + .map(|(row_index, item)| { + let height = 1; + let cells = item.into_iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_table_peak:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); + + let text = if index == 1 && row_index == 2 { + // Month count, so whole numbers are enough here + let new_c = new_c as i64; + format!("{new_c}").separate_with_commas() } else { - Cell::from(c.separate_with_commas()) + format!("{new_c:.2}").separate_with_commas() }; - if index == 0 { - cell = cell.style( - Style::default() - .fg(theme.text()) - .add_modifier(Modifier::BOLD), - ); - } - cell - }); - Row::new(cells) - .height(height) - .bottom_margin(0) - .style(Style::default().fg(theme.text())) + Cell::from(text) + } else { + Cell::from(c.separate_with_commas()) + }; + + if index == 0 { + cell = cell.style( + Style::default() + .fg(theme.text()) + .add_modifier(Modifier::BOLD), + ); + } + cell }); + Row::new(cells) + .height(height) + .bottom_margin(0) + .style(Style::default().fg(theme.text())) + }); let summary_area_peak = Table::new( summary_rows_peak, @@ -378,58 +390,56 @@ pub fn summary_ui( .block(styled_block("", theme)) .style(Style::default().fg(theme.border())); - let method_rows = - full_summary - .method_array() - .into_iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.into_iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("method_table:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); + let method_rows = full_summary + .method_array() + .into_iter() + .map(mask_row) + .enumerate() + .map(|(row_index, item)| { + let cells = item.into_iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("method_table:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); - Cell::from(format!("{new_c:.2}").separate_with_commas()) + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + let symbol = if c.contains('↑') || c.contains('↓') { + c.chars().next() } 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 lerp_id = format!("method_table:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); - - return Cell::from( - format!("{sym}{new_c:.2}").separate_with_commas(), - ); - } - } + None + }; - if c == "∞" { + if let Some(sym) = symbol { + let c = c.replace(sym, ""); + if let Ok(parsed_num) = c.parse::() { let lerp_id = format!("method_table:{index}:{row_index}"); - lerp_state.lerp(&lerp_id, 0.0, None); + let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); + + return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); } - Cell::from(c.separate_with_commas()) - }; + } - if index == 0 { - cell = cell.style( - Style::default() - .fg(theme.text()) - .add_modifier(Modifier::BOLD), - ); + if c == "∞" { + let lerp_id = format!("method_table:{index}:{row_index}"); + lerp_state.lerp(&lerp_id, 0.0, None); } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(theme.text())) + Cell::from(c.separate_with_commas()) + }; + + if index == 0 { + cell = cell.style( + Style::default() + .fg(theme.text()) + .add_modifier(Modifier::BOLD), + ); + } + cell }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(theme.text())) + }); let method_widths = if mode_selection.index == 2 { vec![ @@ -473,6 +483,13 @@ pub fn summary_ui( let lend_borrow_rows = full_summary .lend_borrows_array() .into_iter() + .map(|row| { + if privacy { + mask_numeric_cells(row, false) + } else { + row + } + }) .enumerate() .map(|(row_index, item)| { let cells = item.into_iter().enumerate().map(|(index, c)| { @@ -529,6 +546,7 @@ pub fn summary_ui( let net_row = full_summary .net_array() .into_iter() + .map(mask_row) .enumerate() .map(|(row_index, item)| { let cells = item.iter().enumerate().map(|(index, c)| { diff --git a/tui/src/tx_handler/tx_data.rs b/tui/src/tx_handler/tx_data.rs index 610f347..9d36984 100644 --- a/tui/src/tx_handler/tx_data.rs +++ b/tui/src/tx_handler/tx_data.rs @@ -1131,44 +1131,3 @@ impl TxData { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn accept_autofill_fills_frequency_day_of_week_and_month() { - let mut data = TxData::new_empty(); - - data.frequency = "we".to_string(); - data.autofill = "Weekly".to_string(); - data.accept_autofill(&TxTab::Frequency); - assert_eq!(data.frequency, "Weekly"); - assert!(data.autofill.is_empty()); - assert_eq!(data.get_current_index(), "Weekly".len()); - - data.recur_value = "mon".to_string(); - data.autofill = "Monday".to_string(); - data.accept_autofill(&TxTab::RecurValue); - assert_eq!(data.recur_value, "Monday"); - - data.recur_month = "sep".to_string(); - data.autofill = "September".to_string(); - data.accept_autofill(&TxTab::RecurMonth); - assert_eq!(data.recur_month, "September"); - } - - #[test] - fn accept_autofill_leaves_other_recurring_fields_alone() { - let mut data = TxData::new_empty(); - data.recur_interval = "2".to_string(); - data.end_date = "2027-01-01".to_string(); - data.autofill = "Weekly".to_string(); - - data.accept_autofill(&TxTab::RecurInterval); - data.accept_autofill(&TxTab::EndDate); - - assert_eq!(data.recur_interval, "2"); - assert_eq!(data.end_date, "2027-01-01"); - } -} diff --git a/tui/src/utility/terminal_launcher.rs b/tui/src/utility/terminal_launcher.rs index 51ab2ed..c9748b2 100644 --- a/tui/src/utility/terminal_launcher.rs +++ b/tui/src/utility/terminal_launcher.rs @@ -263,218 +263,3 @@ fn started_successfully(child: &mut Child, grace: Duration) -> bool { } } } - -#[cfg(test)] -mod tests { - use super::*; - - fn exe() -> PathBuf { - PathBuf::from("/opt/rex/rex") - } - - fn dir() -> PathBuf { - PathBuf::from("/home/user/projects") - } - - fn unix(terminal_env: Option<&str>, desktop: &str) -> Vec { - candidates(Platform::Unix, &exe(), &dir(), terminal_env, desktop) - } - - fn find<'a>(launchers: &'a [Launcher], program: &str) -> &'a Launcher { - launchers.iter().find(|l| l.program == program).unwrap() - } - - fn os(values: &[&str]) -> Vec { - values.iter().map(OsString::from).collect() - } - - #[test] - fn launchers_run_the_real_executable_not_a_relative_path() { - for launcher in unix(None, "") { - assert!( - launcher.args.contains(&OsString::from("/opt/rex/rex")), - "{:?} doesn't run the absolute executable path", - launcher.program - ); - } - } - - #[test] - fn known_terminals_get_their_own_flags() { - let launchers = unix(None, ""); - - assert_eq!( - find(&launchers, "konsole").args, - os(&["--workdir", "/home/user/projects", "-e", "/opt/rex/rex"]) - ); - assert_eq!( - find(&launchers, "gnome-terminal").args, - os(&[ - "--working-directory=/home/user/projects", - "--", - "/opt/rex/rex" - ]) - ); - assert_eq!( - find(&launchers, "xfce4-terminal").args, - os(&[ - "--working-directory=/home/user/projects", - "-x", - "/opt/rex/rex" - ]) - ); - assert_eq!( - find(&launchers, "kitty").args, - os(&["--directory", "/home/user/projects", "/opt/rex/rex"]) - ); - assert_eq!( - find(&launchers, "wezterm").args, - os(&[ - "start", - "--cwd", - "/home/user/projects", - "--", - "/opt/rex/rex" - ]) - ); - assert_eq!(find(&launchers, "xterm").args, os(&["-e", "/opt/rex/rex"])); - } - - #[test] - fn default_order_is_stable_and_ends_with_xterm() { - let first = unix(None, ""); - let second = unix(None, ""); - - assert_eq!(first, second); - assert_eq!(first.first().unwrap().program, "konsole"); - assert_eq!(first.last().unwrap().program, "xterm"); - } - - #[test] - fn desktop_picks_its_own_terminal_first() { - for (desktop, expected) in [ - ("KDE", "konsole"), - ("GNOME", "gnome-terminal"), - ("ubuntu:GNOME", "gnome-terminal"), - ("X-Cinnamon", "gnome-terminal"), - ("XFCE", "xfce4-terminal"), - ("MATE", "mate-terminal"), - ("LXQt", "lxterminal"), - ] { - assert_eq!(unix(None, desktop).first().unwrap().program, expected); - } - } - - #[test] - fn gnome_falls_back_to_kgx_before_unrelated_terminals() { - let programs: Vec<_> = unix(None, "GNOME") - .into_iter() - .map(|l| l.program) - .take(2) - .collect(); - - assert_eq!(programs, vec!["gnome-terminal", "kgx"]); - } - - #[test] - fn terminal_env_with_a_known_name_uses_that_terminals_flags() { - let launchers = unix(Some("/usr/bin/kitty"), "KDE"); - - // kitty doesn't take -e, so it must use its own recipe rather than the generic one - assert_eq!(launchers[0].program, "kitty"); - assert_eq!( - launchers[0].args, - os(&["--directory", "/home/user/projects", "/opt/rex/rex"]) - ); - // Nothing is duplicated - assert_eq!(launchers.iter().filter(|l| l.program == "kitty").count(), 1); - } - - #[test] - fn unknown_terminal_env_is_tried_first_with_dash_e_and_its_own_args() { - let launchers = unix(Some("mycoolterm --fast"), "KDE"); - - assert_eq!(launchers[0].program, "mycoolterm"); - assert_eq!(launchers[0].args, os(&["--fast", "-e", "/opt/rex/rex"])); - assert_eq!(launchers[1].program, "konsole"); - } - - #[test] - fn blank_terminal_env_is_ignored() { - assert_eq!(unix(Some(""), ""), unix(None, "")); - assert_eq!(unix(Some(" "), ""), unix(None, "")); - } - - #[test] - fn macos_and_windows_have_a_single_launcher() { - let mac = candidates(Platform::MacOs, &exe(), &dir(), None, ""); - assert_eq!( - mac, - vec![Launcher::new( - "open", - os(&["-a", "Terminal", "/opt/rex/rex"]) - )] - ); - - let windows = candidates(Platform::Windows, &exe(), &dir(), None, ""); - assert_eq!( - windows, - vec![Launcher::new( - "cmd.exe", - os(&["/C", "start", "", "/opt/rex/rex"]) - )] - ); - } - - #[cfg(unix)] - mod launching { - use super::*; - - fn launcher(program: &str, args: &[&str]) -> Launcher { - Launcher::new(program, os(args)) - } - - #[test] - fn a_program_that_exits_cleanly_counts_as_started() { - assert!(try_launch(&launcher("true", &[]), &std::env::temp_dir())); - } - - #[test] - fn a_program_that_exits_with_an_error_is_skipped() { - assert!(!try_launch(&launcher("false", &[]), &std::env::temp_dir())); - } - - #[test] - fn a_missing_program_is_skipped() { - assert!(!try_launch( - &launcher("rex-definitely-not-a-terminal", &[]), - &std::env::temp_dir() - )); - } - - #[test] - fn a_program_still_running_after_the_grace_period_counts_as_started() { - let mut child = Command::new("sleep").arg("5").spawn().unwrap(); - - assert!(started_successfully(&mut child, Duration::from_millis(100))); - - child.kill().unwrap(); - child.wait().unwrap(); - } - - #[test] - fn the_launched_environment_marker_is_passed_down() { - let marker = std::env::temp_dir().join("rex_launcher_marker_test.txt"); - let _ = std::fs::remove_file(&marker); - - let script = format!("printf %s \"${LAUNCHED_ENV}\" > {}", marker.display()); - assert!(try_launch( - &launcher("sh", &["-c", &script]), - &std::env::temp_dir() - )); - - assert_eq!(std::fs::read_to_string(&marker).unwrap(), "1"); - std::fs::remove_file(marker).unwrap(); - } - } -} diff --git a/tui/src/utility/utils.rs b/tui/src/utility/utils.rs index 0110061..75635f6 100644 --- a/tui/src/utility/utils.rs +++ b/tui/src/utility/utils.rs @@ -19,6 +19,25 @@ use crate::outputs::ComparisonType; use crate::page_handler::{IndexedData, SortingType}; use crate::theme::Theme; +pub const PRIVACY_MASK: &str = "••••"; + +#[must_use] +pub fn mask_numeric_cells(row: Vec, keep_first: bool) -> Vec { + row.into_iter() + .enumerate() + .map(|(index, cell)| { + let without_arrow = cell.trim_start_matches(['↑', '↓']); + let numeric = cell == "∞" || without_arrow.parse::().is_ok(); + + if numeric && !(keep_first && index == 0) { + PRIVACY_MASK.to_string() + } else { + cell + } + }) + .collect() +} + pub const RESTRICTED: [&str; 8] = [ "Total", "Balance",