diff --git a/Cargo.lock b/Cargo.lock index 58484cf..91fb412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] name = "async-std" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93c583a035d21e6d6f09adf48abfc55277bf48886406df370e5db6babe3ab98" +checksum = "00d68a33ebc8b57800847d00787307f84a562224a14db069b0acefe4c2abbf5d" dependencies = [ "async-task", "crossbeam-utils", @@ -192,9 +192,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "0.1.10" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" [[package]] name = "chrono" @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "dbus" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b17a12ffaff26515889b006fc029493a3e340366a137c13cec2cdd545ea3b8" +checksum = "5cd9e78c210146a1860f897db03412fd5091fd73100778e43ee255cca252cf32" dependencies = [ "libc", "libdbus-sys", @@ -353,9 +353,9 @@ dependencies = [ [[package]] name = "directories" -version = "2.0.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +checksum = "ba927e81b69134a17de62e7be40364be6844f731238ed166f53aac36344ecc19" dependencies = [ "cfg-if", "dirs-sys", @@ -394,7 +394,7 @@ dependencies = [ "directories", "notify-rust", "rodio", - "serde_derive", + "serde", "toml", "winrt-notification", ] @@ -853,7 +853,7 @@ checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7" dependencies = [ "proc-macro2", "quote 1.0.7", - "syn 1.0.31", + "syn 1.0.32", ] [[package]] @@ -868,18 +868,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01608bfa680dafb103f9207fa944facf572e4e3e708d10de19a0d0c3d36e5f18" -dependencies = [ - "crossbeam-utils", - "futures-io", - "futures-sink", - "futures-util", -] - [[package]] name = "pkg-config" version = "0.3.17" @@ -988,19 +976,22 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.112" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243" +checksum = "6135c78461981c79497158ef777264c51d9d0f4f3fc3a4d22b915900e42dac6a" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.112" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57" +checksum = "93c5eaa17d0954cb481cdcfffe9d84fcfa7a1a9f2349271e678677be4c26ae31" dependencies = [ "proc-macro2", "quote 1.0.7", - "syn 1.0.31", + "syn 1.0.32", ] [[package]] @@ -1055,9 +1046,9 @@ checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" [[package]] name = "smol" -version = "0.1.14" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f50d21240d7045d848746d6244762b8cb96449b586b4200519719784859ef50" +checksum = "620cbb3c6e34da57d3a248cda0cd01cd5848164dc062e764e65d06fe3ea7aed5" dependencies = [ "async-task", "blocking", @@ -1067,11 +1058,11 @@ dependencies = [ "futures-util", "libc", "once_cell", - "piper", "scoped-tls", "slab", "socket2", - "wepoll-binding", + "wepoll-sys-stjepang", + "winapi 0.3.8", ] [[package]] @@ -1127,9 +1118,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6" +checksum = "a994520748611c17d163e81b6c4a4b13d11b7f63884362ab2efac3aa9cf16d00" dependencies = [ "proc-macro2", "quote 1.0.7", @@ -1171,7 +1162,7 @@ checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" dependencies = [ "proc-macro2", "quote 1.0.7", - "syn 1.0.31", + "syn 1.0.32", ] [[package]] @@ -1262,7 +1253,7 @@ dependencies = [ "log", "proc-macro2", "quote 1.0.7", - "syn 1.0.31", + "syn 1.0.32", "wasm-bindgen-shared", ] @@ -1296,7 +1287,7 @@ checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92" dependencies = [ "proc-macro2", "quote 1.0.7", - "syn 1.0.31", + "syn 1.0.32", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1318,20 +1309,10 @@ dependencies = [ ] [[package]] -name = "wepoll-binding" -version = "2.0.2" +name = "wepoll-sys-stjepang" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374fff4ff9701ff8b6ad0d14bacd3156c44063632d8c136186ff5967d48999a7" -dependencies = [ - "bitflags 1.2.1", - "wepoll-sys", -] - -[[package]] -name = "wepoll-sys" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9082a777aed991f6769e2b654aa0cb29f1c3d615daf009829b07b66c7aff6a24" +checksum = "74e0414ddefc9d668689fa1540beede94710b2b978bb531335051a0172a4f496" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index fa6d535..235868f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,12 @@ edition = "2018" [dependencies] rodio = "0.11.0" clap = "2.33.1" -directories = "2.0.2" -async-std = "1.6.0" +directories = "3.0.0" +async-std = "1.6.2" crossterm = "0.17.5" anyhow = "1.0.31" toml = "0.5.6" -serde_derive = "1.0.112" +serde = { version = "1.0.113", features = ["derive"] } [target.'cfg(windows)'.dependencies] # winrt = { git = "https://github.com/microsoft/winrt-rs" } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..fd73da3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,93 @@ +use anyhow::Result; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +const SETTINGS_FILE: &str = "settings.toml"; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Config { + pub work_time: Duration, + pub short_break: Duration, + pub long_break: Duration, + pub sound_file: Option, +} + +impl Config { + pub fn new(work: u64, short_break: u64, long_break: u64) -> Self { + Self::init_directories(); + + Config { + work_time: Duration::from_secs(work), + short_break: Duration::from_secs(short_break), + long_break: Duration::from_secs(long_break), + sound_file: None, + } + } + + fn init_directories() { + let config_dir = Self::get_config_directory(); + let data_dir = Self::get_data_directory(); + + if !config_dir.exists() { + let _ = fs::create_dir_all(config_dir); + } + + if !data_dir.exists() { + let _ = fs::create_dir_all(data_dir); + } + } + + pub fn save(config: &Config) -> Result<()> { + let config_directory = Self::get_config_directory(); + + if !config_directory.exists() { + fs::create_dir_all(&config_directory)?; + } + + Self::write_to_file(&config_directory.join(SETTINGS_FILE), config) + } + + pub fn load() -> Option { + let config_file = Self::get_config_directory().join(SETTINGS_FILE); + Self::read_from_file(&config_file).ok() + } + + fn write_to_file>(path: &P, cfg: &Self) -> Result<()> { + let mut new_file = File::create(path.as_ref())?; + let serialized = toml::to_string(cfg)?; + + Ok(new_file.write_all(serialized.as_bytes())?) + } + + fn read_from_file>(path: &P) -> Result { + let mut toml_buf = vec![]; + let mut file = File::open(path.as_ref())?; + file.read_to_end(&mut toml_buf)?; + + Ok(toml::from_slice(&toml_buf)?) + } + + pub fn get_data_directory() -> PathBuf { + ProjectDirs::from("moe", "paoda", "domasi") + .unwrap() + .config_dir() + .to_path_buf() + } + + fn get_config_directory() -> PathBuf { + ProjectDirs::from("moe", "paoda", "domasi") + .unwrap() + .data_dir() + .to_path_buf() + } +} + +impl Default for Config { + fn default() -> Self { + Config::new(1500, 300, 600) + } +} diff --git a/src/lib.rs b/src/lib.rs index b4995c3..25c1346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,298 +1,5 @@ +pub use config::Config; pub use pomodoro::Pomodoro; -pub mod pomodoro { - use directories::ProjectDirs; - use rodio::{Decoder, Device, Source}; - use std::fs::{self, File}; - use std::io::BufReader; - use std::path::{Path, PathBuf}; - use std::sync::mpsc::Sender; - use std::time::{Duration, Instant}; - - #[derive(Copy, Clone)] - pub struct Alert<'a> { - path: &'a Path, - device: &'a Device, - } - - impl<'a> Alert<'a> { - pub fn new>(path: &'a P, device: &'a Device) -> Alert<'a> { - Alert { - path: path.as_ref(), - device, - } - } - - pub fn play(&self) { - if self.path.exists() { - let file = File::open(self.path).unwrap(); - let source = Decoder::new(BufReader::new(file)).unwrap(); - rodio::play_raw(self.device, source.convert_samples()); - } - } - - pub fn load>(mut self, new_path: &'a P) -> Self { - self.path = new_path.as_ref(); - self - } - } - #[derive(Copy, Clone, Debug)] - pub enum State { - Work, - ShortBreak, - LongBreak, - Inactive, - } - - impl std::fmt::Display for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - State::Work => f.write_str("Working: "), - State::ShortBreak => f.write_str("Resting: "), - State::LongBreak => f.write_str("REALLY Resting: "), - State::Inactive => f.write_str("Inactive: "), - } - } - } - - pub struct Config { - pub work_time: Duration, - pub short_break: Duration, - pub long_break: Duration, - pub data_directory: PathBuf, - } - - impl Config { - pub fn new(work: u64, short_break: u64, long_break: u64) -> Config { - let data_directory = Self::get_data_directory(); - if !data_directory.exists() { - fs::create_dir_all(&data_directory).unwrap(); - } - let seconds_in_minutes = 60; - - Config { - work_time: Duration::from_secs(work * seconds_in_minutes), - short_break: Duration::from_secs(short_break * seconds_in_minutes), - long_break: Duration::from_secs(long_break * seconds_in_minutes), - data_directory, - } - } - - pub fn with_data_directory>(mut self, path: &P) -> Config { - self.data_directory = path.as_ref().to_path_buf(); - self - } - - fn get_data_directory() -> PathBuf { - let dirs = ProjectDirs::from("moe", "paoda", "Domasi").unwrap(); - dirs.data_dir().to_path_buf() - } - } - - impl Default for Config { - fn default() -> Self { - Config::new(20, 5, 10) - } - } - - #[derive(Copy, Clone)] - pub struct Pomodoro<'a> { - count: u64, - state: State, - alert: Alert<'a>, - } - - impl Pomodoro<'_> { - pub fn new<'a>(alert: &'a Alert) -> Pomodoro<'a> { - Pomodoro { - count: 0, - state: State::Inactive, - alert: *alert, - } - } - - fn next(&mut self) { - match self.state { - State::Work => { - self.count += 1; - - if (self.count % 4) == 0 { - self.state = State::LongBreak; - } else { - self.state = State::ShortBreak; - } - } - State::LongBreak => self.state = State::Inactive, - State::ShortBreak => self.state = State::Work, - State::Inactive => self.state = State::Work, - } - } - - async fn wait(duration: Duration) { - async_std::task::sleep(duration).await; - } - - pub async fn start(&mut self, config: Config, tx: Sender) { - loop { - self.next(); - self.alert.play(); - - Self::notify(&self.state); - - match self.state { - State::Work => { - Self::send_to_clock(&tx, self.state, config.work_time); - Self::wait(config.work_time).await; - } - State::ShortBreak => { - Self::send_to_clock(&tx, self.state, config.short_break); - Self::wait(config.short_break).await; - } - State::LongBreak => { - Self::send_to_clock(&tx, self.state, config.long_break); - Self::wait(config.long_break).await; - } - State::Inactive => { - println!("Pomodoro Cycle is complete"); - break; - } - } - } - } - - fn send_to_clock(tx: &Sender, state: State, length: Duration) { - let status = Status { - start: Instant::now(), - length, - state, - }; - - tx.send(status).unwrap(); - } - - #[cfg(any(target_os = "macos", target_os = "linux"))] - fn notify(state: &State) { - let mut toast = notify_rust::Notification::new(); - - match state { - State::Work => { - toast - .summary("Time to Work!") - .body("Remember to stay focused!") - .show() - .unwrap(); - } - State::ShortBreak | State::LongBreak => { - toast - .summary("Break Time!") - .body("Enjoy your well deserved rest!") - .show() - .unwrap(); - } - State::Inactive => { - toast - .summary("Pomodoro Cycle Complete.") - .body("Now waiting for user input....") - .show() - .unwrap(); - } - } - } - - #[cfg(target_os = "windows")] - fn notify(state: &State) { - use winrt_notification::{Duration, Sound, Toast}; - - let toast = Toast::new(Toast::POWERSHELL_APP_ID); - - match state { - State::Work => { - toast - .title("Time to Work!") - .text1("Remember to stay focused!") - .sound(Some(Sound::Default)) - .duration(Duration::Short) - .show() - .unwrap(); - } - State::ShortBreak | State::LongBreak => { - toast - .title("Break Time!") - .text1("Enjoy your well deserved rest!") - .sound(Some(Sound::Default)) - .duration(Duration::Short) - .show() - .unwrap(); - } - State::Inactive => { - toast - .title("Pomodoro Cycle Complete.") - .text1("Now waiting for user input....") - .sound(Some(Sound::Default)) - .duration(Duration::Short) - .show() - .unwrap(); - } - } - } - } - - #[derive(Copy, Clone, Debug)] - pub struct Status { - pub start: Instant, - pub length: Duration, - pub state: State, - } - - pub struct Clock; - - impl Clock { - pub fn get_formatted_string(status: Status) -> (u64, String) { - let now = Instant::now(); - let elapsed = now.checked_duration_since(status.start); - - match elapsed { - Some(duration) => { - let remaining = status.length.as_secs() - duration.as_secs(); - - let seconds = remaining; - let hours = seconds / 3600; - let minutes = (seconds - (hours * 3600)) / 60; - let seconds = seconds - (hours * 3600) - (minutes * 60); - - let mut clock = String::new(); - - clock.push_str(&format!("{}", status.state)); - - if hours > 0 { - // We don't want o bother with the hours part if there is none - if hours < 10 { - clock.push_str(&format!("0{}:", hours)); - } else { - clock.push_str(&format!("{}:", hours)); - } - } - - if minutes < 10 { - clock.push_str(&format!("0{}:", minutes)); - } else { - clock.push_str(&format!("{}:", minutes)); - } - - if seconds < 10 { - clock.push_str(&format!("0{}", seconds)); - } else { - clock.push_str(&format!("{}", seconds)); - } - - (remaining, clock) - } - None => (0, "??:??:??".to_string()), // This will break the loop - } - } - - pub fn get_polling_interval() -> Duration { - Duration::from_millis(500) - } - } -} +pub mod config; +pub mod pomodoro; diff --git a/src/main.rs b/src/main.rs index c949309..a562c2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,11 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, QueueableCommand, }; -use domasi::pomodoro::{Alert, Clock, Config, Status}; -use domasi::Pomodoro; +use domasi::pomodoro::{Alert, Clock, Status}; +use domasi::{Config, Pomodoro}; +use std::fs; use std::io::{stdout, Write}; use std::path::{Path, PathBuf}; - use std::sync::mpsc::{channel, Receiver, Sender}; use std::thread; @@ -44,18 +44,7 @@ fn main() { } pub fn start(args: &ArgMatches) { - let config = Config::default(); - - let audio_path: PathBuf; - if let Some(path) = args.value_of("alert") { - audio_path = Path::new(path).to_path_buf(); - } else { - audio_path = config.data_directory.join("sound/alert.ogg"); - } - - let default_device = rodio::default_output_device().unwrap(); - let alert = Alert::new(&audio_path, &default_device); - let mut pomodoro = Pomodoro::new(&alert); + let mut pomodoro = Pomodoro::new(); let (tx, rx): (Sender, Receiver) = channel(); @@ -63,11 +52,11 @@ pub fn start(args: &ArgMatches) { thread::spawn(move || loop { if let Ok(status) = rx.recv() { loop { - let (remaining, string) = Clock::get_formatted_string(status); - print_overwrite(&string); + let (remain, text) = Clock::get_formatted_string(status); + print_overwrite(&text); - // Super fun race condition that you gotta handle better :) - if remaining < 1 { + // Make this check better pls + if remain < 1 { break; } @@ -77,15 +66,87 @@ pub fn start(args: &ArgMatches) { }); // User Input Thread - thread::spawn(|| { - setup_user_input().unwrap(); - }); + thread::spawn(|| setup_user_input().unwrap()); + // Async Pomodoro task::block_on(async { - pomodoro.start(config, tx).await; + let config = { + match Config::load() { + Some(cfg) => cfg, + None => Default::default(), + } + }; + + let maybe_audio: Option; + + match args.value_of("alert") { + Some(path) => maybe_audio = Some(Path::new(path).to_path_buf()), + None => { + match &config.sound_file { + Some(path) => maybe_audio = Some(path.clone()), + None => { + // Look in the default locations + // check for .mp3, .wav, .ogg, .flac + let data_dir = Config::get_data_directory(); + let data_dir_str = data_dir.to_string_lossy(); + + let items = fs::read_dir(&data_dir).unwrap_or_else(|_err| { + panic!("Unable to read the contents of {}", data_dir_str); + }); + + maybe_audio = get_audio_file(items); + } + } + } + } + + match maybe_audio { + Some(audio_path) => { + let default_device = rodio::default_output_device().unwrap(); + let alert = Alert::new(&audio_path, &default_device); + + pomodoro.start(config, tx, Some(&alert)).await; + } + None => pomodoro.start(config, tx, None).await, + } }); } +fn get_audio_file(items: std::fs::ReadDir) -> Option { + let mut result: Option = None; + + for maybe_entry in items { + match maybe_entry { + Ok(entry) => match entry.path().extension() { + Some(ext) => match ext.to_str() { + Some("mp3") => { + result = Some(entry.path()); + break; + } + Some("wav") => { + result = Some(entry.path()); + break; + } + Some("ogg") => { + result = Some(entry.path()); + break; + } + Some("flac") => { + result = Some(entry.path()); + break; + } + Some(_ext) => continue, + None => continue, + }, + None => continue, + }, + Err(_err) => continue, + } + } + + result +} + fn setup_user_input() -> crossterm::Result<()> { enable_raw_mode()?; @@ -118,7 +179,7 @@ fn handle_key_event(event: KeyEvent) { match event.code { KeyCode::Char('c') => { if let KeyModifiers::CONTROL = event.modifiers { - exit_domasi(1) + exit_domasi(0) } } KeyCode::Esc | KeyCode::Char('q') => exit_domasi(0), diff --git a/src/pomodoro.rs b/src/pomodoro.rs new file mode 100644 index 0000000..b05e93b --- /dev/null +++ b/src/pomodoro.rs @@ -0,0 +1,271 @@ +use super::Config; +use rodio::{Decoder, Device, Source}; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use std::sync::mpsc::Sender; +use std::time::{Duration, Instant}; + +// #[derive()] +pub struct Alert<'a> { + path: &'a Path, + device: &'a Device, +} + +impl<'a> Alert<'a> { + pub fn new>(path: &'a P, device: &'a Device) -> Alert<'a> { + Alert { + path: path.as_ref(), + device, + } + } + + pub fn play(&self) { + if self.path.exists() { + let file = File::open(self.path).unwrap(); + let source = Decoder::new(BufReader::new(file)).unwrap(); + rodio::play_raw(&self.device, source.convert_samples()); + } + } + + pub fn load>(mut self, new_path: &'a P) -> Self { + self.path = new_path.as_ref(); + self + } +} +#[derive(Copy, Clone, Debug)] +pub enum State { + Work, + ShortBreak, + LongBreak, + Inactive, +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + State::Work => f.write_str("Working: "), + State::ShortBreak => f.write_str("Resting: "), + State::LongBreak => f.write_str("REALLY Resting: "), + State::Inactive => f.write_str("Inactive: "), + } + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Pomodoro { + count: u64, + state: State, +} + +impl Default for Pomodoro { + fn default() -> Self { + Pomodoro { + count: 0, + state: State::Inactive, + } + } +} + +impl<'a> Pomodoro { + pub async fn start( + &mut self, + config: Config, + tx: Sender, + maybe_alert: Option<&Alert<'a>>, + ) { + loop { + self.next(); + + if let Some(alert) = maybe_alert { + alert.play(); + } + + Self::notify(&self.state); + + match self.state { + State::Work => { + Self::send_to_clock(&tx, self.state, config.work_time); + Self::wait(config.work_time).await; + } + State::ShortBreak => { + Self::send_to_clock(&tx, self.state, config.short_break); + Self::wait(config.short_break).await; + } + State::LongBreak => { + Self::send_to_clock(&tx, self.state, config.long_break); + Self::wait(config.long_break).await; + } + State::Inactive => { + println!("Pomodoro Cycle is complete"); + break; + } + } + } + } +} + +impl Pomodoro { + pub fn new() -> Pomodoro { + Pomodoro { + count: 0, + state: State::Inactive, + } + } + + fn next(&mut self) { + match self.state { + State::Work => { + self.count += 1; + + if (self.count % 4) == 0 { + self.state = State::LongBreak; + } else { + self.state = State::ShortBreak; + } + } + State::LongBreak => self.state = State::Inactive, + State::ShortBreak => self.state = State::Work, + State::Inactive => self.state = State::Work, + } + } + + async fn wait(duration: Duration) { + async_std::task::sleep(duration).await; + } + + fn send_to_clock(tx: &Sender, state: State, length: Duration) { + let status = Status { + start: Instant::now(), + length, + state, + }; + + tx.send(status).unwrap(); + } + + #[cfg(any(target_os = "macos", target_os = "linux"))] + fn notify(state: &State) { + let mut toast = notify_rust::Notification::new(); + + match state { + State::Work => { + toast + .summary("Time to Work!") + .body("Remember to stay focused!") + .show() + .unwrap(); + } + State::ShortBreak | State::LongBreak => { + toast + .summary("Break Time!") + .body("Enjoy your well deserved rest!") + .show() + .unwrap(); + } + State::Inactive => { + toast + .summary("Pomodoro Cycle Complete.") + .body("Now waiting for user input....") + .show() + .unwrap(); + } + } + } + + #[cfg(target_os = "windows")] + fn notify(state: &State) { + use winrt_notification::{Duration, Sound, Toast}; + + let toast = Toast::new(Toast::POWERSHELL_APP_ID); + + match state { + State::Work => { + toast + .title("Time to Work!") + .text1("Remember to stay focused!") + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .unwrap(); + } + State::ShortBreak | State::LongBreak => { + toast + .title("Break Time!") + .text1("Enjoy your well deserved rest!") + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .unwrap(); + } + State::Inactive => { + toast + .title("Pomodoro Cycle Complete.") + .text1("Now waiting for user input....") + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .unwrap(); + } + } + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Status { + pub start: Instant, + pub length: Duration, + pub state: State, +} + +pub struct Clock; + +impl Clock { + pub fn get_formatted_string(status: Status) -> (u64, String) { + let now = Instant::now(); + let elapsed = now.checked_duration_since(status.start); + + match elapsed { + Some(duration) => { + let remaining = status.length.as_secs() - duration.as_secs(); + + let seconds = remaining; + let hours = seconds / 3600; + let minutes = (seconds - (hours * 3600)) / 60; + let seconds = seconds - (hours * 3600) - (minutes * 60); + + let mut clock = String::new(); + + clock.push_str(&format!("{}", status.state)); + + if hours > 0 { + // We don't want o bother with the hours part if there is none + if hours < 10 { + clock.push_str(&format!("0{}:", hours)); + } else { + clock.push_str(&format!("{}:", hours)); + } + } + + if minutes < 10 { + clock.push_str(&format!("0{}:", minutes)); + } else { + clock.push_str(&format!("{}:", minutes)); + } + + if seconds < 10 { + clock.push_str(&format!("0{}", seconds)); + } else { + clock.push_str(&format!("{}", seconds)); + } + + (remaining, clock) + } + None => (0, "??:??:??".to_string()), // This will break the loop + } + } + + pub fn get_polling_interval() -> Duration { + Duration::from_millis(500) + } +}