Add ability to load pomodoro settings from TOML file
This commit is contained in:
parent
ad79175056
commit
f79fde6d16
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue