diff --git a/Cargo.lock b/Cargo.lock index 344a89f..da79f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "arc-swap" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62" + [[package]] name = "arrayref" version = "0.3.6" @@ -185,6 +191,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86c952727a495bda7abaf09bafdee1a939194dd793d9a8e26281df55ac43b00" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -280,6 +295,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a880035bfe4707e344da9acf50cc94d003fe337f50afd94c8722c1bb4e0a933" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio", + "parking_lot", + "signal-hook", + "winapi 0.3.8", +] + +[[package]] +name = "crossterm_winapi" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "directories" version = "2.0.2" @@ -308,6 +348,7 @@ version = "0.1.0" dependencies = [ "async-std", "clap", + "crossterm", "directories", "rodio", ] @@ -445,6 +486,15 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.8" @@ -600,6 +650,30 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -718,6 +792,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" +[[package]] +name = "signal-hook" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6" +dependencies = [ + "libc", + "mio", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +dependencies = [ + "arc-swap", + "libc", +] + [[package]] name = "slab" version = "0.4.2" @@ -735,6 +830,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "smallvec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" + [[package]] name = "stdweb" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 9ef32a0..49dcd13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ rodio = "0.11.0" clap = "2.33.1" directories = "2.0.2" async-std = "1.5.0" +crossterm = "0.17.4" diff --git a/src/lib.rs b/src/lib.rs index c612b04..4c85b14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ pub mod pomodoro { 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> { @@ -21,10 +23,6 @@ pub mod pomodoro { } } - pub fn load<'a, 'b, P: AsRef>(&'a mut self, _path: &'b P) { - unimplemented!() - } - pub fn play(&self) { let file = File::open(self.path).unwrap(); let source = Decoder::new(BufReader::new(file)).unwrap(); @@ -33,7 +31,7 @@ pub mod pomodoro { } } - #[derive(PartialEq, Eq)] + #[derive(Copy, Clone, Debug)] pub enum State { Work, ShortBreak, @@ -41,32 +39,58 @@ pub mod pomodoro { Inactive, } - pub struct Config { - pub work_time: u64, - pub short_break: u64, - pub long_break: u64, - pub data_directory: PathBuf, - } - - impl Default for Config { - fn default() -> Self { - let dirs = ProjectDirs::from("moe", "paoda", "domasi").unwrap(); - - let data_directory = dirs.data_dir().to_owned(); - - if !data_directory.exists() { - fs::create_dir_all(&data_directory).unwrap(); - } - - Config { - work_time: 1, - short_break: 1, - long_break: 1, - data_directory, + 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_owned(); + self + } + + fn get_data_directory() -> PathBuf { + let dirs = ProjectDirs::from("moe", "paoda", "domasi").unwrap(); + dirs.data_dir().to_owned() + } + } + + impl Default for Config { + fn default() -> Self { + Config::new(20, 5, 10) + } + } + + #[derive(Copy, Clone)] pub struct Pomodoro<'a> { count: u64, state: State, @@ -99,30 +123,26 @@ pub mod pomodoro { } } - async fn wait(minutes: u64) { - use std::time::Duration; - - let duration = Duration::from_secs(minutes * 5); + async fn wait(duration: Duration) { async_std::task::sleep(duration).await; } - pub async fn start(&mut self, config: Config) { + pub async fn start(&mut self, config: Config, tx: Sender) { loop { self.next(); - self.alert.play(); match self.state { State::Work => { - println!("Start Work."); + Self::send_to_clock(&tx, self.state, config.work_time); Self::wait(config.work_time).await; } State::ShortBreak => { - println!("Start Short Break."); + Self::send_to_clock(&tx, self.state, config.short_break); Self::wait(config.short_break).await; } State::LongBreak => { - println!("Start Long Break."); + Self::send_to_clock(&tx, self.state, config.long_break); Self::wait(config.long_break).await; } State::Inactive => { @@ -133,8 +153,73 @@ pub mod pomodoro { } } - pub fn completed(&self) -> u64 { - self.count / 4 + fn send_to_clock(tx: &Sender, state: State, length: Duration) { + let status = Status { + start: Instant::now(), + length, + state, + }; + + tx.send(status).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() * 60) - 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) } } } diff --git a/src/main.rs b/src/main.rs index 8d85529..7117c63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,12 @@ use async_std::task; use clap::{App, ArgMatches, SubCommand}; -use domasi::pomodoro::{Alert, Config}; +use crossterm::{cursor, terminal::Clear, terminal::ClearType, QueueableCommand}; +use domasi::pomodoro::{Alert, Clock, Config, Status}; use domasi::Pomodoro; +use std::io::{stdout, Write}; + +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; fn main() { let matches = App::new("Domasi") @@ -11,9 +16,13 @@ fn main() { .subcommand(SubCommand::with_name("start").about("Start the Pomodoro Timer")) .get_matches(); - match matches.subcommand() { - ("start", Some(sub_matches)) => start(sub_matches), - _ => {} + // match matches.subcommand() { + // ("start", Some(sub_matches)) => start(sub_matches), + // _ => {} + // } + + if let ("start", Some(sub_matches)) = matches.subcommand() { + start(sub_matches); } } @@ -26,6 +35,34 @@ pub fn start(_args: &ArgMatches) { let mut pomodoro = Pomodoro::new(&alert); task::block_on(async { - pomodoro.start(config).await; + let (tx, rx): (Sender, Receiver) = channel(); + + thread::spawn(move || loop { + if let Ok(status) = rx.recv() { + loop { + let (remaining, string) = Clock::get_formatted_string(status); + print_overwrite(&string); + + // Super fun race condition that you gotta handle better :) + if remaining < 1 { + break; + } + + thread::sleep(Clock::get_polling_interval()); + } + } + }); + + pomodoro.start(config, tx).await; }); } + +fn print_overwrite(text: &str) { + let mut stdout = stdout(); + + stdout.queue(Clear(ClearType::CurrentLine)).unwrap(); + stdout.queue(cursor::SavePosition).unwrap(); + stdout.write_all(text.as_bytes()).unwrap(); + stdout.queue(cursor::RestorePosition).unwrap(); + stdout.flush().unwrap(); +}