diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 77fc061..d3763c4 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -9,7 +9,7 @@ license=('MIT') depends=('alsa-lib' 'gtk3' 'hicolor-icon-theme' 'glibc' 'webkit2gtk-4.1' 'libsoup' 'cairo' 'glib2' 'pango' 'gcc-libs' 'gdk-pixbuf2' 'libayatana-appindicator') provides=('pomodorolm') source=("https://github.com/vjousse/pomodorolm/releases/download/app-v$pkgver/pomodorolm_${pkgver}_amd64.deb") -sha256sums=('8042ccb3d1be79c96ff8b5107ec5e1aee44cf09853d991dba8bd462e3fa14eb8') +sha256sums=('2cc41dbd3937998db8c10387a581af8779fc5d5cbbf2972c6dbe786df9f2f5ce') package() { bsdtar -xf "$srcdir/data.tar.gz" -C "$pkgdir" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7a23b9c..8512594 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -801,8 +801,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -3810,6 +3812,8 @@ dependencies = [ name = "pomodorolm" version = "0.8.0" dependencies = [ + "anyhow", + "chrono", "clap", "dirs 6.0.0", "futures", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c63d068..0379943 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -5,8 +5,8 @@ description = "A Tauri App" authors = ["Vincent Jousse"] license = "" repository = "" -edition = "2021" -rust-version = "1.60" +edition = "2024" +rust-version = "1.89" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,6 +33,8 @@ rustls-pemfile = "2.2.0" rodio = "0.19.0" clap = { version = "4.0.32", features = ["derive"] } dirs = "6.0.0" +anyhow = "1.0.98" +chrono = { version = "0.4.42", features = ["serde"] } [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index d7b4d86..341e953 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -1,84 +1,82 @@ extern crate dirs; -use crate::config::Config; +use crate::config::{Config, pomodoro_state_from_config}; +use crate::pomodoro::{SessionStatus, get_session_info, tick_with_file_session_info}; +use anyhow::{Context, Result}; use std::fs; use std::path::Path; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use tokio::time::interval; -pub fn run(config_dir_name: &str) { +pub fn run(config_dir_name: &str, display_label: bool) -> Result<()> { let config_dir = dirs::config_dir() .expect("Error while getting the config directory") .join(config_dir_name); let config = - Config::get_or_create_from_disk(&config_dir, None).expect("Unable to get config file"); + Config::get_or_create_from_disk(&config_dir, None).context("Unable to get config file")?; // Initialize the Tokio runtime let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(run_pomodoro_checker(config)); + rt.block_on(run_pomodoro_checker(config, display_label)) } -async fn run_pomodoro_checker(config: Config) { - let cache_dir = dirs::cache_dir().expect("Error while getting the cache directory"); +async fn run_pomodoro_checker(config: Config, display_label: bool) -> Result<()> { + let mut pomodoro = pomodoro_state_from_config(&config); - let file_path = cache_dir.join("pomodoro_session"); let mut interval = interval(Duration::from_secs(1)); loop { interval.tick().await; - if file_exists(&file_path).await { - if let Some(remaining_time) = - get_remaining_time(&file_path, config.focus_duration as u64).await - { - let total_seconds = config.focus_duration as u64; // Total time for Pomodoro in seconds - let remaining_seconds = remaining_time.as_secs(); - let elapsed_seconds = total_seconds - remaining_seconds; - - // Create the progress bar - let progress_bar = create_progress_bar(total_seconds, elapsed_seconds); - let formatted_time = format_time(remaining_seconds); - - // Check if remaining time is zero - if remaining_seconds == 0 { - // Delete the session file - if let Err(e) = fs::remove_file(&file_path) { - eprintln!("Failed to delete session file: {e}"); - } - continue; - } + let next_pomodoro = tick_with_file_session_info( + &pomodoro, + get_session_info(&pomodoro.config.session_file), + )?; + + if next_pomodoro.current_session.elapsed_seconds == 1 { + println!("-> New pomodoro created"); + } + + // Create the progress bar + let progress_bar = create_progress_bar( + next_pomodoro.config.focus_duration.into(), + next_pomodoro.current_session.elapsed_seconds.into(), + ); + let remaining_seconds = (next_pomodoro.config.focus_duration + - next_pomodoro.current_session.elapsed_seconds) as u64; + let formatted_time = format_time(remaining_seconds); + + if next_pomodoro.current_session.status != SessionStatus::NotStarted { + if let Some(ref label) = next_pomodoro.current_session.label + && display_label + { + println!("{progress_bar} {formatted_time} {}", label); + } else { println!("{progress_bar} {formatted_time}"); } } else { println!("P -"); } + + // Check if remaining time is zero + if remaining_seconds == 0 && file_exists(&pomodoro.config.session_file) { + // Delete the session file + fs::remove_file(&pomodoro.config.session_file) + .context("Failed to delete session file")?; + println!("-> Pomodoro ended normally"); + continue; + } + + pomodoro = next_pomodoro; } } -async fn file_exists(path: &Path) -> bool { +fn file_exists(path: &Path) -> bool { fs::metadata(path).is_ok() } -async fn get_remaining_time(path: &Path, duration: u64) -> Option { - if let Ok(metadata) = fs::metadata(path) { - if let Ok(modified_time) = metadata.modified() { - let now = SystemTime::now(); - let elapsed = now.duration_since(modified_time).ok()?; - let total_duration = Duration::from_secs(duration); - - if elapsed >= total_duration { - return Some(Duration::from_secs(0)); // Return zero if the time is up - } - - let remaining = total_duration - elapsed; - return Some(remaining); - } - } - None -} - fn create_progress_bar(total_seconds: u64, elapsed_seconds: u64) -> String { let total_hashes = 10; // Total number of '#' in the progress bar let filled_length = diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 058b4f0..d863acd 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,4 +1,5 @@ -use crate::pomodoro; +use crate::pomodoro::{self, default_session_file}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::fs; use std::fs::OpenOptions; @@ -32,6 +33,8 @@ pub struct Config { pub minimize_to_tray_on_close: bool, #[serde(default)] pub muted: bool, + #[serde(default = "default_session_file")] + pub session_file: PathBuf, pub short_break_audio: Option, pub short_break_duration: u16, #[serde(default)] @@ -69,7 +72,7 @@ impl Config { pub fn get_or_create_from_disk( config_dir: &Path, config_file_name: Option, - ) -> Result> { + ) -> Result { let config_file_path = Self::get_config_file_path(config_dir, config_file_name); // Create the config dir and the themes one if they don’t exist @@ -120,6 +123,7 @@ impl Default for Config { minimize_to_tray: true, minimize_to_tray_on_close: true, muted: false, + session_file: default_session_file(), short_break_audio: None, short_break_duration: 5 * 60, start_minimized: false, @@ -130,3 +134,26 @@ impl Default for Config { } } } + +pub fn pomodoro_config(config: &Config) -> pomodoro::Config { + pomodoro::Config { + auto_start_long_break_timer: config.auto_start_break_timer, + auto_start_short_break_timer: config.auto_start_break_timer, + auto_start_focus_timer: config.auto_start_work_timer, + default_focus_label: config.default_focus_label.clone(), + default_short_break_label: config.default_short_break_label.clone(), + default_long_break_label: config.default_long_break_label.clone(), + focus_duration: config.focus_duration, + long_break_duration: config.long_break_duration, + max_focus_rounds: config.max_round_number, + session_file: config.session_file.clone(), + short_break_duration: config.short_break_duration, + } +} + +pub fn pomodoro_state_from_config(config: &Config) -> pomodoro::Pomodoro { + pomodoro::Pomodoro { + config: pomodoro_config(config), + ..pomodoro::Pomodoro::default() + } +} diff --git a/src-tauri/src/gui.rs b/src-tauri/src/gui.rs index ea78a88..1203948 100644 --- a/src-tauri/src/gui.rs +++ b/src-tauri/src/gui.rs @@ -3,7 +3,7 @@ // Fix for https://github.com/tauri-apps/tauri/issues/12382 #![allow(deprecated)] -use crate::config::Config; +use crate::config::{Config, pomodoro_config, pomodoro_state_from_config}; use crate::icon; use crate::pomodoro; use crate::sound; @@ -14,15 +14,16 @@ use std::fs::OpenOptions; use std::io::Write; use std::sync::Arc; use std::time::Duration; -use tauri::menu::{MenuBuilder, MenuItemBuilder}; -use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent}; use tauri::AppHandle; use tauri::Runtime; -use tauri::{path::BaseDirectory, Manager}; +use tauri::menu::{MenuBuilder, MenuItemBuilder}; +use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent}; +use tauri::{Manager, path::BaseDirectory}; use tokio::sync::Mutex; use tokio::time; // 1.3.0 // pub struct AppState(Arc>); pub struct AppMenuStates(std::sync::Mutex>); +use anyhow::Result; use futures::StreamExt; use hex_color::HexColor; use std::path::PathBuf; @@ -295,6 +296,53 @@ pub fn run_app(config_dir_name: &str, _builder: tauri::Builder) { } _ => (), }) + .on_menu_event(move |app, event| match event.id().as_ref() { + "quit" => { + app.exit(0); + } + "toggle_play" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.emit("toggle-play", ""); + } + } + "skip" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.emit("skip", ""); + } + } + "toggle_visibility" => { + if let Some(window) = app.get_webview_window("main") { + let new_title = if window.is_visible().unwrap_or_default() { + #[cfg(target_os = "macos")] + let _ = app.hide(); + #[cfg(not(target_os = "macos"))] + let _ = window.hide(); + "Show" + } else { + #[cfg(target_os = "macos")] + let _ = app.show(); + let _ = window.show(); + let _ = window.set_focus(); + "Hide" + }; + + let state: tauri::State<'_, AppMenuStates> = app.state(); + + let state_guard = state.0.lock(); + match state_guard { + Ok(guard) => { + let set_text_result = + guard.toggle_visibility_menu.set_text(new_title); + if let Err(e) = set_text_result { + eprintln!("Error setting MenuItem title: {e:?}."); + } + } + Err(e) => eprintln!("Error getting state lock: {e:?}."), + }; + } + } + _ => (), + }) .build(app); let config = read_config_from_disk(&config_dir_name_owned, app.path())?; @@ -306,10 +354,10 @@ pub fn run_app(config_dir_name: &str, _builder: tauri::Builder) { #[cfg(not(target_os = "macos"))] let _ = window.hide(); let _ = toggle_visibility.set_text("Show"); - }; + } let _ = window.set_always_on_top(config.always_on_top); - } + }; let pomodoro = pomodoro_state_from_config(&config); @@ -385,31 +433,12 @@ fn manage_autostart( fn read_config_from_disk( config_dir_name: &str, app_path: &tauri::path::PathResolver, -) -> Result> { +) -> Result { let config_dir = get_config_dir(config_dir_name, app_path)?; Config::get_or_create_from_disk(&config_dir, None) } -fn pomodoro_config(config: &Config) -> pomodoro::Config { - pomodoro::Config { - auto_start_long_break_timer: config.auto_start_break_timer, - auto_start_short_break_timer: config.auto_start_break_timer, - auto_start_focus_timer: config.auto_start_work_timer, - focus_duration: config.focus_duration, - long_break_duration: config.long_break_duration, - max_focus_rounds: config.max_round_number, - short_break_duration: config.short_break_duration, - } -} - -fn pomodoro_state_from_config(config: &Config) -> Pomodoro { - pomodoro::Pomodoro { - config: pomodoro_config(config), - ..pomodoro::Pomodoro::default() - } -} - fn get_themes_for_directory(themes_path: PathBuf) -> Vec { let mut themes_paths_bufs: Vec = vec![]; let themes_path_dir = fs::read_dir(themes_path.clone()); @@ -460,11 +489,15 @@ async fn tick(app_handle: AppHandle, path: String) { let state: tauri::State = app_handle.state(); let new_state = state.clone(); let mut state_guard = new_state.0.lock().await; - //let play_tick: bool = state_guard.play_tick; + let play_tick: bool = should_play_tick_sound(&state_guard.config, &state_guard.pomodoro); - state_guard.pomodoro = pomodoro::tick(&state_guard.pomodoro); + state_guard.pomodoro = pomodoro::tick_with_file_session_info( + &state_guard.pomodoro, + pomodoro::get_session_info(&state_guard.pomodoro.config.session_file), + ) + .expect("Error when ticking pomodoro"); let _ = window.emit("external-message", state_guard.pomodoro.to_unborrowed()); @@ -793,25 +826,42 @@ async fn handle_external_message( ) -> Result { let mut app_state_guard = state.0.lock().await; + eprintln!("[rust] external message: {name}"); + match name.as_str() { "pause" => { - app_state_guard.pomodoro = pomodoro::pause(&app_state_guard.pomodoro); + app_state_guard.pomodoro = + pomodoro::pause_with_session_file(&app_state_guard.pomodoro, None).map_err( + |e| { + eprintln!("[rust] Unable to play pomodoro `{e}`."); + }, + )?; } "play" => { - app_state_guard.pomodoro = pomodoro::play(&app_state_guard.pomodoro); + app_state_guard.pomodoro = + pomodoro::play_with_session_file(&app_state_guard.pomodoro, None).map_err(|e| { + eprintln!("[rust] Unable to play pomodoro `{e}`."); + })?; } "quit" => { app.exit(0); } "reset_round" => { - app_state_guard.pomodoro = pomodoro::reset_round(&app_state_guard.pomodoro); + app_state_guard.pomodoro = + pomodoro::reset_round(&app_state_guard.pomodoro).map_err(|e| { + eprintln!("[rust] Unable to reset pomodoro round `{e}`."); + })?; } "reset_session" => { - app_state_guard.pomodoro = pomodoro::reset_session(&app_state_guard.pomodoro); + app_state_guard.pomodoro = + pomodoro::reset_session(&app_state_guard.pomodoro).map_err(|e| { + eprintln!("[rust] Unable to reset pomodoro session `{e}`."); + })?; + app_state_guard.pomodoro.current_work_round_number = 1; } "skip" => { - app_state_guard.pomodoro = pomodoro::next(&app_state_guard.pomodoro); + app_state_guard.pomodoro = pomodoro::get_next_pomodoro(&app_state_guard.pomodoro); } message => eprintln!("[rust] Got unknown message `{message}`, ignoring."), } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e4c99cd..d0e1267 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,8 +6,10 @@ pub mod gui; mod icon; pub mod pomodoro; mod sound; +use anyhow::Result; #[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run_gui(config_dir_name: &str) { - gui::run_app(config_dir_name, tauri::Builder::default()) +pub fn run_gui(config_dir_name: &str) -> Result<()> { + gui::run_app(config_dir_name, tauri::Builder::default()); + Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 25aa572..32e0531 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,8 +1,9 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use clap::Parser; - use clap::Subcommand; +use anyhow::Result; + #[derive(Parser)] #[command(version, about, long_about = None)] #[command(propagate_version = true)] @@ -14,19 +15,25 @@ struct Cli { #[derive(Subcommand)] enum Commands { /// Run the CLI version of the app - Cli, + Cli { + /// Display current session label after the timer + #[arg(short, long, default_value_t = false)] + display_label: bool, + }, } const CONFIG_DIR_NAME: &str = "pomodorolm"; -fn main() { +fn main() -> Result<()> { let cli = Cli::parse(); // You can check for the existence of subcommands, and if found use their // matches just as you would the top level cmd match &cli.command { Some(command) => match command { - Commands::Cli => pomodorolm_lib::cli::run(CONFIG_DIR_NAME), + Commands::Cli { display_label } => { + pomodorolm_lib::cli::run(CONFIG_DIR_NAME, *display_label) + } }, None => pomodorolm_lib::run_gui(CONFIG_DIR_NAME), } diff --git a/src-tauri/src/pomodoro.rs b/src-tauri/src/pomodoro.rs index a1dce96..509ac85 100644 --- a/src-tauri/src/pomodoro.rs +++ b/src-tauri/src/pomodoro.rs @@ -1,4 +1,13 @@ +use anyhow::{Context, Result, anyhow}; use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt; +use std::fs; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::{Duration, SystemTime}; #[derive(PartialEq, Copy, Debug, Serialize, Deserialize, Clone)] pub enum SessionStatus { @@ -7,6 +16,41 @@ pub enum SessionStatus { Running, } +#[derive(Debug, PartialEq, Eq)] +pub struct ParseSessionStatusError; + +impl fmt::Display for ParseSessionStatusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "unable to parse session status") + } +} + +impl Error for ParseSessionStatusError {} + +impl FromStr for SessionStatus { + type Err = ParseSessionStatusError; + + fn from_str(s: &str) -> Result { + let opt = match s.to_lowercase().as_str() { + "notstarted" => Self::NotStarted, + "paused" => Self::Paused, + "running" => Self::Running, + _ => return Err(ParseSessionStatusError), + }; + Ok(opt) + } +} + +impl fmt::Display for SessionStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + SessionStatus::NotStarted => write!(f, "notstarted"), + SessionStatus::Paused => write!(f, "paused"), + SessionStatus::Running => write!(f, "running"), + } + } +} + #[derive(Copy, Debug, PartialEq, Serialize, Deserialize, Clone)] pub enum SessionType { Focus, @@ -14,16 +58,71 @@ pub enum SessionType { LongBreak, } +impl fmt::Display for SessionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + SessionType::Focus => write!(f, "focus"), + SessionType::ShortBreak => write!(f, "shortbreak"), + SessionType::LongBreak => write!(f, "longbreak"), + } + } +} + +#[derive(Debug, Clone)] +pub struct SessionInfo { + pub elapsed_seconds: Seconds, + pub label: String, + pub session_status: SessionStatus, + pub session_type: SessionType, +} + +#[derive(Debug)] +struct SessionLineContent { + elapsed_seconds: Option, + label: String, + session_status: SessionStatus, + session_type: SessionType, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseSessionTypeError; + +impl fmt::Display for ParseSessionTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "unable to parse session type") + } +} + +impl Error for ParseSessionTypeError {} + +impl FromStr for SessionType { + type Err = ParseSessionTypeError; + + fn from_str(s: &str) -> Result { + let opt = match s.to_lowercase().as_str() { + "focus" => Self::Focus, + "longbreak" => Self::LongBreak, + "shortbreak" => Self::ShortBreak, + _ => return Err(ParseSessionTypeError), + }; + Ok(opt) + } +} + type Seconds = u16; -#[derive(PartialEq, Copy, Debug, Serialize, Deserialize, Clone)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct Config { pub auto_start_long_break_timer: bool, pub auto_start_short_break_timer: bool, pub auto_start_focus_timer: bool, + pub default_focus_label: String, + pub default_long_break_label: String, + pub default_short_break_label: String, pub focus_duration: Seconds, pub long_break_duration: Seconds, pub max_focus_rounds: u16, + pub session_file: PathBuf, pub short_break_duration: Seconds, } @@ -33,14 +132,24 @@ impl Default for Config { auto_start_long_break_timer: false, auto_start_short_break_timer: false, auto_start_focus_timer: false, + default_focus_label: "Focus".to_owned(), + default_long_break_label: "Long Break".to_owned(), + default_short_break_label: "Short Break".to_owned(), focus_duration: 25 * 60, long_break_duration: 20 * 60, max_focus_rounds: 4, + session_file: default_session_file(), short_break_duration: 5 * 60, } } } +pub fn default_session_file() -> PathBuf { + dirs::cache_dir().map_or(PathBuf::from("~/.cache/pomodorolm_session"), |p| { + p.join("pomodorolm_session") + }) +} + #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct Pomodoro { pub config: Config, @@ -67,20 +176,13 @@ pub struct SessionUnburrowed { impl Default for Pomodoro { fn default() -> Self { Pomodoro { - config: Config { - auto_start_long_break_timer: false, - auto_start_short_break_timer: false, - auto_start_focus_timer: false, - focus_duration: 25 * 60, - long_break_duration: 20 * 60, - max_focus_rounds: 4, - short_break_duration: 5 * 60, - }, + config: Config::default(), current_session: Session::default(), current_work_round_number: 1, } } } + impl Pomodoro { pub fn duration_of_session(&self, session: &Session) -> Seconds { match session.session_type { @@ -92,10 +194,10 @@ impl Pomodoro { pub fn to_unborrowed(&self) -> PomodoroUnborrowed { PomodoroUnborrowed { - config: self.config, + config: self.config.clone(), current_work_round_number: self.current_work_round_number, current_session: SessionUnburrowed { - current_time: self.current_session.current_time, + current_time: self.current_session.elapsed_seconds, session_type: self.current_session.session_type, status: self.current_session.status, label: self.current_session.label.clone(), @@ -106,17 +208,32 @@ impl Pomodoro { #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct Session { - pub current_time: Seconds, + pub elapsed_seconds: Seconds, pub label: Option, + pub session_file: Option, pub session_type: SessionType, pub status: SessionStatus, } +impl fmt::Display for Session { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{};{};{};{}", + self.session_type, + self.label.clone().unwrap_or("working".into()), + self.status, + self.elapsed_seconds + ) + } +} + impl Default for Session { fn default() -> Self { Session { - current_time: 0, + elapsed_seconds: 0, label: None, + session_file: None, session_type: SessionType::Focus, status: SessionStatus::NotStarted, } @@ -128,46 +245,160 @@ pub fn pause(pomodoro: &Pomodoro) -> Pomodoro { current_session: Session { status: SessionStatus::Paused, label: pomodoro.current_session.label.clone(), + session_file: pomodoro.current_session.session_file.clone(), ..pomodoro.current_session }, + config: pomodoro.config.clone(), ..*pomodoro } } -pub fn play(pomodoro: &Pomodoro) -> Pomodoro { - Pomodoro { +pub fn pause_with_session_file( + pomodoro: &Pomodoro, + _session_info: Option, +) -> Result { + let new_pomodoro = Pomodoro { current_session: Session { - status: SessionStatus::Running, + status: SessionStatus::Paused, label: pomodoro.current_session.label.clone(), + session_file: pomodoro.current_session.session_file.clone(), ..pomodoro.current_session }, + config: pomodoro.config.clone(), ..*pomodoro + }; + + write_session_file_from_pomodoro( + &new_pomodoro, + new_pomodoro.current_session.session_type, + SessionStatus::Paused, + )?; + + Ok(new_pomodoro) +} + +pub fn play_with_session_file( + pomodoro: &Pomodoro, + session_info: Option, +) -> Result { + eprintln!("### -> session_info: {session_info:?}"); + + let mut new_pomodoro = pomodoro.clone(); + + if let Some(info) = session_info { + let current_session = Session { + elapsed_seconds: info.elapsed_seconds, + label: Some(info.label), + session_file: Some(pomodoro.config.session_file.clone()), + session_type: SessionType::Focus, + status: SessionStatus::NotStarted, + }; + + new_pomodoro.current_session = current_session; } + + if pomodoro.current_session.session_file.is_none() { + eprintln!("[rust] creating session file"); + write_session_file_from_pomodoro( + pomodoro, + pomodoro.current_session.session_type, + SessionStatus::Running, + )?; + }; + + new_pomodoro = play(&new_pomodoro)?; + + new_pomodoro.current_session.session_file = Some(pomodoro.config.session_file.clone()); + // new_pomodoro.current_session.elapsed_seconds = session_info.elapsed_seconds; + + Ok(new_pomodoro) } -pub fn reset_round(pomodoro: &Pomodoro) -> Pomodoro { - Pomodoro { +pub fn play(pomodoro: &Pomodoro) -> Result { + let new_pomodoro = Pomodoro { + current_session: Session { + status: SessionStatus::Running, + label: pomodoro.current_session.label.clone(), + session_file: Some(pomodoro.config.session_file.clone()), + elapsed_seconds: pomodoro.current_session.elapsed_seconds, + ..pomodoro.current_session + }, + config: pomodoro.config.clone(), + ..*pomodoro + }; + Ok(new_pomodoro) +} + +pub fn remove_session_file(pomodoro: &Pomodoro) -> io::Result<()> { + if let Some(session_file) = &pomodoro.current_session.session_file { + // Check that the file exists + if fs::metadata(session_file).is_ok() { + eprintln!("[rust] removing {session_file:?}"); + fs::remove_file(session_file)?; + }; + }; + Ok(()) +} + +pub fn write_session_file_from_pomodoro( + pomodoro: &Pomodoro, + session_type: SessionType, + session_status: SessionStatus, +) -> io::Result { + write_session_file( + &Session { + status: session_status, + label: pomodoro.current_session.label.clone(), + session_file: Some(pomodoro.config.session_file.clone()), + session_type, + elapsed_seconds: pomodoro.current_session.elapsed_seconds, + }, + pomodoro.config.session_file.clone(), + ) +} + +pub fn write_session_file(session: &Session, session_file: PathBuf) -> io::Result { + eprintln!("[rust] writing session file {:?}", session_file); + fs::create_dir_all(session_file.clone().parent().unwrap())?; + File::create(session_file.clone())?; + fs::write(&session_file, format!("{}", session))?; + + Ok(session_file.clone()) +} + +pub fn reset_round(pomodoro: &Pomodoro) -> Result { + remove_session_file(pomodoro)?; + + Ok(Pomodoro { current_session: Session { status: SessionStatus::NotStarted, - current_time: 0, + elapsed_seconds: 0, label: pomodoro.current_session.label.clone(), + session_file: None, ..pomodoro.current_session }, + config: pomodoro.config.clone(), ..*pomodoro - } + }) } -pub fn reset_session(pomodoro: &Pomodoro) -> Pomodoro { - Pomodoro { +pub fn reset_session(pomodoro: &Pomodoro) -> io::Result { + if let Some(session_file) = &pomodoro.current_session.session_file { + eprintln!("[rust] removing {session_file:?}"); + fs::remove_file(session_file)?; + }; + + Ok(Pomodoro { current_session: Session { status: SessionStatus::NotStarted, - current_time: 0, + elapsed_seconds: 0, label: pomodoro.current_session.label.clone(), + session_file: pomodoro.current_session.session_file.clone(), session_type: SessionType::Focus, }, + config: pomodoro.config.clone(), current_work_round_number: 1, - ..*pomodoro - } + }) } pub fn get_next_session(pomodoro: &Pomodoro) -> Session { @@ -177,6 +408,7 @@ pub fn get_next_session(pomodoro: &Pomodoro) -> Session { if pomodoro.current_work_round_number == pomodoro.config.max_focus_rounds { Session { session_type: SessionType::LongBreak, + session_file: session.session_file, status: if pomodoro.config.auto_start_long_break_timer { SessionStatus::Running } else { @@ -188,6 +420,7 @@ pub fn get_next_session(pomodoro: &Pomodoro) -> Session { } else { Session { session_type: SessionType::ShortBreak, + session_file: session.session_file, status: if pomodoro.config.auto_start_short_break_timer { SessionStatus::Running } else { @@ -199,6 +432,7 @@ pub fn get_next_session(pomodoro: &Pomodoro) -> Session { } _ => Session { session_type: SessionType::Focus, + session_file: session.session_file, status: if pomodoro.config.auto_start_focus_timer { SessionStatus::Running } else { @@ -209,7 +443,7 @@ pub fn get_next_session(pomodoro: &Pomodoro) -> Session { } } -pub fn next(pomodoro: &Pomodoro) -> Pomodoro { +pub fn get_next_pomodoro(pomodoro: &Pomodoro) -> Pomodoro { Pomodoro { current_session: get_next_session(pomodoro), current_work_round_number: match pomodoro.current_session.session_type { @@ -217,31 +451,349 @@ pub fn next(pomodoro: &Pomodoro) -> Pomodoro { SessionType::LongBreak => 1, _ => pomodoro.current_work_round_number, }, - ..*pomodoro + config: pomodoro.config.clone(), } } -pub fn tick(pomodoro: &Pomodoro) -> Pomodoro { - let session = pomodoro.current_session.clone(); +pub fn tick_with_file_session_info( + pomodoro: &Pomodoro, + session_info: Option, +) -> Result { + let mut new_pomodoro = pomodoro.clone(); - match session.status { - // Tick should do something only if the current session is in running mode - SessionStatus::Running => { - // If it was the last tick, return the next status - if session.current_time + 1 == pomodoro.duration_of_session(&session) { - return next(pomodoro); + let pomodoro_result = match session_info { + // We have some session info from the disk + Some(info) => { + // @TODO: Here we need to check the consistency between the session_info that we have (coming from + // reading the session file on disk) and the state of the current pomodoro + // We need a way to check that the file has possibly been reset, deleted to adapt the next + // pomodoro state + + // If there is no session file let’s play the pomodoro, it means a new file has been + // created + if new_pomodoro.current_session.session_file.is_none() { + let new_pomodoro = play_with_session_file(pomodoro, Some(info.clone()))?; + + // // If the external file creation time is newer than the current start time, we should + // // reset the current pomodoro + // // @TODO: it looks like this logic should be handle elsewhere? + // if current_session_start_time < info.start_time { + // new_pomodoro.current_session.elapsed_seconds = 0; + // }; + Ok(new_pomodoro) + } else { + new_pomodoro.current_session.status = info.session_status; + new_pomodoro.current_session.session_type = info.session_type; + new_pomodoro.current_session.status = info.session_status; + new_pomodoro.current_session.elapsed_seconds = info.elapsed_seconds; + new_pomodoro.current_session.label = Some(info.label.clone()); + + let next_pomodoro = match new_pomodoro.current_session.status { + // Tick should do something if the current session is in running mode + SessionStatus::Running => { + // If it was the last tick, return the next status + if is_end_of_session(&new_pomodoro) { + get_next_pomodoro(&new_pomodoro) + } else { + // If we're not a the end of a session, just update the time of the current session + Pomodoro { + // The source of truth is always the file on the disk + current_session: Session { + elapsed_seconds: new_pomodoro.current_session.elapsed_seconds + + 1, + label: Some(info.label), + session_file: new_pomodoro.current_session.session_file.clone(), + session_type: new_pomodoro.current_session.session_type, + status: new_pomodoro.current_session.status, + }, + config: new_pomodoro.config.clone(), + ..*pomodoro + } + } + } + // If it’s not running we don’t do anything at tick + _ => { + Pomodoro { + // The source of truth is always the file on the disk + current_session: new_pomodoro.current_session, + config: pomodoro.config.clone(), + ..*pomodoro + } + } + }; + + Ok(next_pomodoro) } + } + // File is not present on disk, it may have been deleted + // so we should reset the current round + None => reset_round(pomodoro), + }?; + + write_session_file_from_pomodoro( + &pomodoro_result, + pomodoro_result.current_session.session_type, + pomodoro_result.current_session.status, + )?; + Ok(pomodoro_result) +} + +pub fn is_end_of_session(pomodoro: &Pomodoro) -> bool { + pomodoro.current_session.elapsed_seconds + 1 + >= pomodoro.duration_of_session(&pomodoro.current_session) +} + +pub fn tick(pomodoro: &Pomodoro) -> Result { + let current_session = pomodoro.current_session.clone(); + + if file_exists(pomodoro.config.session_file.as_path()) && current_session.session_file.is_none() + { + // File created externally, start the pomodoro + play_with_session_file(pomodoro, None) + } else { + let mut new_pomodoro = match current_session.status { + // Tick should do something if the current session is in running mode + SessionStatus::Running => { + // If it was the last tick, return the next status + if is_end_of_session(pomodoro) { + get_next_pomodoro(pomodoro) + } else { + // If we're not a the end of a session, just update the time of the current session + Pomodoro { + current_session: Session { + elapsed_seconds: current_session.elapsed_seconds + 1, + label: current_session.label, + session_file: pomodoro.current_session.session_file.clone(), + ..pomodoro.current_session + }, + config: pomodoro.config.clone(), + ..*pomodoro + } + } + } + _ => pomodoro.clone(), + }; + + if new_pomodoro.current_session.status == SessionStatus::NotStarted { + remove_session_file(&new_pomodoro)?; + new_pomodoro.current_session.session_file = None; + } + + Ok(new_pomodoro) + } +} + +// Session file format should be +// current session_type;label +// +// `session_type` can be any of "focus", "shortbreak", "longbreak" +// `;` is the separator +// `label` can be any type of string +// +// To start a new session with the label working and a time of 20 minutes, do the +// following: +// +// echo "focus;working" > ~/.cache/pomodorolm_session + +pub fn get_session_info(session_file_path: &PathBuf) -> Option { + if file_exists(session_file_path) { + let line: String = fs::read_to_string(session_file_path) + .context("Unable to read the session file") + .ok()?; + + let session_line_content = parse_line(line).ok()?; + + Some(SessionInfo { + elapsed_seconds: session_line_content.elapsed_seconds.unwrap_or(0), + label: session_line_content.label, + session_status: session_line_content.session_status, + session_type: session_line_content.session_type, + }) + } else { + Err(anyhow!( + "Unable to read session file {session_file_path:?}, file doesn’t exist" + )) + .ok() + } +} + +pub fn get_session_info_with_default( + session_file_path: &PathBuf, + pomodoro: &Pomodoro, +) -> SessionInfo { + let default = SessionInfo { + elapsed_seconds: 0, + label: pomodoro.config.default_focus_label.clone(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + if file_exists(session_file_path) { + let line: String = fs::read_to_string(session_file_path) + .context("Unable to read the session file") + .unwrap(); - // If we're not a the end of a session, just update the time of the current session - Pomodoro { - current_session: Session { - current_time: session.current_time + 1, - label: session.label, - ..pomodoro.current_session - }, - ..*pomodoro + let _modified = fs::metadata(session_file_path).unwrap().modified().unwrap(); + + let session_line_content = parse_line(line); + + // eprintln!("session line content: {:?}", session_line_content); + + match session_line_content { + Ok(line_content) => { + let elapsed_seconds = match line_content.elapsed_seconds { + Some(seconds) => seconds, + None => { + let remaining_seconds = get_remaining_time( + session_file_path, + pomodoro.config.focus_duration as u64, + ) + .unwrap_or(Duration::from_secs(pomodoro.config.focus_duration.into())) + .as_secs() as u16; + + eprintln!("# -> remaining seconds: {remaining_seconds:?}"); + + let to_return = if line_content.session_status == SessionStatus::Running { + // @FIXME: + pomodoro.config.focus_duration - remaining_seconds + } else { + pomodoro.current_session.elapsed_seconds + }; + + eprintln!("# -> remaining seconds to return: {to_return:?}"); + to_return + } + }; + + // eprintln!("# -> elapsed seconds: {elapsed_seconds:?}"); + + SessionInfo { + elapsed_seconds, + label: line_content.label, + session_status: line_content.session_status, + session_type: line_content.session_type, + } + } + Err(e) => { + eprintln!( + "Unable to parse session line: {e}. Fallback to config defaults: {}/{}.", + pomodoro.config.default_focus_label, pomodoro.config.focus_duration + ); + default } } - _ => pomodoro.clone(), + } else { + default + } +} + +fn file_exists(path: &Path) -> bool { + fs::metadata(path).is_ok() +} + +fn parse_line(line: String) -> Result { + let parts = line.trim().split(";").collect::>(); + + if parts.len() < 2 { + return Err(anyhow!( + "Unable to read session line, it should have at least 2 parts between a ;" + )); + } + + let session_type_string = parts[0]; + let label = parts[1]; + + let session_status_string = if parts.len() >= 3 { + parts[2] + } else { + "running" + }; + + let elapsed_seconds = if parts.len() >= 4 { + Some( + parts[3] + .parse::() + .context("Unable to read timestamp in session line")?, + ) + } else { + None + }; + + Ok(SessionLineContent { + elapsed_seconds, + label: label.to_owned(), + session_status: SessionStatus::from_str(session_status_string).context(format!( + "Unable to read session line, unknown session status: {session_status_string}" + ))?, + session_type: SessionType::from_str(session_type_string).context(format!( + "Unable to read session line, unknown session type: {session_type_string}" + ))?, + }) +} + +fn get_remaining_time(path: &Path, duration: u64) -> Result { + let modified_time = fs::metadata(path)?.modified()?; + + let now = SystemTime::now(); + let elapsed = now.duration_since(modified_time)?; + let total_duration = Duration::from_secs(duration); + + if elapsed >= total_duration { + eprintln!("# Remaining returning {:?}", Duration::from_secs(0)); + return Ok(Duration::from_secs(0)); // Return zero if the time is up + } + let remaining = total_duration - elapsed; + + eprintln!("# Remaining: {remaining:?}"); + Ok(remaining) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parsing_error() { + let result = parse_line("invalid line".to_owned()); + let error = result.unwrap_err(); + assert_eq!( + format!("{error}"), + "Unable to read session line, it should have at least 2 parts between a ;" + ); + + let result = parse_line("focus-;label".to_owned()); + let error = result.unwrap_err(); + assert_eq!( + format!("{error}"), + "Unable to read session line, unknown session type: focus-" + ); + } + + #[test] + fn parsing_ok_in_minutes() { + let result = parse_line("Focus;label".to_owned()).unwrap(); + assert_eq!(result.label, "label"); + assert_eq!(result.session_type, SessionType::Focus); + assert_eq!(result.session_status, SessionStatus::Running); + + let result = parse_line("ShortBreak;label".to_owned()).unwrap(); + assert_eq!(result.label, "label"); + assert_eq!(result.session_type, SessionType::ShortBreak); + + let result = parse_line("LongBreak;label".to_owned()).unwrap(); + assert_eq!(result.label, "label"); + assert_eq!(result.session_type, SessionType::LongBreak); + + let result = parse_line("Focus;label;paused".to_owned()).unwrap(); + assert_eq!(result.label, "label"); + assert_eq!(result.session_type, SessionType::Focus); + assert_eq!(result.session_status, SessionStatus::Paused); + + // Add timestamp of pause + let result = parse_line("Focus;label;paused;8".to_owned()).unwrap(); + assert_eq!(result.label, "label"); + assert_eq!(result.session_type, SessionType::Focus); + assert_eq!(result.session_status, SessionStatus::Paused); + assert_eq!(result.elapsed_seconds, Some(8)); } } diff --git a/src-tauri/tests/pomodoro_session_file_test.rs b/src-tauri/tests/pomodoro_session_file_test.rs new file mode 100644 index 0000000..3b71b9b --- /dev/null +++ b/src-tauri/tests/pomodoro_session_file_test.rs @@ -0,0 +1,132 @@ +use pomodorolm_lib::pomodoro::{ + self, Config, Pomodoro, Session, SessionInfo, SessionStatus, SessionType, +}; +use tempfile::NamedTempFile; + +fn get_config() -> Config { + Config { + auto_start_long_break_timer: false, + auto_start_short_break_timer: false, + auto_start_focus_timer: false, + default_focus_label: "Focus".to_owned(), + default_long_break_label: "Long Break".to_owned(), + default_short_break_label: "Short Break".to_owned(), + focus_duration: 25 * 60, + long_break_duration: 20 * 60, + max_focus_rounds: 4, + // Be sure that the tests can be run in // + // by using an unique session_file per test + session_file: NamedTempFile::new().unwrap().path().to_path_buf(), + short_break_duration: 5 * 60, + } +} +fn get_initial_state() -> Pomodoro { + Pomodoro { + config: get_config(), + current_session: Session::default(), + current_work_round_number: 1, + } +} + +fn get_running_state() -> Pomodoro { + let config = get_config(); + Pomodoro { + config: config.clone(), + current_session: Session { + // 5 minutes + elapsed_seconds: 300, + label: None, + session_file: Some(config.session_file), + session_type: SessionType::Focus, + status: SessionStatus::Running, + }, + current_work_round_number: 1, + } +} + +#[test] +fn tick_with_file_session_info_start_test() { + let pomodoro_state = get_initial_state(); + + let session_info = SessionInfo { + elapsed_seconds: 0, + label: "Test label".to_string(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + // Be sure we have no starting file + assert!(pomodoro_state.current_session.session_file.is_none()); + + let new_state = + pomodoro::tick_with_file_session_info(&pomodoro_state, Some(session_info)).unwrap(); + + // If we have a new file we should start the pomodoro + assert_eq!(new_state.current_session.status, SessionStatus::Running); + assert!(new_state.current_session.session_file.is_some()); + + // If we did remove the file, the pomodoro should be stopped + let new_state = pomodoro::tick_with_file_session_info(&pomodoro_state, None).unwrap(); + + assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); + assert!(new_state.current_session.session_file.is_none()); + + // Start it again + let session_info = SessionInfo { + elapsed_seconds: 0, + label: "Test label".to_string(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + let new_state = + pomodoro::tick_with_file_session_info(&pomodoro_state, Some(session_info)).unwrap(); + + assert_eq!(new_state.current_session.status, SessionStatus::Running); + assert!(new_state.current_session.session_file.is_some()); +} + +#[test] +fn tick_with_file_session_info_running_test() { + let pomodoro_state = get_running_state(); + + let session_info = SessionInfo { + // 5 minutes + elapsed_seconds: 300, + label: "Test label".to_string(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + let new_state = + pomodoro::tick_with_file_session_info(&pomodoro_state, Some(session_info)).unwrap(); + + // If we have a new file we should start the pomodoro + assert_eq!(new_state.current_session.status, SessionStatus::Running); + assert!(new_state.current_session.session_file.is_some()); + + assert_eq!(new_state.current_session.elapsed_seconds, 301); +} + +#[test] +fn tick_with_file_session_info_change_running_test() { + let pomodoro_state = get_running_state(); + + let session_info = SessionInfo { + // 5 minutes + elapsed_seconds: 200, + label: "Test".to_string(), + session_status: SessionStatus::Paused, + session_type: SessionType::Focus, + }; + + let new_state = + pomodoro::tick_with_file_session_info(&pomodoro_state, Some(session_info)).unwrap(); + + // If we have a new file we should start the pomodoro + assert_eq!(new_state.current_session.status, SessionStatus::Paused); + assert!(new_state.current_session.session_file.is_some()); + + assert_eq!(new_state.current_session.elapsed_seconds, 200); + assert_eq!(new_state.current_session.label, Some("Test".to_string())); +} diff --git a/src-tauri/tests/pomodoro_test.rs b/src-tauri/tests/pomodoro_test.rs index cd10dfc..378d5f8 100644 --- a/src-tauri/tests/pomodoro_test.rs +++ b/src-tauri/tests/pomodoro_test.rs @@ -1,8 +1,36 @@ -use pomodorolm_lib::pomodoro::{self, Config, Pomodoro, Session, SessionStatus, SessionType}; +use pomodorolm_lib::pomodoro::{ + self, Config, Pomodoro, Session, SessionInfo, SessionStatus, SessionType, + write_session_file_from_pomodoro, +}; +use std::fs; +use tempfile::NamedTempFile; + +fn get_initial_state() -> Pomodoro { + Pomodoro { + config: Config { + auto_start_long_break_timer: false, + auto_start_short_break_timer: false, + auto_start_focus_timer: false, + default_focus_label: "Focus".to_owned(), + default_long_break_label: "Long Break".to_owned(), + default_short_break_label: "Short Break".to_owned(), + focus_duration: 25 * 60, + long_break_duration: 20 * 60, + max_focus_rounds: 4, + // Be sure that the tests can be run in // + // by using an unique session_file per test + session_file: NamedTempFile::new().unwrap().path().to_path_buf(), + short_break_duration: 5 * 60, + }, + current_session: Session::default(), + current_work_round_number: 1, + } +} #[test] fn it_defaults_the_way_it_should() { - let pomodoro = Pomodoro::default(); + let pomodoro = get_initial_state(); + assert_eq!(pomodoro.current_work_round_number, 1); assert_eq!( pomodoro.current_session.session_type, @@ -12,47 +40,116 @@ fn it_defaults_the_way_it_should() { pomodoro.current_session.status, pomodoro::SessionStatus::NotStarted ); + + assert_eq!(pomodoro.current_session.session_file, None); } #[test] fn tick_should_not_do_anything_if_not_running() { - let initial_state = Pomodoro::default(); - let new_state = pomodoro::tick(&initial_state); + let initial_state = get_initial_state(); + let new_state = pomodoro::tick(&initial_state).unwrap(); - assert_eq!(initial_state, new_state); + assert_eq!(initial_state.clone(), new_state); +} - let new_state = pomodoro::pause(&Pomodoro::default()); +#[test] +fn tick_should_start_session_if_file_created() { + let initial_state = get_initial_state(); + let _ = write_session_file_from_pomodoro( + &initial_state, + initial_state.current_session.session_type, + SessionStatus::Running, + ); + let new_state = pomodoro::tick(&initial_state).unwrap(); + assert_eq!( + new_state, + Pomodoro { + current_session: Session { + // Session should be running + status: SessionStatus::Running, + label: initial_state.current_session.label.clone(), + // Session file should have been set + session_file: Some(initial_state.config.session_file.clone()), + ..initial_state.current_session + }, + config: initial_state.config.clone(), + ..initial_state + } + ); + + // Session file should have been created + assert!( + new_state + .current_session + .session_file + .as_ref() + .unwrap() + .exists() + ); +} + +#[test] +fn pause_should_change_session_status() { + let initial_state = get_initial_state(); + + // It should pause the session status + let new_state = pomodoro::pause(&initial_state); assert_eq!( - initial_state.current_session.current_time, - new_state.current_session.current_time + new_state, + Pomodoro { + current_session: Session { + status: SessionStatus::Paused, + label: initial_state.current_session.label.clone(), + session_file: initial_state.current_session.session_file.clone(), + ..initial_state.current_session + }, + config: initial_state.config.clone(), + ..initial_state + } ); } #[test] fn tick_should_tick_if_started() { - let initial_state = pomodoro::play(&Pomodoro::default()); - let new_state = pomodoro::tick(&initial_state); + let initial_state = pomodoro::play(&get_initial_state()).unwrap(); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!( - new_state.current_session.current_time, - initial_state.current_session.current_time + 1 + new_state.current_session.elapsed_seconds, + initial_state.current_session.elapsed_seconds + 1 ); } +#[test] +fn play_should_create_session_file() { + let play_state = pomodoro::play_with_session_file(&get_initial_state(), None).unwrap(); + + assert!(play_state.current_session.session_file.is_some()); + assert!(play_state.current_session.session_file.unwrap().exists()); +} + #[test] fn tick_should_return_next_session_at_end_of_turn() { - let mut initial_state = pomodoro::play(&Pomodoro::default()); - initial_state.current_session.current_time = initial_state.config.focus_duration - 1; + let mut initial_state = pomodoro::play(&get_initial_state()).unwrap(); + + initial_state.current_session.elapsed_seconds = initial_state.config.focus_duration - 1; + + // As the session is started, the session file should have been created + assert!(initial_state.current_session.session_file.is_some()); // At the end of a focus session, we should switch to a short break - let new_state = pomodoro::tick(&initial_state); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!( new_state.current_session.session_type, SessionType::ShortBreak ); - assert_eq!(new_state.current_session.current_time, 0); + assert_eq!(new_state.current_session.elapsed_seconds, 0); assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); + + // As the ShortBreak session is not started, the session file should have been removed + assert_eq!(new_state.current_session.session_file, None); + // A work round includes a Focus and a Break, so the counter should be incremented only // at the end of a break assert_eq!( @@ -62,26 +159,29 @@ fn tick_should_return_next_session_at_end_of_turn() { // At the end of a short break round, we should switch to a focus round and // increment the current_work_round_number counter - let mut initial_state = pomodoro::play(&new_state); - initial_state.current_session.current_time = initial_state.config.short_break_duration - 1; + let mut initial_state = pomodoro::play(&new_state).unwrap(); + initial_state.current_session.elapsed_seconds = initial_state.config.short_break_duration - 1; + assert!(initial_state.current_session.session_file.is_some()); - let mut new_state = pomodoro::tick(&initial_state); + let mut new_state = pomodoro::tick(&initial_state).unwrap(); - assert_eq!(new_state.current_session.current_time, 0); + assert_eq!(new_state.current_session.elapsed_seconds, 0); assert_eq!(new_state.current_session.session_type, SessionType::Focus); assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); assert_eq!( new_state.current_work_round_number, initial_state.current_work_round_number + 1 ); + assert!(new_state.current_session.session_file.is_none()); // We are at the end of the last focus session, we should switch to a long break new_state.current_work_round_number = new_state.config.max_focus_rounds; - new_state.current_session.current_time = new_state.config.focus_duration - 1; + new_state.current_session.elapsed_seconds = new_state.config.focus_duration - 1; + let pomodoro_play = &pomodoro::play(&new_state).unwrap(); - let mut new_state = pomodoro::tick(&pomodoro::play(&new_state)); + let mut new_state = pomodoro::tick(pomodoro_play).unwrap(); - assert_eq!(new_state.current_session.current_time, 0); + assert_eq!(new_state.current_session.elapsed_seconds, 0); assert_eq!( new_state.current_session.session_type, SessionType::LongBreak @@ -93,10 +193,10 @@ fn tick_should_return_next_session_at_end_of_turn() { ); // We are at the end of the long break, we should reset to a focus session - new_state.current_session.current_time = new_state.config.long_break_duration - 1; - let new_state = pomodoro::tick(&pomodoro::play(&new_state)); + new_state.current_session.elapsed_seconds = new_state.config.long_break_duration - 1; + let new_state = pomodoro::tick(&pomodoro::play(&new_state).unwrap()).unwrap(); - assert_eq!(new_state.current_session.current_time, 0); + assert_eq!(new_state.current_session.elapsed_seconds, 0); assert_eq!(new_state.current_session.session_type, SessionType::Focus); assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); assert_eq!(new_state.current_work_round_number, 1); @@ -104,36 +204,41 @@ fn tick_should_return_next_session_at_end_of_turn() { #[test] fn reset_should_stop_the_current_round() { - let initial_state = pomodoro::play(&Pomodoro::default()); - let new_state = pomodoro::tick(&initial_state); + let initial_state = pomodoro::play_with_session_file(&get_initial_state(), None).unwrap(); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!( - new_state.current_session.current_time, - initial_state.current_session.current_time + 1 + new_state.current_session.elapsed_seconds, + initial_state.current_session.elapsed_seconds + 1 ); - let new_state = pomodoro::reset_round(&new_state); + let session_file = new_state.current_session.session_file.clone().unwrap(); + + assert!(session_file.exists()); + let new_state = pomodoro::reset_round(&new_state).unwrap(); + + // Reset should delete the file on disk + assert!(!session_file.exists()); - assert_eq!(new_state.current_session.current_time, 0); + assert_eq!(new_state.current_session.session_file, None); + assert_eq!(new_state.current_session.elapsed_seconds, 0); assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); assert_eq!(new_state.current_session.session_type, SessionType::Focus); } #[test] fn auto_start_should_run_next_state() { - let pomodoro_with_auto_start_short_break = Pomodoro { - config: Config { - auto_start_short_break_timer: true, - ..Default::default() - }, - ..Default::default() - }; - let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_short_break); - initial_state.current_session.current_time = initial_state.config.focus_duration - 1; + let mut pomodoro_with_auto_start_short_break = get_initial_state(); + pomodoro_with_auto_start_short_break + .config + .auto_start_short_break_timer = true; + + let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_short_break).unwrap(); + initial_state.current_session.elapsed_seconds = initial_state.config.focus_duration - 1; // At the end of a focus session, we should switch to a short break // that should run automatically - let new_state = pomodoro::tick(&initial_state); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!( new_state.current_session.session_type, SessionType::ShortBreak @@ -149,12 +254,12 @@ fn auto_start_should_run_next_state() { ..Default::default() }; - let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_long_break); - initial_state.current_session.current_time = initial_state.config.focus_duration - 1; + let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_long_break).unwrap(); + initial_state.current_session.elapsed_seconds = initial_state.config.focus_duration - 1; // At the end of the 4th focus session, we should switch to a long break // that should run automatically - let new_state = pomodoro::tick(&initial_state); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!( new_state.current_session.session_type, @@ -174,13 +279,113 @@ fn auto_start_should_run_next_state() { ..Default::default() }; - let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_focus); - initial_state.current_session.current_time = initial_state.config.short_break_duration - 1; + let mut initial_state = pomodoro::play(&pomodoro_with_auto_start_focus).unwrap(); + initial_state.current_session.elapsed_seconds = initial_state.config.short_break_duration - 1; // At the end of a break, we should switch to a focus session // that should run automatically - let new_state = pomodoro::tick(&initial_state); + let new_state = pomodoro::tick(&initial_state).unwrap(); assert_eq!(new_state.current_session.session_type, SessionType::Focus); assert_eq!(new_state.current_session.status, SessionStatus::Running); } + +#[test] +fn tick_with_file_session_info_ended_test() { + let pomodoro_state = get_initial_state(); + + let mut session_info = SessionInfo { + elapsed_seconds: 0, + label: "Test label".to_string(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + let mut new_state = + pomodoro::tick_with_file_session_info(&pomodoro_state, Some(session_info.clone())).unwrap(); + + eprintln!("\n# 1 : {:?}", new_state); + + // End the pomodoro + for i in 1..=new_state.config.focus_duration - 1 { + // Simulate updating file elapsed seconds + session_info.elapsed_seconds = i; + new_state = + pomodoro::tick_with_file_session_info(&new_state, Some(session_info.clone())).unwrap(); + eprintln!("\n# 2 : {:?}", new_state); + } + + assert_eq!(new_state.current_session.elapsed_seconds, 0); + assert_eq!( + new_state.current_session.session_type, + SessionType::ShortBreak + ); + assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); + assert_eq!( + new_state.current_session.session_type, + SessionType::ShortBreak + ); + + eprintln!("\n##### -> Latest test"); + let session_info = SessionInfo { + elapsed_seconds: new_state.config.focus_duration, + label: "Test label".to_string(), + session_status: SessionStatus::Running, + session_type: SessionType::Focus, + }; + + let new_state = pomodoro::tick_with_file_session_info(&new_state, Some(session_info)).unwrap(); + + eprintln!("# 3 : {:?}", new_state); + + assert_eq!(new_state.current_session.status, SessionStatus::NotStarted); + assert_eq!( + new_state.current_session.session_type, + SessionType::ShortBreak + ); + assert_eq!(new_state.current_session.elapsed_seconds, 0); +} + +#[test] +fn get_session_info_with_default_test() { + let pomodoro = get_initial_state(); + let session_info = + pomodoro::get_session_info_with_default(&pomodoro.config.session_file, &pomodoro); + + assert_eq!(session_info.session_type, SessionType::Focus); + assert_eq!(session_info.label, pomodoro.config.default_focus_label); + // Default label + assert_eq!(session_info.label, "Focus"); + + let _ = write_session_file_from_pomodoro( + &pomodoro, + pomodoro.current_session.session_type, + SessionStatus::Running, + ); + + let session_info = + pomodoro::get_session_info_with_default(&pomodoro.config.session_file, &pomodoro); + + assert_eq!(session_info.session_type, SessionType::Focus); + assert_eq!(session_info.label, "working"); + + let _ = fs::write( + &pomodoro.config.session_file, + format!("{};{}", SessionType::ShortBreak, "test"), + ); + + let session_info = + pomodoro::get_session_info_with_default(&pomodoro.config.session_file, &pomodoro); + + assert_eq!(session_info.session_type, SessionType::ShortBreak); + assert_eq!(session_info.label, "test"); + + let _ = fs::write(&pomodoro.config.session_file, format!("{};{}", "-", "")); + let session_info = + pomodoro::get_session_info_with_default(&pomodoro.config.session_file, &pomodoro); + + assert_eq!(session_info.session_type, SessionType::Focus); + assert_eq!(session_info.label, pomodoro.config.default_focus_label); + // Default label + assert_eq!(session_info.label, "Focus"); +} diff --git a/src-ts/main.ts b/src-ts/main.ts index 3413fdd..b592dd8 100644 --- a/src-ts/main.ts +++ b/src-ts/main.ts @@ -278,13 +278,8 @@ app.ports.sendMessageFromElm.subscribe(async function (message: Message) { case "update_config": console.log("Should update config"); - console.log(message); - let config: ElmConfig = message.value as ElmConfig; - console.log(config); - // break; - invoke("update_config", { config: { always_on_top: config.alwaysOnTop, @@ -367,10 +362,6 @@ app.ports.setThemeColors.subscribe(function (themeColors: ThemeColors) { mainHtmlElement.style.setProperty("--color-accent", themeColors.accent); }); -await listen("tick-event", () => { - app.ports.tick.send(null); -}); - await listen("external-message", (message) => { app.ports.sendMessageToElm.send(message.payload); });