Rewrite Domasi

This commit is contained in:
Rekai Nyangadzayi Musuka 2020-08-30 20:22:27 -05:00
parent 68ec6883b7
commit 6954e3dfd8
6 changed files with 179 additions and 1545 deletions

1078
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,3 @@ version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
clap = "2.33"
crossterm = "0.17"
directories = "3.0"
notify-rust = "4.0"
rodio = "0.11"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
crossbeam = "0.7"

View File

@ -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)
}
}

View File

@ -1,5 +1,3 @@
pub use config::Config;
pub use pomodoro::Pomodoro;
pub mod config;
pub mod pomodoro;
mod pomodoro;

View File

@ -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();
}

View File

@ -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: "),
}
}
}