Rewrite Domasi
This commit is contained in:
		@@ -1,73 +0,0 @@
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use directories::ProjectDirs;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::fs::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 {
 | 
			
		||||
        Config {
 | 
			
		||||
            work_time: Duration::from_secs(work),
 | 
			
		||||
            short_break: Duration::from_secs(short_break),
 | 
			
		||||
            long_break: Duration::from_secs(long_break),
 | 
			
		||||
            sound_file: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(config: &Config) -> Result<()> {
 | 
			
		||||
        let config_directory = Self::get_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()
 | 
			
		||||
            .data_dir()
 | 
			
		||||
            .to_path_buf()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_config_directory() -> PathBuf {
 | 
			
		||||
        ProjectDirs::from("moe", "paoda", "domasi")
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .config_dir()
 | 
			
		||||
            .to_path_buf()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Config {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Config::new(1500, 300, 600)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
pub use config::Config;
 | 
			
		||||
pub use pomodoro::Pomodoro;
 | 
			
		||||
 | 
			
		||||
pub mod config;
 | 
			
		||||
pub mod pomodoro;
 | 
			
		||||
mod pomodoro;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										203
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										203
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,204 +1,7 @@
 | 
			
		||||
use clap::{App, Arg, ArgMatches, SubCommand};
 | 
			
		||||
use crossbeam::channel::{unbounded, Receiver, Sender};
 | 
			
		||||
use crossterm::{
 | 
			
		||||
    event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers},
 | 
			
		||||
    terminal::{disable_raw_mode, enable_raw_mode},
 | 
			
		||||
};
 | 
			
		||||
use domasi::pomodoro::{Alert, Remaining, Status};
 | 
			
		||||
use domasi::{Config, Pomodoro};
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io::{self, Write};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::thread;
 | 
			
		||||
use domasi::Pomodoro;
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let matches = App::new("domasi")
 | 
			
		||||
        .version("0.1.0")
 | 
			
		||||
        .author("Rekai Musuka <rekai@musuka.dev>")
 | 
			
		||||
        .about("Yet another pomodoro timer.")
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("create-config")
 | 
			
		||||
                .short("C")
 | 
			
		||||
                .long("create-config")
 | 
			
		||||
                .help("Creates a Settings.toml and an alert directory"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("start")
 | 
			
		||||
                .about("Start the Pomodoro Timer")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("alert")
 | 
			
		||||
                        .short("a")
 | 
			
		||||
                        .long("alert")
 | 
			
		||||
                        .value_name("FILE")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .help("Aloud Sound. (Supports WAV, MP3, Vorbis, FLAC)"),
 | 
			
		||||
                ),
 | 
			
		||||
        )
 | 
			
		||||
        .get_matches();
 | 
			
		||||
    let mut timer: Pomodoro = Default::default();
 | 
			
		||||
 | 
			
		||||
    // match matches.subcommand() {
 | 
			
		||||
    //     ("start", Some(sub_matches)) => start(sub_matches),
 | 
			
		||||
    //     _ => {}
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    if matches.is_present("create-config") {
 | 
			
		||||
        create_config()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let ("start", Some(sub_matches)) = matches.subcommand() {
 | 
			
		||||
        start(sub_matches);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_config() {
 | 
			
		||||
    let config_dir = Config::get_config_directory();
 | 
			
		||||
    let data_dir = Config::get_data_directory().join("alert");
 | 
			
		||||
 | 
			
		||||
    if !config_dir.exists() {
 | 
			
		||||
        fs::create_dir_all(&config_dir).unwrap_or_else(|err| {
 | 
			
		||||
            panic!("Failed to create {}: {}", config_dir.to_string_lossy(), err)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !data_dir.exists() {
 | 
			
		||||
        fs::create_dir_all(&data_dir).unwrap_or_else(|err| {
 | 
			
		||||
            panic!("Failed to create {}: {}", data_dir.to_string_lossy(), err)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let config: Config = Default::default();
 | 
			
		||||
 | 
			
		||||
    Config::save(&config).unwrap_or_else(|err| {
 | 
			
		||||
        let cfg_path = config_dir.to_string_lossy();
 | 
			
		||||
        panic!("Error while writing settings.toml to {}: {}", cfg_path, err);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let data_path = data_dir.to_string_lossy();
 | 
			
		||||
    let settings_path = config_dir.join("settings.toml");
 | 
			
		||||
    let settings_path = settings_path.to_string_lossy();
 | 
			
		||||
 | 
			
		||||
    println!(
 | 
			
		||||
        "Successfully created \"{}\" and \"{}\"",
 | 
			
		||||
        settings_path, data_path
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn start(args: &ArgMatches) {
 | 
			
		||||
    let mut pomodoro = Pomodoro::new();
 | 
			
		||||
    let (tx, rx): (Sender<Status>, Receiver<Status>) = unbounded();
 | 
			
		||||
 | 
			
		||||
    // UI Thread
 | 
			
		||||
    thread::spawn(move || loop {
 | 
			
		||||
        if let Ok(status) = rx.recv() {
 | 
			
		||||
            loop {
 | 
			
		||||
                let (text, remain) = match Remaining::from_status(status) {
 | 
			
		||||
                    Some(seconds) => (seconds.to_string(), seconds.remaining_secs()),
 | 
			
		||||
                    None => ("??? Status: ??:??".to_string(), 0),
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                let out = io::stdout();
 | 
			
		||||
                let mut handle = out.lock();
 | 
			
		||||
                handle.write_all(text.as_bytes()).unwrap();
 | 
			
		||||
                handle.flush().unwrap();
 | 
			
		||||
 | 
			
		||||
                // TODO: Make sure this isn't an issue.
 | 
			
		||||
                if remain < 1 {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                thread::sleep(Remaining::polling_interval());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // User Input Thread
 | 
			
		||||
    thread::spawn(|| setup_user_input().unwrap());
 | 
			
		||||
 | 
			
		||||
    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().join("alert");
 | 
			
		||||
                    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));
 | 
			
		||||
        }
 | 
			
		||||
        None => pomodoro.start(config, tx, None),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_audio_file(items: std::fs::ReadDir) -> Option<PathBuf> {
 | 
			
		||||
    for maybe_entry in items {
 | 
			
		||||
        if let Ok(entry) = maybe_entry {
 | 
			
		||||
            if let Some(ext) = entry.path().extension() {
 | 
			
		||||
                if let Some("mp3") | Some("wav") | Some("ogg") | Some("flac") = ext.to_str() {
 | 
			
		||||
                    return Some(entry.path());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn setup_user_input() -> crossterm::Result<()> {
 | 
			
		||||
    enable_raw_mode()?;
 | 
			
		||||
    get_user_input()?;
 | 
			
		||||
    disable_raw_mode()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_user_input() -> crossterm::Result<()> {
 | 
			
		||||
    loop {
 | 
			
		||||
        if poll(Remaining::polling_interval())? {
 | 
			
		||||
            if let Event::Key(event) = read()? {
 | 
			
		||||
                handle_key_event(event);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn handle_key_event(event: KeyEvent) {
 | 
			
		||||
    match event.code {
 | 
			
		||||
        KeyCode::Char('c') => {
 | 
			
		||||
            if let KeyModifiers::CONTROL = event.modifiers {
 | 
			
		||||
                exit_domasi(0)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        KeyCode::Esc | KeyCode::Char('q') => exit_domasi(0),
 | 
			
		||||
        _ => {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn exit_domasi(code: i32) {
 | 
			
		||||
    disable_raw_mode().unwrap();
 | 
			
		||||
 | 
			
		||||
    std::process::exit(code);
 | 
			
		||||
    timer.start();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										357
									
								
								src/pomodoro.rs
									
									
									
									
									
								
							
							
						
						
									
										357
									
								
								src/pomodoro.rs
									
									
									
									
									
								
							@@ -1,220 +1,166 @@
 | 
			
		||||
use super::Config;
 | 
			
		||||
use crossbeam::channel::Sender;
 | 
			
		||||
use rodio::{Decoder, Device, Source};
 | 
			
		||||
use std::fmt::{Display, Formatter, Result as FmtResult};
 | 
			
		||||
use std::fs::File;
 | 
			
		||||
use std::io::BufReader;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use std::io::{self, Write};
 | 
			
		||||
use std::thread;
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
 | 
			
		||||
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)]
 | 
			
		||||
#[derive(Debug, Copy, Clone, Default)]
 | 
			
		||||
pub struct Pomodoro {
 | 
			
		||||
    count: u64,
 | 
			
		||||
    state: State,
 | 
			
		||||
    count: u64,
 | 
			
		||||
    wait_start: Option<Instant>,
 | 
			
		||||
    paused: PausedState,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Pomodoro {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Pomodoro {
 | 
			
		||||
            count: 0,
 | 
			
		||||
            state: State::Inactive,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Pomodoro {
 | 
			
		||||
    pub 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();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let _ = Self::notify(&self.state);
 | 
			
		||||
 | 
			
		||||
            match self.state {
 | 
			
		||||
                State::Work => {
 | 
			
		||||
                    Self::send_to_clock(&tx, self.state, config.work_time);
 | 
			
		||||
                    Self::wait(config.work_time);
 | 
			
		||||
                }
 | 
			
		||||
                State::ShortBreak => {
 | 
			
		||||
                    Self::send_to_clock(&tx, self.state, config.short_break);
 | 
			
		||||
                    Self::wait(config.short_break);
 | 
			
		||||
                }
 | 
			
		||||
                State::LongBreak => {
 | 
			
		||||
                    Self::send_to_clock(&tx, self.state, config.long_break);
 | 
			
		||||
                    Self::wait(config.long_break);
 | 
			
		||||
                }
 | 
			
		||||
                State::Inactive => {
 | 
			
		||||
                    println!("Pomodoro Cycle is complete");
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
static POLLING_RATE: Duration = Duration::from_millis(300);
 | 
			
		||||
static WORK_TIME: u64 = 2;
 | 
			
		||||
static SBREAK_TIME: u64 = 2;
 | 
			
		||||
static LBREAK_TIME: u64 = 2;
 | 
			
		||||
 | 
			
		||||
impl Pomodoro {
 | 
			
		||||
    pub fn new() -> Pomodoro {
 | 
			
		||||
        Pomodoro {
 | 
			
		||||
            count: 0,
 | 
			
		||||
            state: State::Inactive,
 | 
			
		||||
        }
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self::default()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
                }
 | 
			
		||||
    pub fn start(&mut self) {
 | 
			
		||||
        loop {
 | 
			
		||||
            if let Status::Complete = self.poll() {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            State::LongBreak => self.state = State::Inactive,
 | 
			
		||||
            State::ShortBreak => self.state = State::Work,
 | 
			
		||||
            State::Inactive => self.state = State::Work,
 | 
			
		||||
            thread::sleep(POLLING_RATE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn wait(duration: Duration) {
 | 
			
		||||
        std::thread::sleep(duration);
 | 
			
		||||
    }
 | 
			
		||||
    fn poll(&mut self) -> Status {
 | 
			
		||||
        let status = self.check();
 | 
			
		||||
 | 
			
		||||
    fn send_to_clock(tx: &Sender<Status>, state: State, length: Duration) {
 | 
			
		||||
        let status = Status {
 | 
			
		||||
            start: Instant::now(),
 | 
			
		||||
            length,
 | 
			
		||||
            state,
 | 
			
		||||
        match status {
 | 
			
		||||
            Status::Paused => {
 | 
			
		||||
                assert!(self.paused.value);
 | 
			
		||||
                let now = Instant::now();
 | 
			
		||||
 | 
			
		||||
                self.paused.duration += match self.paused.previous {
 | 
			
		||||
                    Some(earlier) => now - earlier,
 | 
			
		||||
                    None => Default::default(),
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                self.paused.previous = Some(now);
 | 
			
		||||
            }
 | 
			
		||||
            Status::Active => self.display_time(),
 | 
			
		||||
            Status::NextState => {
 | 
			
		||||
                let (update_count, new_state) = self.next();
 | 
			
		||||
 | 
			
		||||
                if let Count::Increase = update_count {
 | 
			
		||||
                    self.count += 1;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.state = new_state;
 | 
			
		||||
                self.wait_start = match new_state {
 | 
			
		||||
                    State::Inactive => None,
 | 
			
		||||
                    State::Work | State::ShortBreak | State::LongBreak => Some(Instant::now()),
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                self.display_time();
 | 
			
		||||
            }
 | 
			
		||||
            Status::Complete => {
 | 
			
		||||
                println!("\rPomodoro Cycle Complete!");
 | 
			
		||||
            }
 | 
			
		||||
            Status::Inactive => {}
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        tx.send(status).unwrap();
 | 
			
		||||
        status
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn notify(state: &State) -> Result<(), anyhow::Error> {
 | 
			
		||||
        let mut toast = notify_rust::Notification::new();
 | 
			
		||||
        match state {
 | 
			
		||||
            State::Work => toast
 | 
			
		||||
                .summary("Time to Work!")
 | 
			
		||||
                .body("Remember to stay focused!")
 | 
			
		||||
                .show()?,
 | 
			
		||||
            State::ShortBreak | State::LongBreak => toast
 | 
			
		||||
                .summary("Break Time!")
 | 
			
		||||
                .body("Enjoy your well deserved rest!")
 | 
			
		||||
                .show()?,
 | 
			
		||||
            State::Inactive => toast
 | 
			
		||||
                .summary("Pomodoro Cycle Complete.")
 | 
			
		||||
                .body("Now waiting for user input....")
 | 
			
		||||
                .show()?,
 | 
			
		||||
        };
 | 
			
		||||
    fn display_time(&self) {
 | 
			
		||||
        if let Some(earlier) = self.wait_start {
 | 
			
		||||
            let wait = Instant::now() - earlier;
 | 
			
		||||
            let left: Clock = (Self::wait_times(self.state) - wait).into();
 | 
			
		||||
            let _ = Self::print(&format!("\r{} {}", self.state, left));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn print(text: &str) -> io::Result<()> {
 | 
			
		||||
        let out = io::stdout();
 | 
			
		||||
        let mut handle = out.lock();
 | 
			
		||||
        handle.write_all(text.as_bytes())?;
 | 
			
		||||
        handle.flush()?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Copy, Clone, Debug)]
 | 
			
		||||
pub struct Status {
 | 
			
		||||
    pub start: Instant,
 | 
			
		||||
    pub length: Duration,
 | 
			
		||||
    pub state: State,
 | 
			
		||||
}
 | 
			
		||||
    fn next(&self) -> (Count, State) {
 | 
			
		||||
        match self.state {
 | 
			
		||||
            State::Work => {
 | 
			
		||||
                let state: State = if (self.count + 1) % 4 == 0 {
 | 
			
		||||
                    State::LongBreak
 | 
			
		||||
                } else {
 | 
			
		||||
                    State::ShortBreak
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
pub struct Remaining {
 | 
			
		||||
    state: State,
 | 
			
		||||
    remaining: u64,
 | 
			
		||||
    seconds: u64,
 | 
			
		||||
    hours: u64,
 | 
			
		||||
    minutes: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Remaining {
 | 
			
		||||
    pub fn from_status(status: Status) -> Option<Self> {
 | 
			
		||||
        let now = Instant::now();
 | 
			
		||||
        let maybe_elapsed = now.checked_duration_since(status.start);
 | 
			
		||||
 | 
			
		||||
        match maybe_elapsed {
 | 
			
		||||
            Some(duration) => {
 | 
			
		||||
                let remaining = status.length.as_secs() - duration.as_secs();
 | 
			
		||||
 | 
			
		||||
                let hours = remaining / 3600;
 | 
			
		||||
                let minutes = (remaining - (hours * 3600)) / 60;
 | 
			
		||||
                let seconds = remaining - (hours * 3600) - (minutes * 60);
 | 
			
		||||
 | 
			
		||||
                Some(Self {
 | 
			
		||||
                    state: status.state,
 | 
			
		||||
                    remaining,
 | 
			
		||||
                    hours,
 | 
			
		||||
                    minutes,
 | 
			
		||||
                    seconds,
 | 
			
		||||
                })
 | 
			
		||||
                (Count::Increase, state)
 | 
			
		||||
            }
 | 
			
		||||
            None => None,
 | 
			
		||||
            State::ShortBreak | State::Inactive => (Count::Remain, State::Work),
 | 
			
		||||
            State::LongBreak => (Count::Remain, State::Inactive),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn remaining_secs(&self) -> u64 {
 | 
			
		||||
        self.remaining
 | 
			
		||||
    fn check(&self) -> Status {
 | 
			
		||||
        if self.paused.value {
 | 
			
		||||
            return Status::Paused;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match self.wait_start {
 | 
			
		||||
            Some(earlier) => {
 | 
			
		||||
                let diff: Duration = (Instant::now() - self.paused.duration) - earlier;
 | 
			
		||||
 | 
			
		||||
                if diff > Self::wait_times(self.state) {
 | 
			
		||||
                    Status::NextState
 | 
			
		||||
                } else {
 | 
			
		||||
                    Status::Active
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => {
 | 
			
		||||
                if self.count == 0 {
 | 
			
		||||
                    Status::NextState
 | 
			
		||||
                } else {
 | 
			
		||||
                    Status::Complete
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn polling_interval() -> Duration {
 | 
			
		||||
        Duration::from_millis(500)
 | 
			
		||||
    fn wait_times(state: State) -> Duration {
 | 
			
		||||
        match state {
 | 
			
		||||
            State::Work => Duration::from_secs(WORK_TIME),
 | 
			
		||||
            State::ShortBreak => Duration::from_secs(SBREAK_TIME),
 | 
			
		||||
            State::LongBreak => Duration::from_secs(LBREAK_TIME),
 | 
			
		||||
            _ => unreachable!("Can not have Pomodoro state = State::Inactive and wait_start = Some(...) at the same time.")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for Remaining {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
 | 
			
		||||
        f.write_str("\r")?;
 | 
			
		||||
        write!(f, "{}", self.state)?;
 | 
			
		||||
struct Clock {
 | 
			
		||||
    hours: u64,
 | 
			
		||||
    minutes: u64,
 | 
			
		||||
    seconds: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Duration> for Clock {
 | 
			
		||||
    fn from(dur: Duration) -> Self {
 | 
			
		||||
        let dur = dur.as_secs();
 | 
			
		||||
 | 
			
		||||
        let hours = dur / 3600;
 | 
			
		||||
        let minutes = (dur - (hours * 3600)) / 60;
 | 
			
		||||
        let seconds = dur - (hours * 3600) - (minutes * 60);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            hours,
 | 
			
		||||
            minutes,
 | 
			
		||||
            seconds,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for Clock {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
 | 
			
		||||
        if self.hours > 0 {
 | 
			
		||||
            // No need to print the hours if there are none.
 | 
			
		||||
            if self.hours < 10 {
 | 
			
		||||
@@ -237,3 +183,50 @@ impl Display for Remaining {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone, Default)]
 | 
			
		||||
struct PausedState {
 | 
			
		||||
    value: bool,
 | 
			
		||||
    duration: Duration,
 | 
			
		||||
    previous: Option<Instant>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone)]
 | 
			
		||||
enum Count {
 | 
			
		||||
    Increase,
 | 
			
		||||
    Remain,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone)]
 | 
			
		||||
enum Status {
 | 
			
		||||
    Paused,
 | 
			
		||||
    Active,
 | 
			
		||||
    NextState,
 | 
			
		||||
    Inactive,
 | 
			
		||||
    Complete,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone)]
 | 
			
		||||
enum State {
 | 
			
		||||
    Inactive,
 | 
			
		||||
    Work,
 | 
			
		||||
    ShortBreak,
 | 
			
		||||
    LongBreak,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for State {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        State::Inactive
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for State {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
 | 
			
		||||
        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: "),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user