Compare commits
10 commits
06efebc17b
...
8cc79d2331
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cc79d2331 | ||
|
|
4d65995af1 | ||
|
|
27e40f6b25 | ||
|
|
3fd54ff8ea | ||
|
|
e67eb6200b | ||
|
|
283cf7a4a5 | ||
|
|
afccfcfb56 | ||
|
|
09f35f84ea | ||
|
|
7545d7768f | ||
|
|
e9ff7f7dcc |
4 changed files with 357 additions and 196 deletions
90
flake.nix
90
flake.nix
|
|
@ -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";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
348
src/compare.rs
348
src/compare.rs
|
|
@ -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
|
||||||
|
|
|
||||||
40
src/edit.rs
40
src/edit.rs
|
|
@ -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!(
|
||||||
|
|
|
||||||
75
src/main.rs
75
src/main.rs
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue