Compare commits

...

10 commits

Author SHA1 Message Date
traxys
8cc79d2331 Correctly calculate same remaining main account
This used to include the total savings
2024-05-29 20:13:15 +02:00
traxys
4d65995af1 Fix typo in same remaining 2024-05-29 20:01:19 +02:00
traxys
27e40f6b25 Update flake.nix 2024-05-13 23:04:12 +02:00
traxys
3fd54ff8ea Add desktop file 2024-01-27 12:12:04 +01:00
traxys
e67eb6200b Refactor saves 2023-12-09 22:03:51 +01:00
traxys
283cf7a4a5 Avoid using HashMap for coherent iteration order 2023-12-09 19:05:26 +01:00
traxys
afccfcfb56 Add a section comparing earnings 2023-12-09 18:09:55 +01:00
traxys
09f35f84ea Add a section for comparing remaining amounts 2023-12-09 18:05:38 +01:00
traxys
7545d7768f Reduce boilerplate in compare sections 2023-12-09 17:43:21 +01:00
traxys
e9ff7f7dcc Add a total spendings section 2023-12-09 17:28:21 +01:00
4 changed files with 357 additions and 196 deletions

View file

@ -5,38 +5,62 @@
inputs.naersk.url = "github:nix-community/naersk"; inputs.naersk.url = "github:nix-community/naersk";
inputs.rust-overlay.url = "github:oxalica/rust-overlay"; inputs.rust-overlay.url = "github:oxalica/rust-overlay";
outputs = { outputs =
self, {
nixpkgs, self,
flake-utils, nixpkgs,
naersk, flake-utils,
rust-overlay, naersk,
}: rust-overlay,
flake-utils.lib.eachDefaultSystem (system: let }:
pkgs = import nixpkgs { flake-utils.lib.eachDefaultSystem (
inherit system; system:
overlays = [(import rust-overlay)]; let
}; pkgs = import nixpkgs {
rust = pkgs.rust-bin.stable.latest.default; inherit system;
naersk' = pkgs.callPackage naersk { overlays = [ (import rust-overlay) ];
cargo = rust; };
rustc = rust; rust = pkgs.rust-bin.stable.latest.default;
}; naersk' = pkgs.callPackage naersk {
libPath = with pkgs; lib.makeLibraryPath [libxkbcommon wayland vulkan-loader]; cargo = rust;
in { rustc = rust;
devShell = pkgs.mkShell { };
nativeBuildInputs = [rust pkgs.cargo-watch]; libPath =
RUST_PATH = "${rust}"; with pkgs;
RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html"; lib.makeLibraryPath [
LD_LIBRARY_PATH = libPath; libxkbcommon
}; wayland
vulkan-loader
];
in
{
devShell = pkgs.mkShell {
nativeBuildInputs = [ rust ] ++ (with pkgs; [ cargo-watch ]);
RUST_PATH = "${rust}";
RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html";
LD_LIBRARY_PATH = libPath;
};
defaultPackage = naersk'.buildPackage { packages = rec {
src = ./.; glaurung = default;
nativeBuildInputs = [pkgs.makeWrapper]; default = naersk'.buildPackage {
postInstall = '' src = ./.;
wrapProgram "$out/bin/glaurung" --prefix LD_LIBRARY_PATH : "${libPath}" nativeBuildInputs = with pkgs; [
''; makeWrapper
}; copyDesktopItems
}); ];
postInstall = ''
wrapProgram "$out/bin/glaurung" --prefix LD_LIBRARY_PATH : "${libPath}"
'';
desktopItems = [
(pkgs.makeDesktopItem {
name = "glaurung";
exec = "glaurung";
desktopName = "Glaurung";
})
];
};
};
}
);
} }

View file

@ -1,4 +1,4 @@
use std::{cmp::Ordering, collections::BTreeMap, iter}; use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, iter};
use enum_map::{Enum, EnumMap}; use enum_map::{Enum, EnumMap};
use iced::{ use iced::{
@ -13,12 +13,15 @@ use itertools::Itertools;
use crate::{ use crate::{
CompareLoad, CompareSide, Report, ReportDate, Spendings, TEXT_EMPH2, TEXT_H1, TEXT_H2, CompareLoad, CompareSide, Report, ReportDate, Spendings, TEXT_EMPH2, TEXT_H1, TEXT_H2,
TEXT_NORMAL,
}; };
pub(crate) struct Compare<'a, F> { pub(crate) struct Compare<'a, F> {
pub left: Option<(CompareLoad, Box<dyn Spendings>)>, pub left: Option<(CompareLoad, Box<dyn Spendings>)>,
pub right: Option<(CompareLoad, Box<dyn Spendings>)>, pub right: Option<(CompareLoad, Box<dyn Spendings>)>,
pub archive: &'a BTreeMap<ReportDate, Report>, pub archive: &'a BTreeMap<ReportDate, Report>,
pub person_1: &'a str,
pub person_2: &'a str,
pub on_load: F, pub on_load: F,
} }
@ -38,7 +41,9 @@ pub(crate) enum CompareMsg {
pub(crate) enum Section { pub(crate) enum Section {
Recurring, Recurring,
Variable, Variable,
Total, Accounts,
Remaining,
Earnings,
} }
impl Section { impl Section {
@ -46,7 +51,9 @@ impl Section {
match self { match self {
Section::Recurring => "Recurring", Section::Recurring => "Recurring",
Section::Variable => "Variable", Section::Variable => "Variable",
Section::Total => "Total", Section::Accounts => "Accounts",
Section::Remaining => "Remaining",
Section::Earnings => "Earnings",
} }
} }
} }
@ -116,64 +123,155 @@ where
]; ];
let headings = [ let headings = [
Some( column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)]
column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)] .align_items(Alignment::Center)
.align_items(Alignment::Center) .into(),
.into(), column![text("Left").size(TEXT_H1), heading_left]
), .align_items(Alignment::Center)
Some( .into(),
column![text("Left").size(TEXT_H1), heading_left] column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.into(), .into(),
), column![text("Right").size(TEXT_H1), heading_right]
Some( .align_items(Alignment::Center)
column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)] .into(),
.align_items(Alignment::Center)
.into(),
),
Some(
column![text("Right").size(TEXT_H1), heading_right]
.align_items(Alignment::Center)
.into(),
),
]; ];
let mk_section = |section: Section| { fn text_row<'a, T, M>(size: u16, r: [Option<T>; 4]) -> [iced::Element<'a, M>; 4]
where
T: Into<iced::Element<'a, M>>,
{
let [a, b, c, d] = r;
let map = |e: Option<T>| {
e.map(Into::into)
.unwrap_or_else(|| text("").size(size).into())
};
[map(a), map(b), map(c), map(d)]
}
let mk_section_header = |section: Section| {
let status = state.collapsed[section]; let status = state.collapsed[section];
[ text_row(
Some( TEXT_EMPH2,
row![ [
text(section.name()).size(TEXT_EMPH2), Some(
horizontal_space(Length::Fill), row![
toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b)), text(section.name()).size(TEXT_EMPH2),
] horizontal_space(Length::Fill),
.align_items(Alignment::Center) toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b))
.into(), .width(Length::Shrink),
), ]
Some(text("").size(TEXT_EMPH2).into()), .align_items(Alignment::Center),
Some(text("").size(TEXT_EMPH2).into()), ),
Some(text("").size(TEXT_EMPH2).into()), None,
] None,
None,
],
)
}; };
let left = self.left.as_ref().map(|(_, r)| &**r); let left = self.left.as_ref().map(|(_, r)| &**r);
let right = self.right.as_ref().map(|(_, r)| &**r); let right = self.right.as_ref().map(|(_, r)| &**r);
fn item_compare<'a, I, M>( fn compare_row<'b, S, M>(
size: u16,
always: Option<&str>,
left: Option<(&S, f64)>,
right: Option<(&S, f64)>,
) -> Vec<[iced::Element<'b, M>; 4]>
where
S: AsRef<str> + ?Sized,
{
let float_text = |f: f64| Some(text(format!("{f:.2}")).size(size));
match (left, right) {
(None, None) => match always {
None => vec![],
Some(l) => vec![text_row(size, [Some(text(l).size(size)), None, None, None])],
},
(None, Some((k, v))) => {
vec![text_row(
size,
[Some(text(k.as_ref()).size(size)), None, None, float_text(v)],
)]
}
(Some((k, v)), None) => {
vec![text_row(
size,
[Some(text(k.as_ref()).size(size)), float_text(v), None, None],
)]
}
(Some((lk, lv)), Some((rk, rv))) if lk.as_ref() != rk.as_ref() => {
vec![
text_row(
size,
[
Some(text(lk.as_ref()).size(size)),
float_text(lv),
None,
None,
],
),
text_row(
size,
[
Some(text(rk.as_ref()).size(size)),
None,
None,
float_text(rv),
],
),
]
}
(Some((k, lv)), Some((_, rv))) => {
if rv == 0. || lv == 0. {
vec![text_row(
size,
[
Some(text(k.as_ref()).size(size)),
float_text(lv),
None,
float_text(rv),
],
)]
} else {
let difference = ((rv / lv - 1.) * 100.) as i32;
let sign = match difference.cmp(&0) {
Ordering::Less => "- ",
Ordering::Equal => "",
Ordering::Greater => "+ ",
};
vec![text_row(
size,
[
Some(text(k.as_ref()).size(size)),
float_text(lv),
Some(text(format!("{sign}{}%", difference.abs())).size(size)),
float_text(rv),
],
)]
}
}
}
}
fn item_compare<'a, 's, I, S, M>(
collapse: bool, collapse: bool,
left: Option<I>, left: Option<I>,
right: Option<I>, right: Option<I>,
) -> Vec<[Option<iced::Element<'a, M>>; 4]> ) -> Vec<[iced::Element<'a, M>; 4]>
where where
I: IntoIterator<Item = (&'a str, f64)>, I: IntoIterator<Item = (S, f64)>,
S: Into<Cow<'s, str>>,
{ {
if collapse { if collapse {
return Vec::new(); return Vec::new();
} }
let to_btree = |i: I| i.into_iter().collect::<BTreeMap<_, _>>(); let entry_ref = |(s, v): (S, f64)| (s.into(), v);
let to_btree = |i: I| i.into_iter().map(entry_ref).collect::<BTreeMap<_, _>>();
let float_text = |f: f64| Some(text(format!("{f:.2}")).into()); let float_text = |f: f64| Some(text(format!("{f:.2}")).size(TEXT_NORMAL));
let (left, right) = match (left, right) { let (left, right) = match (left, right) {
(None, None) => return Vec::new(), (None, None) => return Vec::new(),
@ -181,15 +279,27 @@ where
(Some(u), None) => { (Some(u), None) => {
return u return u
.into_iter() .into_iter()
.sorted_by_key(|(k, _)| *k) .map(entry_ref)
.map(|(k, v)| [Some(text(k).into()), float_text(v), None, None]) .sorted_by(|(ka, _), (kb, _)| ka.cmp(kb))
.map(|(k, v)| {
text_row(
TEXT_NORMAL,
[Some(text(k).size(TEXT_NORMAL)), float_text(v), None, None],
)
})
.collect(); .collect();
} }
(None, Some(u)) => { (None, Some(u)) => {
return u return u
.into_iter() .into_iter()
.sorted_by_key(|(k, _)| *k) .map(entry_ref)
.map(|(k, v)| [Some(text(k).into()), None, None, float_text(v)]) .sorted_by(|(ka, _), (kb, _)| ka.cmp(kb))
.map(|(k, v)| {
text_row(
TEXT_NORMAL,
[Some(text(k).size(TEXT_NORMAL)), None, None, float_text(v)],
)
})
.collect(); .collect();
} }
}; };
@ -202,16 +312,16 @@ where
Right, Right,
} }
type Peek<'a> = Option<&'a (&'a &'a str, &'a f64)>; type Peek<'a> = Option<&'a (&'a Cow<'a, str>, &'a f64)>;
let needs_to_wait = |l: Peek, r: Peek| match (l, r) { let needs_to_wait = |l: Peek, r: Peek| match (l, r) {
(_, None) | (None, _) => None, (_, None) | (None, _) => None,
(Some((&l, _)), Some((&r, _))) => match l.cmp(r) { (Some((l, _)), Some((r, _))) => match l.cmp(r) {
Ordering::Less => match left.contains_key(r) { Ordering::Less => match left.contains_key(*r) {
false => None, false => None,
true => Some(Side::Left), true => Some(Side::Left),
}, },
Ordering::Equal => None, Ordering::Equal => None,
Ordering::Greater => match right.contains_key(l) { Ordering::Greater => match right.contains_key(*l) {
false => None, false => None,
true => Some(Side::Right), true => Some(Side::Right),
}, },
@ -220,50 +330,22 @@ where
let mut compare = Vec::new(); let mut compare = Vec::new();
type Item<'a> = Option<(&'a &'a str, &'a f64)>; type Item<'a> = Option<(&'a Cow<'a, str>, &'a f64)>;
let mut insert_row = |l: Item, r: Item| match (l, r) { let mut insert_row = |l: Item, r: Item| {
(None, None) => false, let mut inserted = false;
(None, Some((k, v))) => { compare.extend(
compare.push([Some(text(k).into()), None, None, float_text(*v)]); compare_row(
true TEXT_NORMAL,
} None,
(Some((k, v)), None) => { l.map(|(a, b)| (a, *b)),
compare.push([Some(text(k).into()), float_text(*v), None, None]); r.map(|(a, b)| (a, *b)),
true )
} .into_iter()
(Some((lk, lv)), Some((rk, rv))) if lk != rk => { .inspect(|_| {
compare.push([Some(text(lk).into()), float_text(*lv), None, None]); inserted = true;
}),
compare.push([Some(text(rk).into()), None, None, float_text(*rv)]); );
inserted
true
}
(Some((k, lv)), Some((_, rv))) => {
if *rv == 0. || *lv == 0. {
compare.push([
Some(text(k).into()),
float_text(*lv),
None,
float_text(*rv),
]);
} else {
let difference = ((rv / lv - 1.) * 100.) as i32;
let sign = match difference.cmp(&0) {
Ordering::Less => "- ",
Ordering::Equal => "",
Ordering::Greater => "+ ",
};
compare.push([
Some(text(k).into()),
float_text(*lv),
Some(text(format!("{sign}{}%", difference.abs())).into()),
float_text(*rv),
]);
}
true
}
}; };
loop { loop {
@ -288,38 +370,60 @@ where
compare compare
} }
fn mk_total<'a>( let mk_total = |side: Option<&dyn Spendings>| {
archive: &BTreeMap<ReportDate, Report>,
side: Option<&'a dyn Spendings>,
) -> Option<impl Iterator<Item = (&'a str, f64)>> {
let side = side?; let side = side?;
let (main_account, _) = side.main_account(archive);
Some(std::iter::once(("Main", main_account)).chain(side.savings())) Some(("Total", side.total_spendings()))
};
macro_rules! mk_section {
{$section:expr, $map:expr} => {
itertools::chain![
iter::once(mk_section_header($section)),
item_compare(
state.collapsed[$section],
left.map($map),
right.map($map)
)
]
};
} }
scrollable(table::<_, _, iced::Element<_>, _>( scrollable(table::<_, _, iced::Element<_>, _>(
properties, properties,
itertools::chain![ itertools::chain![
iter::once(headings), iter::once(headings),
iter::once(mk_section(Section::Recurring)), mk_section!(Section::Recurring, |r| r.recurring()),
item_compare( mk_section!(Section::Variable, |r| r.variable()),
state.collapsed[Section::Recurring], compare_row(TEXT_EMPH2, Some("Total"), mk_total(left), mk_total(right)),
left.map(|r| r.recurring()), mk_section!(Section::Accounts, |s| {
right.map(|r| r.recurring()) let (main_account, _) = s.main_account(self.archive);
),
iter::once(mk_section(Section::Variable)), std::iter::once(("Main", main_account)).chain(s.savings())
item_compare( }),
state.collapsed[Section::Variable], mk_section!(Section::Earnings, |s| {
left.map(|r| r.variable()), let earnings = s.earnings();
right.map(|r| r.variable())
), [
iter::once(mk_section(Section::Total)), (self.person_1, earnings.person_1),
item_compare( (self.person_2, earnings.person_2),
state.collapsed[Section::Variable], ]
mk_total(self.archive, left), }),
mk_total(self.archive, right), mk_section!(Section::Remaining, move |s| {
), let contrib = s.contributions(self.archive);
let remaining_1 = format!("Equal {}", self.person_1);
let remaining_2 = format!("Equal {}", self.person_2);
[
(
Cow::Borrowed("Same"),
contrib.same_remaining.remaining.person_1,
),
(remaining_1.into(), contrib.proportional.remaining.person_1),
(remaining_2.into(), contrib.proportional.remaining.person_2),
]
}),
], ],
)) ))
.into() .into()
@ -336,7 +440,7 @@ pub struct ColumnProperties {
pub fn table<'a, I, J, T, M>(props: Vec<ColumnProperties>, items: I) -> Row<'a, M> pub fn table<'a, I, J, T, M>(props: Vec<ColumnProperties>, items: I) -> Row<'a, M>
where where
I: IntoIterator<Item = J>, I: IntoIterator<Item = J>,
J: IntoIterator<Item = Option<T>>, J: IntoIterator<Item = T>,
T: Into<iced::Element<'a, M>>, T: Into<iced::Element<'a, M>>,
M: 'a, M: 'a,
{ {
@ -350,7 +454,7 @@ where
col.push(horizontal_rule(5.).into()); col.push(horizontal_rule(5.).into());
} }
let inner = item.map(Into::into).unwrap_or_else(|| text("").into()); let inner = item.into();
col.push(inner); col.push(inner);
}); });
columns columns

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, fs::OpenOptions, num::NonZeroU8, path::PathBuf}; use std::{collections::BTreeMap, num::NonZeroU8};
use iced::{ use iced::{
theme, theme,
@ -10,10 +10,11 @@ use iced::{
}; };
use iced_aw::{card, modal, number_input, selection_list}; use iced_aw::{card, modal, number_input, selection_list};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
calc::calc_parser, BoxIter, Config, ContributionPoint, Message, Report, ReportDate, SaveFile, calc::calc_parser, BoxIter, Config, ContributionPoint, Message, Report, ReportDate, Spendings,
Spendings, LIST_RULE, SECTION_RULE, TEXT_EMPH1, TEXT_EMPH2, TEXT_H1, TEXT_H2, LIST_RULE, SECTION_RULE, TEXT_EMPH1, TEXT_EMPH2, TEXT_H1, TEXT_H2,
}; };
struct AddFixed<F> { struct AddFixed<F> {
@ -778,7 +779,6 @@ pub struct EditState {
recurring: BTreeMap<String, f64>, recurring: BTreeMap<String, f64>,
variable: BTreeMap<String, (String, Option<f64>)>, variable: BTreeMap<String, (String, Option<f64>)>,
savings: BTreeMap<String, f64>, savings: BTreeMap<String, f64>,
save_file: PathBuf,
earnings_1: f64, earnings_1: f64,
earnings_2: f64, earnings_2: f64,
average: NonZeroU8, average: NonZeroU8,
@ -850,15 +850,22 @@ macro_rules! msg2 {
}; };
} }
#[derive(Serialize, Deserialize, Default)]
pub(crate) struct EditSaveFile {
#[serde(flatten)]
pub current: Report,
#[serde(default)]
pub archive: BTreeMap<ReportDate, Report>,
}
impl EditState { impl EditState {
pub fn new(save_file: PathBuf) -> Self { pub fn new() -> Self {
Self { Self {
recurring: Default::default(), recurring: Default::default(),
savings: Default::default(), savings: Default::default(),
variable: Default::default(), variable: Default::default(),
earnings_1: Default::default(), earnings_1: Default::default(),
earnings_2: Default::default(), earnings_2: Default::default(),
save_file,
average: NonZeroU8::MIN, average: NonZeroU8::MIN,
date: None, date: None,
} }
@ -910,8 +917,8 @@ impl EditState {
} }
} }
pub(super) fn save(&self, archive: BTreeMap<ReportDate, Report>, pretty: bool) { pub(super) fn save(&self, archive: BTreeMap<ReportDate, Report>) -> EditSaveFile {
let mut save = SaveFile { let mut save = EditSaveFile {
current: Default::default(), current: Default::default(),
archive, archive,
}; };
@ -923,20 +930,7 @@ impl EditState {
None => save.current = self.report(), None => save.current = self.report(),
} }
let tmp_path = format!(".glaurung-{}", std::process::id()); save
let save_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp_path)
.expect("Can't open temp save file");
let to_writer = match pretty {
true => serde_json::to_writer_pretty,
false => serde_json::to_writer,
};
to_writer(save_file, &save).expect("could not write save file");
std::fs::rename(tmp_path, &self.save_file).expect("could not save data");
} }
pub(super) fn update(&mut self, message: EditMessage) { pub(super) fn update(&mut self, message: EditMessage) {
@ -1020,7 +1014,7 @@ impl EditState {
)), )),
]; ];
for (account, contribution) in &contributions.proportional.savings { for (account, contribution) in &contributions.same_remaining.savings {
same_remaining = same_remaining same_remaining = same_remaining
.push(text(account).size(TEXT_EMPH2)) .push(text(account).size(TEXT_EMPH2))
.push(text(format!( .push(text(format!(

View file

@ -1,9 +1,9 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::BTreeMap,
fs::File, fs::{File, OpenOptions},
io::BufReader, io::BufReader,
num::NonZeroU8, num::NonZeroU8,
ops::{Mul, MulAssign, Sub, SubAssign}, ops::{AddAssign, Mul, MulAssign, Sub, SubAssign},
path::PathBuf, path::PathBuf,
}; };
@ -11,7 +11,7 @@ use anyhow::anyhow;
use calc::calc_parser; use calc::calc_parser;
use compare::Compare; use compare::Compare;
use directories::ProjectDirs; use directories::ProjectDirs;
use edit::{EditMessage, EditState}; use edit::{EditMessage, EditSaveFile, EditState};
use either::Either; use either::Either;
use iced::{ use iced::{
font, subscription, font, subscription,
@ -29,6 +29,7 @@ mod edit;
const TEXT_H1: u16 = 30; const TEXT_H1: u16 = 30;
const TEXT_H2: u16 = 25; const TEXT_H2: u16 = 25;
const TEXT_NORMAL: u16 = 15;
const TEXT_EMPH1: u16 = 17; const TEXT_EMPH1: u16 = 17;
const TEXT_EMPH2: u16 = 20; const TEXT_EMPH2: u16 = 20;
@ -53,6 +54,13 @@ impl ContributionPoint {
} }
} }
impl AddAssign<ContributionPoint> for ContributionPoint {
fn add_assign(&mut self, rhs: ContributionPoint) {
self.person_1 += rhs.person_1;
self.person_2 += rhs.person_2;
}
}
impl SubAssign<ContributionPoint> for ContributionPoint { impl SubAssign<ContributionPoint> for ContributionPoint {
fn sub_assign(&mut self, rhs: ContributionPoint) { fn sub_assign(&mut self, rhs: ContributionPoint) {
self.person_1 -= rhs.person_1; self.person_1 -= rhs.person_1;
@ -155,6 +163,10 @@ pub(crate) trait Spendings {
remaining -= main; remaining -= main;
let mut total_depositing = main_account; let mut total_depositing = main_account;
let mut total_savings = ContributionPoint {
person_1: 0.,
person_2: 0.,
};
let (savings_same_percent, saving_same_remaining) = self let (savings_same_percent, saving_same_remaining) = self
.savings() .savings()
@ -165,6 +177,12 @@ pub(crate) trait Spendings {
total_depositing += amount; total_depositing += amount;
remaining -= contributions; remaining -= contributions;
let savings = ContributionPoint {
person_1: amount / 2.,
person_2: amount / 2.,
};
total_savings += savings;
( (
(account.to_string(), contributions), (account.to_string(), contributions),
( (
@ -191,7 +209,7 @@ pub(crate) trait Spendings {
savings: savings_same_percent, savings: savings_same_percent,
}, },
same_remaining: Contribution { same_remaining: Contribution {
main: self.earnings() - remaining_per_person, main: self.earnings() - remaining_per_person - total_savings,
remaining: remaining_per_person, remaining: remaining_per_person,
savings: saving_same_remaining, savings: saving_same_remaining,
}, },
@ -202,13 +220,13 @@ pub(crate) trait Spendings {
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
struct Report { pub(crate) struct Report {
#[serde(default)] #[serde(default)]
recurring: BTreeMap<String, f64>, recurring: BTreeMap<String, f64>,
#[serde(default)] #[serde(default)]
savings: BTreeMap<String, f64>, savings: BTreeMap<String, f64>,
#[serde(default)] #[serde(default)]
variable: HashMap<String, String>, variable: BTreeMap<String, String>,
#[serde(default)] #[serde(default)]
earnings_1: f64, earnings_1: f64,
#[serde(default)] #[serde(default)]
@ -282,7 +300,7 @@ impl Spendings for (ReportDate, Report) {
} }
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)]
struct ReportDate { pub(crate) struct ReportDate {
pub year: u64, pub year: u64,
pub month: u8, pub month: u8,
} }
@ -361,9 +379,7 @@ impl<'de> Deserialize<'de> for ReportDate {
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
struct SaveFile { struct SaveFile {
#[serde(flatten)] #[serde(flatten)]
current: Report, edit: EditSaveFile,
#[serde(default)]
archive: BTreeMap<ReportDate, Report>,
} }
#[derive(Default)] #[derive(Default)]
@ -416,6 +432,8 @@ struct Glaurung {
edit: EditState, edit: EditState,
view: CurrentView, view: CurrentView,
save_file: PathBuf,
compare_left: Option<CompareLoad>, compare_left: Option<CompareLoad>,
compare_right: Option<CompareLoad>, compare_right: Option<CompareLoad>,
@ -435,6 +453,26 @@ impl Glaurung {
.map(|rep| (e, Box::new((r, rep.clone())) as Box<dyn Spendings>)), .map(|rep| (e, Box::new((r, rep.clone())) as Box<dyn Spendings>)),
} }
} }
fn save(&self) {
let edit = self.edit.save(self.archive.clone());
let save = SaveFile { edit };
let tmp_path = format!(".glaurung-{}", std::process::id());
let save_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp_path)
.expect("Can't open temp save file");
let to_writer = match self.config.pretty_save {
true => serde_json::to_writer_pretty,
false => serde_json::to_writer,
};
to_writer(save_file, &save).expect("could not write save file");
std::fs::rename(tmp_path, &self.save_file).expect("could not save data");
}
} }
impl Application for Glaurung { impl Application for Glaurung {
@ -446,14 +484,15 @@ impl Application for Glaurung {
fn new(config: Self::Flags) -> (Self, Command<Message>) { fn new(config: Self::Flags) -> (Self, Command<Message>) {
let mut this = Self { let mut this = Self {
config: config.config, config: config.config,
edit: EditState::new(config.save_file), edit: EditState::new(),
archive: config.save.archive, save_file: config.save_file,
archive: config.save.edit.archive,
view: CurrentView::Edit, view: CurrentView::Edit,
compare_left: None, compare_left: None,
compare_right: None, compare_right: None,
}; };
this.edit.load(None, config.save.current); this.edit.load(None, config.save.edit.current);
( (
this, this,
@ -480,14 +519,12 @@ impl Application for Glaurung {
Message::FontLoaded(r) => r.expect("could not load font"), Message::FontLoaded(r) => r.expect("could not load font"),
Message::Event(ev) => { Message::Event(ev) => {
if let Event::Window(window::Event::CloseRequested) = ev { if let Event::Window(window::Event::CloseRequested) = ev {
self.edit self.save();
.save(self.archive.clone(), self.config.pretty_save);
return window::close(); return window::close();
} }
} }
Message::Load(d) => { Message::Load(d) => {
self.edit self.save();
.save(self.archive.clone(), self.config.pretty_save);
self.edit self.edit
.load(Some(d), self.archive.get(&d).cloned().unwrap_or_default()); .load(Some(d), self.archive.get(&d).cloned().unwrap_or_default());
} }
@ -520,6 +557,8 @@ impl Application for Glaurung {
right: self.compare_load(self.compare_right), right: self.compare_load(self.compare_right),
archive: &self.archive, archive: &self.archive,
on_load: Message::CompareLoad, on_load: Message::CompareLoad,
person_1: &self.config.person_1,
person_2: &self.config.person_2,
}), }),
}; };