Persist app state (#21)

This commit is contained in:
Jens K. 2024-12-22 18:56:55 +01:00 committed by GitHub
parent 2cf411e2ae
commit c9b444e91a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 336 additions and 112 deletions

13
Cargo.lock generated
View File

@ -893,6 +893,18 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1049,6 +1061,7 @@ dependencies = [
"futures",
"ratatui",
"serde",
"serde_json",
"strum",
"tokio",
"tokio-stream",

View File

@ -9,6 +9,7 @@ crossterm = {version = "0.28.1", features = ["event-stream", "serde"] }
color-eyre = "0.6.2"
futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.26.3", features = ["derive"] }
tokio = { version = "1.41.1", features = ["full"] }
tokio-stream = "0.1.16"

View File

@ -1,17 +1,19 @@
use crate::{
args::{Args, ClockStyle, Content},
args::Args,
constants::TICK_VALUE_MS,
events::{Event, EventHandler, Events},
storage::AppStorage,
terminal::Terminal,
widgets::{
clock::{self, Clock, ClockArgs},
clock::{self, Clock, ClockArgs, Style},
countdown::{Countdown, CountdownWidget},
footer::Footer,
header::Header,
pomodoro::{Pomodoro, PomodoroArgs, PomodoroWidget},
pomodoro::{Mode as PomodoroMode, Pomodoro, PomodoroArgs, PomodoroWidget},
timer::{Timer, TimerWidget},
},
};
use clap::ValueEnum;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
@ -19,9 +21,23 @@ use ratatui::{
layout::{Constraint, Layout, Rect},
widgets::{StatefulWidget, Widget},
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::debug;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize,
)]
pub enum Content {
#[default]
#[value(name = "countdown", alias = "c")]
Countdown,
#[value(name = "timer", alias = "t")]
Timer,
#[value(name = "pomodoro", alias = "p")]
Pomodoro,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Running,
@ -36,49 +52,92 @@ pub struct App {
countdown: Countdown,
timer: Timer,
pomodoro: Pomodoro,
clock_style: ClockStyle,
style: Style,
with_decis: bool,
}
pub struct AppArgs {
pub style: Style,
pub with_decis: bool,
pub content: Content,
pub pomodoro_mode: PomodoroMode,
pub initial_value_work: Duration,
pub current_value_work: Duration,
pub initial_value_pause: Duration,
pub current_value_pause: Duration,
pub initial_value_countdown: Duration,
pub current_value_countdown: Duration,
pub current_value_timer: Duration,
}
/// Getting `AppArgs` by merging `Args` and `AppStorage`.
/// `Args` wins btw.
impl From<(Args, AppStorage)> for AppArgs {
fn from((args, stg): (Args, AppStorage)) -> Self {
AppArgs {
with_decis: args.decis || stg.with_decis,
content: args.mode.unwrap_or(stg.content),
style: args.style.unwrap_or(stg.style),
pomodoro_mode: stg.pomodoro_mode,
initial_value_work: args.work.unwrap_or(stg.inital_value_work),
current_value_work: stg.current_value_work,
initial_value_pause: args.pause,
current_value_pause: stg.current_value_pause,
initial_value_countdown: args.countdown,
current_value_countdown: stg.current_value_countdown,
current_value_timer: stg.current_value_timer,
}
}
}
impl App {
pub fn new(args: Args) -> Self {
let Args {
pub fn new(args: AppArgs) -> Self {
let AppArgs {
style,
work: work_initial_value,
pause: pause_initial_value,
mode: content,
countdown: countdown_initial_value,
decis: with_decis,
..
initial_value_work,
initial_value_pause,
initial_value_countdown,
current_value_work,
current_value_pause,
current_value_countdown,
current_value_timer,
content,
with_decis,
pomodoro_mode,
} = args;
Self {
mode: Mode::Running,
content,
show_menu: false,
clock_style: style,
style,
with_decis,
countdown: Countdown::new(Clock::<clock::Countdown>::new(ClockArgs {
initial_value: countdown_initial_value,
initial_value: initial_value_countdown,
current_value: current_value_countdown,
tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis,
})),
timer: Timer::new(Clock::<clock::Timer>::new(ClockArgs {
initial_value: Duration::ZERO,
current_value: current_value_timer,
tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis,
})),
pomodoro: Pomodoro::new(PomodoroArgs {
work: work_initial_value,
pause: pause_initial_value,
mode: pomodoro_mode,
initial_value_work,
current_value_work,
initial_value_pause,
current_value_pause,
style,
with_decis,
}),
}
}
pub async fn run(&mut self, mut terminal: Terminal, mut events: Events) -> Result<()> {
pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result<Self> {
while self.is_running() {
if let Some(event) = events.next().await {
// Pipe events into subviews and handle only 'unhandled' events afterwards
@ -97,7 +156,7 @@ impl App {
}
}
}
Ok(())
Ok(self)
}
fn is_running(&self) -> bool {
@ -113,11 +172,11 @@ impl App {
KeyCode::Char('p') => self.content = Content::Pomodoro,
KeyCode::Char('m') => self.show_menu = !self.show_menu,
KeyCode::Char(',') => {
self.clock_style = self.clock_style.next();
self.style = self.style.next();
// update clocks
self.timer.set_style(self.clock_style);
self.countdown.set_style(self.clock_style);
self.pomodoro.set_style(self.clock_style);
self.timer.set_style(self.style);
self.countdown.set_style(self.style);
self.pomodoro.set_style(self.style);
}
KeyCode::Char('.') => {
self.with_decis = !self.with_decis;
@ -138,6 +197,23 @@ impl App {
})?;
Ok(())
}
pub fn to_storage(&self) -> AppStorage {
AppStorage {
content: self.content,
show_menu: self.show_menu,
style: self.style,
with_decis: self.with_decis,
pomodoro_mode: self.pomodoro.get_mode().clone(),
inital_value_work: self.pomodoro.get_clock_work().initial_value,
current_value_work: self.pomodoro.get_clock_work().current_value,
inital_value_pause: self.pomodoro.get_clock_pause().initial_value,
current_value_pause: self.pomodoro.get_clock_pause().current_value,
inital_value_countdown: self.countdown.get_clock().initial_value,
current_value_countdown: self.countdown.get_clock().current_value,
current_value_timer: self.timer.get_clock().current_value,
}
}
}
struct AppWidget;

View File

@ -1,42 +1,11 @@
use clap::{Parser, ValueEnum};
use clap::Parser;
use color_eyre::{
eyre::{ensure, eyre},
Report,
};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum Content {
#[value(name = "countdown", alias = "c")]
Countdown,
#[value(name = "timer", alias = "t")]
Timer,
#[value(name = "pomodoro", alias = "p")]
Pomodoro,
}
#[derive(Debug, Copy, Clone, ValueEnum)]
pub enum ClockStyle {
#[value(name = "bold", alias = "b")]
Bold,
#[value(name = "empty", alias = "e")]
Empty,
#[value(name = "thick", alias = "t")]
Thick,
#[value(name = "cross", alias = "c")]
Cross,
}
impl ClockStyle {
pub fn next(&self) -> Self {
match self {
ClockStyle::Bold => ClockStyle::Empty,
ClockStyle::Empty => ClockStyle::Thick,
ClockStyle::Thick => ClockStyle::Cross,
ClockStyle::Cross => ClockStyle::Bold,
}
}
}
use crate::{app::Content, widgets::clock::Style};
#[derive(Parser)]
pub struct Args {
@ -45,41 +14,38 @@ pub struct Args {
help = "Countdown time to start from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'"
)]
pub countdown: Duration,
#[arg(long, short, value_parser = parse_duration,
default_value="25:00" /* 25min */,
help = "Work time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'"
)]
pub work: Duration,
pub work: Option<Duration>,
#[arg(long, short, value_parser = parse_duration,
default_value="5:00" /* 5min */,
help = "Pause time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'"
)]
pub pause: Duration,
#[arg(
long,
short = 'd',
default_value = "false",
help = "Wether to show deciseconds or not"
)]
#[arg(long, short = 'd', help = "Whether to show deciseconds or not")]
pub decis: bool,
#[arg(
short = 'm',
value_enum,
default_value = "timer",
help = "Mode to start with: [t]imer, [c]ountdown, [p]omodoro"
)]
pub mode: Content,
pub mode: Option<Content>,
#[arg(
long,
short = 's',
value_enum,
default_value = "bold",
help = "Style to display time with: [b]old, [t]hick, [c]ross, [e]mpty"
)]
pub style: ClockStyle,
pub style: Option<Style>,
#[arg(long, short = 'r', help = "Reset stored values to default")]
pub reset: bool,
}
fn parse_duration(arg: &str) -> Result<Duration, Report> {
@ -130,19 +96,16 @@ mod tests {
fn test_parse_duration() {
// ss
assert_eq!(parse_duration("50").unwrap(), Duration::from_secs(50));
// mm:ss
assert_eq!(
parse_duration("01:30").unwrap(),
Duration::from_secs(60 + 30)
);
// hh:mm:ss
assert_eq!(
parse_duration("01:30:00").unwrap(),
Duration::from_secs(60 * 60 + 30 * 60)
);
// errors
assert!(parse_duration("1:60").is_err()); // invalid seconds
assert!(parse_duration("60:00").is_err()); // invalid minutes

View File

@ -5,18 +5,20 @@ use std::fs;
use std::path::PathBuf;
pub struct Config {
pub log_dir: PathBuf,
pub data_dir: PathBuf,
}
impl Config {
pub fn init() -> Result<Self> {
let log_dir = get_default_state_dir()?.join("logs");
fs::create_dir_all(&log_dir)?;
let data_dir = get_default_state_dir()?.join("data");
fs::create_dir_all(&data_dir)?;
Ok(Self { log_dir })
Ok(Self { log_dir, data_dir })
}
}
// fn new
pub fn get_project_dir() -> Result<ProjectDirs> {
let dirs = ProjectDirs::from("", "", APP_NAME)
.ok_or_else(|| eyre!("Failed to get project directories"))?;

View File

@ -6,6 +6,7 @@ mod events;
mod logging;
mod args;
mod storage;
mod terminal;
mod utils;
mod widgets;
@ -14,20 +15,38 @@ use app::App;
use args::Args;
use clap::Parser;
use color_eyre::Result;
use config::Config;
use storage::{AppStorage, Storage};
#[tokio::main]
async fn main() -> Result<()> {
let config = config::Config::init()?;
let Config { log_dir, data_dir } = Config::init()?;
#[cfg(debug_assertions)]
logging::Logger::new(config.log_dir).init()?;
logging::Logger::new(log_dir).init()?;
color_eyre::install()?;
let args = Args::parse();
let terminal = terminal::setup()?;
let events = events::Events::new();
App::new(args).run(terminal, events).await?;
// get args given by CLI
let args = Args::parse();
// check persistant storage
let storage = Storage::new(data_dir);
// option to reset previous stored data to `default`
let stg = if args.reset {
AppStorage::default()
} else {
storage.load().unwrap_or_default()
};
// merge `Args` and `AppStorage`.
let app_args = (args, stg).into();
let app_storage = App::new(app_args).run(terminal, events).await?.to_storage();
// store app state persistantly
storage.save(app_storage)?;
terminal::teardown()?;
Ok(())

82
src/storage.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::{
app::Content,
constants::APP_NAME,
widgets::{clock::Style, pomodoro::Mode as PomodoroMode},
};
use color_eyre::eyre::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Serialize, Deserialize)]
pub struct AppStorage {
pub content: Content,
pub show_menu: bool,
pub style: Style,
pub with_decis: bool,
pub pomodoro_mode: PomodoroMode,
// pomodoro -> work
pub inital_value_work: Duration,
pub current_value_work: Duration,
// pomodoro -> pause
pub inital_value_pause: Duration,
pub current_value_pause: Duration,
// countdown
pub inital_value_countdown: Duration,
pub current_value_countdown: Duration,
// timer
pub current_value_timer: Duration,
}
impl Default for AppStorage {
fn default() -> Self {
const DEFAULT_WORK: Duration = Duration::from_secs(60 * 25); /* 25min */
const DEFAULT_PAUSE: Duration = Duration::from_secs(60 * 5); /* 5min */
const DEFAULT_COUNTDOWN: Duration = Duration::from_secs(60 * 10); /* 10min */
AppStorage {
content: Content::default(),
show_menu: false,
style: Style::default(),
with_decis: false,
pomodoro_mode: PomodoroMode::Work,
// pomodoro -> work
inital_value_work: DEFAULT_WORK,
current_value_work: DEFAULT_WORK,
// pomodoro -> pause
inital_value_pause: DEFAULT_PAUSE,
current_value_pause: DEFAULT_PAUSE,
// countdown
inital_value_countdown: DEFAULT_COUNTDOWN,
current_value_countdown: DEFAULT_COUNTDOWN,
// timer
current_value_timer: Duration::ZERO,
}
}
}
pub struct Storage {
data_dir: PathBuf,
}
impl Storage {
pub fn new(data_dir: PathBuf) -> Self {
Self { data_dir }
}
fn get_storage_path(&self) -> PathBuf {
self.data_dir.join(format!("{}.data", APP_NAME))
}
pub fn save(&self, data: AppStorage) -> Result<()> {
let file = fs::File::create(self.get_storage_path())?;
serde_json::to_writer(file, &data)?;
Ok(())
}
pub fn load(&self) -> Result<AppStorage> {
let file = fs::File::open(self.get_storage_path())?;
let data = serde_json::from_reader(file)?;
Ok(data)
}
}

View File

@ -1,3 +1,5 @@
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::marker::PhantomData;
use std::time::Duration;
@ -9,7 +11,7 @@ use ratatui::{
widgets::StatefulWidget,
};
use crate::{args::ClockStyle, utils::center_horizontal};
use crate::utils::center_horizontal;
#[derive(Debug, Copy, Clone, Display, PartialEq, Eq)]
pub enum Time {
@ -74,22 +76,47 @@ pub enum Format {
HhMmSs,
}
#[derive(Debug, Copy, Clone, ValueEnum, Default, Serialize, Deserialize)]
pub enum Style {
#[default]
#[value(name = "bold", alias = "b")]
Bold,
#[value(name = "empty", alias = "e")]
Empty,
#[value(name = "thick", alias = "t")]
Thick,
#[value(name = "cross", alias = "c")]
Cross,
}
impl Style {
pub fn next(&self) -> Self {
match self {
Style::Bold => Style::Empty,
Style::Empty => Style::Thick,
Style::Thick => Style::Cross,
Style::Cross => Style::Bold,
}
}
}
#[derive(Debug, Clone)]
pub struct Clock<T> {
initial_value: Duration,
pub initial_value: Duration,
pub current_value: Duration,
tick_value: Duration,
current_value: Duration,
mode: Mode,
format: Format,
pub style: ClockStyle,
pub style: Style,
pub with_decis: bool,
phantom: PhantomData<T>,
}
pub struct ClockArgs {
pub initial_value: Duration,
pub current_value: Duration,
pub tick_value: Duration,
pub style: ClockStyle,
pub style: Style,
pub with_decis: bool,
}
@ -315,15 +342,22 @@ impl Clock<Countdown> {
pub fn new(args: ClockArgs) -> Self {
let ClockArgs {
initial_value,
current_value,
tick_value,
style,
with_decis,
} = args;
let mut instance = Self {
initial_value,
current_value,
tick_value,
current_value: initial_value,
mode: Mode::Initial,
mode: if current_value == Duration::ZERO {
Mode::Done
} else if current_value == initial_value {
Mode::Initial
} else {
Mode::Pause
},
format: Format::S,
style,
with_decis,
@ -380,15 +414,22 @@ impl Clock<Timer> {
pub fn new(args: ClockArgs) -> Self {
let ClockArgs {
initial_value,
current_value,
tick_value,
style,
with_decis,
} = args;
let mut instance = Self {
initial_value,
current_value,
tick_value,
current_value: Duration::ZERO,
mode: Mode::Initial,
mode: if current_value == initial_value {
Mode::Initial
} else if current_value >= MAX_DURATION {
Mode::Done
} else {
Mode::Pause
},
format: Format::S,
phantom: PhantomData,
style,
@ -552,12 +593,12 @@ where
}
}
fn get_digit_symbol(&self, style: &ClockStyle) -> &str {
fn get_digit_symbol(&self, style: &Style) -> &str {
match &style {
ClockStyle::Bold => "",
ClockStyle::Empty => "",
ClockStyle::Cross => "",
ClockStyle::Thick => "",
Style::Bold => "",
Style::Empty => "",
Style::Cross => "",
Style::Thick => "",
}
}

View File

@ -8,10 +8,9 @@ use ratatui::{
use std::cmp::max;
use crate::{
args::ClockStyle,
events::{Event, EventHandler},
utils::center,
widgets::clock::{self, Clock, ClockWidget},
widgets::clock::{self, Clock, ClockWidget, Style},
};
#[derive(Debug, Clone)]
@ -24,13 +23,17 @@ impl Countdown {
Self { clock }
}
pub fn set_style(&mut self, style: ClockStyle) {
pub fn set_style(&mut self, style: Style) {
self.clock.style = style;
}
pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock.with_decis = with_decis;
}
pub fn get_clock(&self) -> &Clock<clock::Countdown> {
&self.clock
}
}
impl EventHandler for Countdown {

View File

@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use crate::args::Content;
use crate::app::Content;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},

View File

@ -1,9 +1,8 @@
use crate::{
args::ClockStyle,
constants::TICK_VALUE_MS,
events::{Event, EventHandler},
utils::center,
widgets::clock::{Clock, ClockWidget, Countdown},
widgets::clock::{Clock, ClockWidget, Countdown, Style},
};
use ratatui::{
buffer::Buffer,
@ -16,10 +15,12 @@ use std::{cmp::max, time::Duration};
use strum::Display;
use serde::{Deserialize, Serialize};
use super::clock::ClockArgs;
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq)]
enum Mode {
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)]
pub enum Mode {
Work,
Pause,
}
@ -46,31 +47,39 @@ pub struct Pomodoro {
}
pub struct PomodoroArgs {
pub work: Duration,
pub pause: Duration,
pub style: ClockStyle,
pub mode: Mode,
pub initial_value_work: Duration,
pub current_value_work: Duration,
pub initial_value_pause: Duration,
pub current_value_pause: Duration,
pub style: Style,
pub with_decis: bool,
}
impl Pomodoro {
pub fn new(args: PomodoroArgs) -> Self {
let PomodoroArgs {
work,
pause,
mode,
initial_value_work,
current_value_work,
initial_value_pause,
current_value_pause,
style,
with_decis,
} = args;
Self {
mode: Mode::Work,
mode,
clock_map: ClockMap {
work: Clock::<Countdown>::new(ClockArgs {
initial_value: work,
initial_value: initial_value_work,
current_value: current_value_work,
tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis,
}),
pause: Clock::<Countdown>::new(ClockArgs {
initial_value: pause,
initial_value: initial_value_pause,
current_value: current_value_pause,
tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis,
@ -79,11 +88,23 @@ impl Pomodoro {
}
}
fn get_clock(&mut self) -> &mut Clock<Countdown> {
pub fn get_clock(&mut self) -> &mut Clock<Countdown> {
self.clock_map.get(&self.mode)
}
pub fn set_style(&mut self, style: crate::args::ClockStyle) {
pub fn get_clock_work(&self) -> &Clock<Countdown> {
&self.clock_map.work
}
pub fn get_clock_pause(&self) -> &Clock<Countdown> {
&self.clock_map.pause
}
pub fn get_mode(&self) -> &Mode {
&self.mode
}
pub fn set_style(&mut self, style: Style) {
self.clock_map.work.style = style;
self.clock_map.pause.style = style;
}

View File

@ -1,8 +1,7 @@
use crate::{
args::ClockStyle,
events::{Event, EventHandler},
utils::center,
widgets::clock::{self, Clock, ClockWidget},
widgets::clock::{self, Clock, ClockWidget, Style},
};
use ratatui::{
buffer::Buffer,
@ -23,13 +22,17 @@ impl Timer {
Self { clock }
}
pub fn set_style(&mut self, style: ClockStyle) {
pub fn set_style(&mut self, style: Style) {
self.clock.style = style;
}
pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock.with_decis = with_decis;
}
pub fn get_clock(&self) -> &Clock<clock::Timer> {
&self.clock
}
}
impl EventHandler for Timer {