Add ability to load pomodoro settings from TOML file
This commit is contained in:
		
							
								
								
									
										93
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<PathBuf>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<Self> {
 | 
			
		||||
        let config_file = Self::get_config_directory().join(SETTINGS_FILE);
 | 
			
		||||
        Self::read_from_file(&config_file).ok()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn write_to_file<P: AsRef<Path>>(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<P: AsRef<Path>>(path: &P) -> Result<Self> {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										299
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										299
									
								
								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<P: AsRef<Path>>(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<P: AsRef<Path>>(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<P: AsRef<Path>>(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<Status>) {
 | 
			
		||||
            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<Status>, 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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								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<Status>, Receiver<Status>) = 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<PathBuf>;
 | 
			
		||||
 | 
			
		||||
        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<PathBuf> {
 | 
			
		||||
    let mut result: Option<PathBuf> = 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),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										271
									
								
								src/pomodoro.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								src/pomodoro.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<P: AsRef<Path>>(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<P: AsRef<Path>>(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<Status>,
 | 
			
		||||
        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<Status>, 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user