772 lines
22 KiB
Rust
772 lines
22 KiB
Rust
use std::{
|
|
collections::{BTreeMap, HashMap},
|
|
fs::{File, OpenOptions},
|
|
io::BufReader,
|
|
path::PathBuf,
|
|
};
|
|
|
|
use anyhow::anyhow;
|
|
use directories::ProjectDirs;
|
|
use iced::{
|
|
font, subscription, theme,
|
|
widget::{
|
|
button, column, component, horizontal_rule, horizontal_space, row, rule, text, text_input,
|
|
},
|
|
window, Application, Command, Event, Length, Renderer, Settings, Theme,
|
|
};
|
|
use iced_aw::{card, modal};
|
|
use itertools::Itertools;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
type Element<'a> = iced::Element<'a, Message>;
|
|
|
|
mod calc;
|
|
|
|
const TEXT_H1: u16 = 30;
|
|
const TEXT_H2: u16 = 25;
|
|
const TEXT_EMPH1: u16 = 17;
|
|
const TEXT_EMPH2: u16 = 20;
|
|
|
|
const LIST_RULE: u16 = 5;
|
|
const SECTION_RULE: u16 = 15;
|
|
|
|
struct AddRecurring<F> {
|
|
on_submit: F,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct AddRecurringState {
|
|
value: String,
|
|
item: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum AddRecurringEvent {
|
|
SetItem(String),
|
|
SetValue(String),
|
|
SubmitValue,
|
|
}
|
|
|
|
impl<F> AddRecurring<F> {
|
|
fn new(on_submit: F) -> Self {
|
|
Self { on_submit }
|
|
}
|
|
}
|
|
|
|
impl<M, F> iced::widget::Component<M, Renderer> for AddRecurring<F>
|
|
where
|
|
F: FnMut(String, f64) -> M,
|
|
{
|
|
type State = AddRecurringState;
|
|
type Event = AddRecurringEvent;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
AddRecurringEvent::SetItem(i) => state.item = i,
|
|
AddRecurringEvent::SetValue(v) => state.value = v,
|
|
AddRecurringEvent::SubmitValue => {
|
|
if let Ok(v) = state.value.parse() {
|
|
state.value.clear();
|
|
return Some((self.on_submit)(std::mem::take(&mut state.item), v));
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn view(&self, state: &Self::State) -> iced::Element<'_, Self::Event, Renderer> {
|
|
column![
|
|
text_input("item", &state.item).on_input(AddRecurringEvent::SetItem),
|
|
text_input("value", &state.value)
|
|
.on_input(AddRecurringEvent::SetValue)
|
|
.on_submit(AddRecurringEvent::SubmitValue)
|
|
]
|
|
.into()
|
|
}
|
|
}
|
|
|
|
struct EditRecurring<'a, F> {
|
|
value: String,
|
|
name: &'a str,
|
|
on_submit: F,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct EditRecurringState {
|
|
edit: Option<String>,
|
|
modal: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum EditRecurringEvent {
|
|
Edit(String),
|
|
Open,
|
|
Close,
|
|
Submit,
|
|
}
|
|
|
|
impl<'a, F> EditRecurring<'a, F> {
|
|
fn new(value: f64, name: &'a str, on_submit: F) -> Self {
|
|
Self {
|
|
value: value.to_string(),
|
|
name,
|
|
on_submit,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M, F> iced::widget::Component<M, Renderer> for EditRecurring<'_, F>
|
|
where
|
|
F: FnMut(f64) -> M,
|
|
{
|
|
type State = EditRecurringState;
|
|
type Event = EditRecurringEvent;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
EditRecurringEvent::Edit(v) => state.edit = Some(v),
|
|
EditRecurringEvent::Submit => {
|
|
if let Some(e) = &state.edit {
|
|
if let Ok(v) = e.parse() {
|
|
state.edit = None;
|
|
state.modal = false;
|
|
return Some((self.on_submit)(v));
|
|
}
|
|
}
|
|
}
|
|
EditRecurringEvent::Open => {
|
|
state.modal = true;
|
|
}
|
|
EditRecurringEvent::Close => {
|
|
state.modal = false;
|
|
state.edit = None;
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
|
|
let underlay = button(text("Edit")).on_press(EditRecurringEvent::Open);
|
|
let overlay = match state.modal {
|
|
true => Some(
|
|
card(
|
|
text(&format!("Edit {}", self.name)),
|
|
text_input("new value", state.edit.as_ref().unwrap_or(&self.value))
|
|
.on_input(EditRecurringEvent::Edit)
|
|
.on_submit(EditRecurringEvent::Submit),
|
|
)
|
|
.max_width(300.0)
|
|
.on_close(EditRecurringEvent::Close),
|
|
),
|
|
false => None,
|
|
};
|
|
|
|
modal(underlay, overlay)
|
|
.backdrop(EditRecurringEvent::Close)
|
|
.on_esc(EditRecurringEvent::Close)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum RecurringMessage {
|
|
CloseAdd,
|
|
Add,
|
|
DoAdd(String, f64),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecurringState {
|
|
add_recurring: bool,
|
|
}
|
|
|
|
struct Recurring<'a, F> {
|
|
items: &'a BTreeMap<String, f64>,
|
|
on_add: F,
|
|
}
|
|
|
|
impl<M, F> iced::widget::Component<M, Renderer> for Recurring<'_, F>
|
|
where
|
|
F: FnMut(String, f64) -> M,
|
|
{
|
|
type State = RecurringState;
|
|
type Event = RecurringMessage;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
RecurringMessage::Add => state.add_recurring = true,
|
|
RecurringMessage::CloseAdd => {
|
|
state.add_recurring = false;
|
|
}
|
|
RecurringMessage::DoAdd(item, value) => {
|
|
state.add_recurring = false;
|
|
return Some((self.on_add)(item, value));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[allow(unstable_name_collisions)]
|
|
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
|
|
let underlay = column![
|
|
text("Recurring").size(TEXT_H2),
|
|
button(text("Add")).on_press(RecurringMessage::Add),
|
|
horizontal_rule(LIST_RULE),
|
|
column(
|
|
self.items
|
|
.iter()
|
|
.map(|(name, &value)| row![
|
|
text(name).size(TEXT_EMPH1),
|
|
text(&format!("{value} €")),
|
|
horizontal_space(Length::Fill),
|
|
component(EditRecurring::new(
|
|
value,
|
|
name,
|
|
|v| RecurringMessage::DoAdd(name.to_string(), v)
|
|
))
|
|
]
|
|
.spacing(5)
|
|
.align_items(iced::Alignment::Center)
|
|
.into())
|
|
.intersperse_with(|| horizontal_rule(LIST_RULE).into())
|
|
.collect()
|
|
),
|
|
horizontal_rule(LIST_RULE),
|
|
text(&format!("Total: {} €", self.items.values().sum::<f64>())).size(TEXT_EMPH2)
|
|
];
|
|
|
|
let overlay = match state.add_recurring {
|
|
true => Some(
|
|
card(
|
|
text("Add a recurring spending"),
|
|
component(AddRecurring::new(RecurringMessage::DoAdd)),
|
|
)
|
|
.on_close(RecurringMessage::CloseAdd)
|
|
.max_width(300.0),
|
|
),
|
|
false => None,
|
|
};
|
|
|
|
modal(underlay, overlay)
|
|
.backdrop(RecurringMessage::CloseAdd)
|
|
.on_esc(RecurringMessage::CloseAdd)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
struct ExprInput<'a, F> {
|
|
expr: &'a str,
|
|
on_submit: F,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ExprInputState {
|
|
state: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum ExprInputMessage {
|
|
Edit(String),
|
|
Submit,
|
|
}
|
|
|
|
impl<M, F> iced::widget::Component<M, Renderer> for ExprInput<'_, F>
|
|
where
|
|
F: FnMut(String) -> M,
|
|
{
|
|
type State = ExprInputState;
|
|
type Event = ExprInputMessage;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
ExprInputMessage::Edit(v) => {
|
|
state.state = Some(v);
|
|
}
|
|
ExprInputMessage::Submit => {
|
|
return Some((self.on_submit)(
|
|
state.state.clone().unwrap_or_else(|| self.expr.to_string()),
|
|
))
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
|
|
text_input("expr", state.state.as_deref().unwrap_or(self.expr))
|
|
.on_input(ExprInputMessage::Edit)
|
|
.on_submit(ExprInputMessage::Submit)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum VariableMessage {
|
|
CloseAdd,
|
|
Add,
|
|
EditAdd(String),
|
|
ExprEdit(String, String),
|
|
DoAdd,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct VariableState {
|
|
modal: bool,
|
|
add_value: String,
|
|
}
|
|
|
|
struct VariableSpendings<'a, F, G> {
|
|
items: &'a BTreeMap<String, (String, Option<f64>)>,
|
|
on_add: F,
|
|
on_expr_edit: G,
|
|
}
|
|
|
|
impl<M, F, G> iced::widget::Component<M, Renderer> for VariableSpendings<'_, F, G>
|
|
where
|
|
F: FnMut(String) -> M,
|
|
G: FnMut(String, String) -> M,
|
|
{
|
|
type State = VariableState;
|
|
type Event = VariableMessage;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
VariableMessage::CloseAdd => {
|
|
state.add_value.clear();
|
|
state.modal = false;
|
|
}
|
|
VariableMessage::Add => {
|
|
state.modal = true;
|
|
}
|
|
VariableMessage::EditAdd(v) => {
|
|
state.add_value = v;
|
|
}
|
|
VariableMessage::DoAdd => {
|
|
state.modal = false;
|
|
return Some((self.on_add)(std::mem::take(&mut state.add_value)));
|
|
}
|
|
VariableMessage::ExprEdit(name, value) => {
|
|
return Some((self.on_expr_edit)(name, value));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[allow(unstable_name_collisions)]
|
|
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
|
|
let underlay = column![
|
|
text("Variable").size(TEXT_H2),
|
|
button(text("Add")).on_press(VariableMessage::Add),
|
|
horizontal_rule(LIST_RULE),
|
|
column(
|
|
self.items
|
|
.iter()
|
|
.map(|(name, (expr, value))| {
|
|
let row = row![
|
|
text(name).size(TEXT_EMPH1),
|
|
component(ExprInput {
|
|
expr,
|
|
on_submit: |expr| VariableMessage::ExprEdit(name.clone(), expr)
|
|
}),
|
|
]
|
|
.spacing(5)
|
|
.align_items(iced::Alignment::Center);
|
|
|
|
let mut col = vec![row.into()];
|
|
if let Some(value) = value {
|
|
col.push(
|
|
row![
|
|
horizontal_space(Length::Fill),
|
|
text(&format!(" = {value} €")),
|
|
]
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
column(col).into()
|
|
})
|
|
.intersperse_with(|| horizontal_rule(LIST_RULE).into())
|
|
.collect()
|
|
),
|
|
horizontal_rule(LIST_RULE),
|
|
text(&format!(
|
|
"Total: {} €",
|
|
self.items.values().flat_map(|(_, v)| v).sum::<f64>()
|
|
))
|
|
.size(TEXT_EMPH2)
|
|
];
|
|
|
|
let overlay = match state.modal {
|
|
true => Some(
|
|
card(
|
|
text("Add variable spending"),
|
|
text_input("name", &state.add_value)
|
|
.on_input(VariableMessage::EditAdd)
|
|
.on_submit(VariableMessage::DoAdd),
|
|
)
|
|
.max_width(300.0),
|
|
),
|
|
false => None,
|
|
};
|
|
|
|
modal(underlay, overlay)
|
|
.backdrop(VariableMessage::CloseAdd)
|
|
.on_esc(VariableMessage::CloseAdd)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
struct ErrorEdit<F, G> {
|
|
v: String,
|
|
parse: F,
|
|
on_submit: G,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ErrorEditState {
|
|
error: bool,
|
|
value: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum ErrorEditMsg {
|
|
Edit(String),
|
|
Submit,
|
|
}
|
|
|
|
struct ErrorEditStyleSheet {
|
|
error: bool,
|
|
}
|
|
|
|
impl text_input::StyleSheet for ErrorEditStyleSheet {
|
|
type Style = iced::Theme;
|
|
|
|
fn active(&self, style: &Self::Style) -> text_input::Appearance {
|
|
style.active(&theme::TextInput::Default)
|
|
}
|
|
|
|
fn focused(&self, style: &Self::Style) -> text_input::Appearance {
|
|
let def = style.focused(&theme::TextInput::Default);
|
|
|
|
text_input::Appearance {
|
|
border_color: match self.error {
|
|
true => iced::Color::from_rgb8(240, 14, 27),
|
|
false => def.border_color,
|
|
},
|
|
..def
|
|
}
|
|
}
|
|
|
|
fn placeholder_color(&self, style: &Self::Style) -> iced::Color {
|
|
style.placeholder_color(&theme::TextInput::Default)
|
|
}
|
|
|
|
fn value_color(&self, style: &Self::Style) -> iced::Color {
|
|
style.value_color(&theme::TextInput::Default)
|
|
}
|
|
|
|
fn disabled_color(&self, style: &Self::Style) -> iced::Color {
|
|
style.disabled_color(&theme::TextInput::Default)
|
|
}
|
|
|
|
fn selection_color(&self, style: &Self::Style) -> iced::Color {
|
|
style.selection_color(&theme::TextInput::Default)
|
|
}
|
|
|
|
fn disabled(&self, style: &Self::Style) -> text_input::Appearance {
|
|
style.disabled(&theme::TextInput::Default)
|
|
}
|
|
}
|
|
|
|
impl<M, F, G, T, E> iced::widget::Component<M, Renderer> for ErrorEdit<F, G>
|
|
where
|
|
F: FnMut(&str) -> Result<T, E>,
|
|
G: FnMut(T) -> M,
|
|
{
|
|
type State = ErrorEditState;
|
|
type Event = ErrorEditMsg;
|
|
|
|
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
|
|
match event {
|
|
ErrorEditMsg::Edit(e) => {
|
|
state.error = (self.parse)(&e).is_err();
|
|
state.value = Some(e);
|
|
}
|
|
ErrorEditMsg::Submit => {
|
|
if let Some(v) = &state.value {
|
|
if let Ok(v) = (self.parse)(v) {
|
|
return Some((self.on_submit)(v));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> {
|
|
text_input("value", state.value.as_ref().unwrap_or(&self.v))
|
|
.on_input(ErrorEditMsg::Edit)
|
|
.on_submit(ErrorEditMsg::Submit)
|
|
.style(theme::TextInput::Custom(Box::new(ErrorEditStyleSheet {
|
|
error: state.error,
|
|
})))
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum Message {
|
|
Event(Event),
|
|
AddRecurring(String, f64),
|
|
FontLoaded(Result<(), font::Error>),
|
|
AddVariable(String),
|
|
EditVariable(String, String),
|
|
EditEarings1(f64),
|
|
EditEarings2(f64),
|
|
}
|
|
|
|
struct Glaurung {
|
|
config: Config,
|
|
recurring: BTreeMap<String, f64>,
|
|
variable: BTreeMap<String, (String, Option<f64>)>,
|
|
save_file: PathBuf,
|
|
earnings_1: f64,
|
|
earnings_2: f64,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Default)]
|
|
struct SaveFile {
|
|
#[serde(default)]
|
|
recurring: BTreeMap<String, f64>,
|
|
#[serde(default)]
|
|
variable: HashMap<String, String>,
|
|
#[serde(default)]
|
|
earnings_1: f64,
|
|
#[serde(default)]
|
|
earnings_2: f64,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct AppConfig {
|
|
save: SaveFile,
|
|
save_file: PathBuf,
|
|
config: Config,
|
|
}
|
|
|
|
impl Application for Glaurung {
|
|
type Message = Message;
|
|
type Theme = Theme;
|
|
type Executor = iced::executor::Default;
|
|
type Flags = AppConfig;
|
|
|
|
fn new(config: Self::Flags) -> (Self, Command<Message>) {
|
|
(
|
|
Self {
|
|
config: config.config,
|
|
recurring: config.save.recurring,
|
|
variable: config
|
|
.save
|
|
.variable
|
|
.into_iter()
|
|
.map(|(k, e)| {
|
|
let f = calc::calc_parser::calc(&e);
|
|
(k, (e, f.ok()))
|
|
})
|
|
.collect(),
|
|
earnings_1: config.save.earnings_1,
|
|
earnings_2: config.save.earnings_2,
|
|
save_file: config.save_file,
|
|
},
|
|
Command::batch(vec![
|
|
font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(Message::FontLoaded)
|
|
]),
|
|
)
|
|
}
|
|
|
|
fn title(&self) -> String {
|
|
"Glaurung - Account Manager".into()
|
|
}
|
|
|
|
fn subscription(&self) -> iced::Subscription<Self::Message> {
|
|
subscription::events().map(Message::Event)
|
|
}
|
|
|
|
fn update(&mut self, message: Self::Message) -> Command<Message> {
|
|
match message {
|
|
Message::AddRecurring(name, value) => {
|
|
self.recurring.insert(name, value);
|
|
}
|
|
Message::FontLoaded(r) => r.expect("could not load font"),
|
|
Message::Event(ev) => {
|
|
if let Event::Window(window::Event::CloseRequested) = ev {
|
|
let save_file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(&self.save_file)
|
|
.expect("Can't open data file");
|
|
serde_json::to_writer(
|
|
save_file,
|
|
&SaveFile {
|
|
recurring: std::mem::take(&mut self.recurring),
|
|
variable: std::mem::take(&mut self.variable)
|
|
.into_iter()
|
|
.map(|(k, (e, _))| (k, e))
|
|
.collect(),
|
|
earnings_1: self.earnings_1,
|
|
earnings_2: self.earnings_2,
|
|
},
|
|
)
|
|
.expect("could not write save file");
|
|
|
|
return window::close();
|
|
}
|
|
}
|
|
Message::AddVariable(name) => {
|
|
self.variable.insert(name, ("0".into(), Some(0.)));
|
|
}
|
|
Message::EditVariable(v, expr) => {
|
|
if let Some(entry) = self.variable.get_mut(&v) {
|
|
entry.1 = calc::calc_parser::calc(&expr).ok();
|
|
entry.0 = expr;
|
|
}
|
|
}
|
|
Message::EditEarings1(v) => {
|
|
self.earnings_1 = v;
|
|
}
|
|
Message::EditEarings2(v) => {
|
|
self.earnings_2 = v;
|
|
}
|
|
}
|
|
|
|
Command::none()
|
|
}
|
|
|
|
fn view(&self) -> Element {
|
|
column![
|
|
text("Spendings").size(TEXT_H1),
|
|
component(Recurring {
|
|
items: &self.recurring,
|
|
on_add: Message::AddRecurring,
|
|
}),
|
|
horizontal_rule(SECTION_RULE).style(|theme: &Theme| {
|
|
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
|
|
rule::Appearance { width: 3, ..def }
|
|
}),
|
|
component(VariableSpendings {
|
|
items: &self.variable,
|
|
on_add: Message::AddVariable,
|
|
on_expr_edit: Message::EditVariable,
|
|
}),
|
|
horizontal_rule(SECTION_RULE).style(|theme: &Theme| {
|
|
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
|
|
rule::Appearance { width: 3, ..def }
|
|
}),
|
|
text(&format!(
|
|
"Total spendings: {} €",
|
|
self.recurring
|
|
.values()
|
|
.chain(self.variable.values().flat_map(|(_, f)| f))
|
|
.sum::<f64>()
|
|
))
|
|
.size(TEXT_EMPH2),
|
|
text("Earnings").size(TEXT_H1),
|
|
row![
|
|
text(&self.config.person_1).size(TEXT_EMPH1),
|
|
component(ErrorEdit {
|
|
v: self.earnings_1.to_string(),
|
|
parse: |s: &str| -> Result<f64, _> { s.parse() },
|
|
on_submit: Message::EditEarings1,
|
|
})
|
|
]
|
|
.align_items(iced::Alignment::Center),
|
|
row![
|
|
text(&self.config.person_2).size(TEXT_EMPH1),
|
|
component(ErrorEdit {
|
|
v: self.earnings_2.to_string(),
|
|
parse: |s: &str| -> Result<f64, _> { s.parse() },
|
|
on_submit: Message::EditEarings2,
|
|
})
|
|
]
|
|
.align_items(iced::Alignment::Center),
|
|
text(&format!(
|
|
"Total earnings: {} €",
|
|
self.earnings_1 + self.earnings_2,
|
|
))
|
|
.size(TEXT_EMPH2),
|
|
]
|
|
.max_width(500)
|
|
.padding(5)
|
|
.into()
|
|
}
|
|
|
|
fn theme(&self) -> iced::Theme {
|
|
iced::Theme::Dark
|
|
}
|
|
}
|
|
|
|
fn person_1() -> String {
|
|
"Person 1".into()
|
|
}
|
|
|
|
fn person_2() -> String {
|
|
"Person 2".into()
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct Config {
|
|
#[serde(default = "person_1")]
|
|
person_1: String,
|
|
|
|
#[serde(default = "person_2")]
|
|
person_2: String,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
person_1: person_1(),
|
|
person_2: person_2(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let project_dir = ProjectDirs::from("net", "traxys", "glaurung").ok_or(anyhow!(""))?;
|
|
let state_dir = project_dir
|
|
.state_dir()
|
|
.ok_or(anyhow!("No state directory"))?;
|
|
std::fs::create_dir_all(state_dir)?;
|
|
|
|
let save_file = state_dir.join("data.json");
|
|
|
|
let save = match save_file.exists() {
|
|
false => Default::default(),
|
|
true => serde_json::from_reader(BufReader::new(File::open(&save_file)?))?,
|
|
};
|
|
|
|
let config_file = project_dir.config_dir().join("config.toml");
|
|
let config: Config = match config_file.exists() {
|
|
true => {
|
|
let config = std::fs::read_to_string(config_file)?;
|
|
toml::from_str(&config)?
|
|
}
|
|
false => Config::default(),
|
|
};
|
|
|
|
let mut settings = Settings::with_flags(AppConfig {
|
|
save,
|
|
save_file,
|
|
config,
|
|
});
|
|
settings.exit_on_close_request = false;
|
|
|
|
Glaurung::run(settings)?;
|
|
|
|
Ok(())
|
|
}
|