From 5c6405d7fb3ac4b7a2307ea270981cc02079bdc0 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sat, 15 Oct 2022 18:12:56 -0700 Subject: [PATCH] try to clean it up a bit --- src/cli.rs | 33 +++++ src/lib.rs | 2 + src/main.rs | 59 +-------- src/timer.rs | 336 ++++++++++++++++++++++++++------------------------- src/util.rs | 54 +++++++++ 5 files changed, 260 insertions(+), 224 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/util.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..532c72c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,33 @@ +use clap::Parser; + +#[derive(Debug, Parser)] +#[clap(author, version, about)] +pub struct Cli { + /// Hours to count down. + #[clap(long, default_value_t = 0)] + pub hours: u64, + + /// Minutes to count down. + #[clap(long, short, default_value_t = 0)] + pub minutes: u64, + + /// Seconds to count down. + #[clap(long, short, default_value_t = 0)] + pub seconds: u64, + + /// Audio file to play at the end of the countdown. + #[clap(long, short)] + pub alarm: Option, + + /// Begin countdown immediately. + #[clap(long = "immediate", long = "running", short = 'i', short = 'r')] + pub running: bool, + + /// Count up from zero, actually. + #[clap(long, short = 'u')] + pub count_up: bool, + + /// Use the Predator font. + #[clap(long, short)] + pub predator: bool, +} diff --git a/src/lib.rs b/src/lib.rs index 1227065..cb4f116 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,3 @@ +pub mod cli; pub mod timer; +mod util; diff --git a/src/main.rs b/src/main.rs index fd19e8d..9a3eb55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,5 @@ -use std::time::Duration; - -use clap::Parser; use egui::Vec2; -use katabastird::timer::{CountDirection, Timer}; - -#[derive(Debug, Parser)] -#[clap(author, version, about)] -struct Cli { - /// Hours to count down. - #[clap(long, default_value_t = 0)] - hours: u64, - - /// Minutes to count down. - #[clap(long, short, default_value_t = 0)] - minutes: u64, - - /// Seconds to count down. - #[clap(long, short, default_value_t = 0)] - seconds: u64, - - /// Audio file to play at the end of the countdown. - #[clap(long, short)] - alarm: Option, - - /// Begin countdown immediately. - #[clap(long = "immediate", long = "running", short = 'i', short = 'r')] - running: bool, - - /// Count up from zero, actually. - #[clap(long, short = 'u')] - count_up: bool, - - /// Use the Predator font. - #[clap(long, short)] - predator: bool, -} +use katabastird::timer::Timer; fn main() { let options = eframe::NativeOptions { @@ -43,29 +8,9 @@ fn main() { ..Default::default() }; - let cli = Cli::parse(); - - let seconds = cli.hours * 3600 + cli.minutes * 60 + cli.seconds; - let duration = Duration::from_secs(seconds); - - let direction = if cli.count_up { - CountDirection::Up - } else { - CountDirection::Down - }; - eframe::run_native( "katabastird", options, - Box::new(move |cc| { - Box::new(Timer::new( - duration, - direction, - cc, - !cli.running, - cli.predator, - cli.alarm, - )) - }), + Box::new(move |cc| Box::new(Timer::new(cc))), ); } diff --git a/src/timer.rs b/src/timer.rs index 4bf263c..f10ac81 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -2,6 +2,9 @@ use egui::{Color32, Direction, FontId, Layout, RichText, Ui}; use egui_extras::{Size, StripBuilder}; use std::time::{Duration, Instant}; +use crate::{cli::Cli, util::*}; +use clap::Parser; + use eframe::{App, CreationContext}; #[derive(Debug, Clone, Copy)] @@ -14,79 +17,73 @@ pub enum CountDirection { pub struct Timer { direction: CountDirection, duration: Duration, - state: State, + state: TimerState, tstart: Instant, alarm: Option, } #[derive(Debug, Clone, Copy)] -enum State { +enum TimerState { Unstarted, Paused(ChronoState), // time remaining Running(ChronoState), // time remaining + Finished, } -impl PartialEq for State { +impl PartialEq for TimerState { fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Paused(_), Self::Paused(_)) => true, - (Self::Running(_), Self::Running(_)) => true, - (Self::Unstarted, Self::Unstarted) => true, - _ => false, - } + matches!( + (self, other), + (Self::Paused(_), Self::Paused(_)) + | (Self::Running(_), Self::Running(_)) + | (Self::Unstarted, Self::Unstarted) + | (Self::Finished, Self::Finished) + ) } } -impl Eq for State {} +impl Eq for TimerState {} #[derive(Debug, Clone, Copy)] struct ChronoState { - started: Instant, + updated: Instant, remaining: Duration, } -impl Default for Timer { - fn default() -> Self { - let dur = Duration::from_secs(30); - Self { - direction: CountDirection::Down, - duration: dur, - state: State::Unstarted, - tstart: Instant::now(), - alarm: None, - } - } -} - impl App for Timer { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.request_repaint_after(Duration::from_secs(1)); egui::CentralPanel::default().show(ctx, |ui| { let size = ctx.used_size(); - let vsize = if size[1].is_normal() { - size[1].abs() + let vsize = if size.x.is_normal() { + size.x.abs() } else { - 600.0 + 400.0 }; match self.state { - State::Unstarted => self.unstarted(ui, vsize), - _ => self.running(ui, vsize), + TimerState::Unstarted => self.unstarted(ui, vsize), + TimerState::Running(_) => self.running(ui, vsize), + TimerState::Paused(_) => self.paused(ui, vsize), + TimerState::Finished => self.finished(ui, vsize), } }); } } impl Timer { - pub fn new( - duration: Duration, - direction: CountDirection, - ctx: &CreationContext, - paused: bool, - predator: bool, - alarm: Option, - ) -> Self { - let tstart = Instant::now(); + pub fn new(ctx: &CreationContext) -> Self { + let cli = Cli::parse(); + let predator = cli.predator; + let seconds = cli.hours * 3600 + cli.minutes * 60 + cli.seconds; + let duration = Duration::from_secs(seconds); + + let direction = if cli.count_up { + CountDirection::Up + } else { + CountDirection::Down + }; + if predator { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( @@ -101,27 +98,22 @@ impl Timer { ctx.egui_ctx.set_fonts(fonts); } ctx.egui_ctx.request_repaint_after(Duration::from_secs(1)); - if paused { - Timer { - duration, - direction, - state: State::Unstarted, - tstart, - alarm, - } - } else { + let mut timer = Timer { + duration, + direction, + state: TimerState::Unstarted, + tstart: Instant::now(), + alarm: cli.alarm, + }; + if cli.running { let cs = ChronoState { remaining: duration, - started: Instant::now(), + updated: Instant::now(), }; - Timer { - duration, - direction, - state: State::Running(cs), - tstart, - alarm, - } + + timer.state = TimerState::Running(cs); } + timer } fn unstarted(&mut self, ui: &mut Ui, size: f32) { @@ -137,8 +129,8 @@ impl Timer { strip.cell(|ui| { if ui.button(start).clicked() { let dur = self.duration; - self.state = State::Running(ChronoState { - started: Instant::now(), + self.state = TimerState::Running(ChronoState { + updated: Instant::now(), remaining: dur, }); } @@ -153,111 +145,39 @@ impl Timer { .color(Color32::GOLD); let elapsed; - let started; - let mut is_paused; - if let State::Running(rs) = self.state { - elapsed = Instant::now() - rs.started; - started = rs.started; - is_paused = false; - } else if let State::Paused(cs) = self.state { - elapsed = cs.remaining; - started = Instant::now() - (self.duration - elapsed); - is_paused = true; + let remaining; + let mut is_paused = false; + if let TimerState::Running(rs) = self.state { + elapsed = Instant::now() - rs.updated; + remaining = rs.remaining; } else { unreachable!() } - let remaining = if is_paused { - elapsed - } else { - self.duration.saturating_sub(elapsed) - }; + let remaining = remaining.saturating_sub(elapsed); StripBuilder::new(ui) .size(Size::relative(0.3333)) .size(Size::remainder()) .cell_layout(Layout::centered_and_justified(Direction::LeftToRight)) .vertical(|mut strip| { - if is_paused { - strip.strip(|pstrip| { - pstrip - .sizes(Size::remainder(), 2) - .cell_layout(Layout::centered_and_justified(Direction::TopDown)) - .horizontal(|mut pstrip| { - pstrip.cell(|ui| { - let resume = RichText::new("RESUME") - .color(Color32::GREEN) - .font(FontId::monospace(tsize)); - if ui.button(resume).clicked() { - is_paused = false; - } - }); - pstrip.cell(|ui| { - let reset = RichText::new("RESET") - .color(Color32::RED) - .font(FontId::monospace(tsize)); - if ui.button(reset).clicked() { - self.state = State::Unstarted; - is_paused = true; - } - }); - }); - }); - } else { - // first the pause - strip.cell(|ui| { - if ui.button(text).clicked() { - is_paused = true; - } - }); - } + // first the pause + strip.cell(|ui| { + if ui.button(text).clicked() { + is_paused = true; + } + }); // if we're counting up, do the right thing let remaining = match self.direction { CountDirection::Down => remaining, CountDirection::Up => self.duration - remaining, }; - // now the numbers - let color = if is_paused { - let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0; - if blink { - Color32::BLACK - } else { - Color32::DARK_GRAY - } - } else { - Color32::DARK_GRAY - }; - let hours = remaining.as_secs() / 3600; - let minutes = (remaining.as_secs() / 60) % 60; - let seconds = remaining.as_secs() % 60; + let color = Color32::DARK_GRAY; let tsize = size * 0.7; - let hours = RichText::new(format!("{:02}", hours)) - .font(FontId::monospace(tsize)) - .color(color); - let minutes = RichText::new(format!("{:02}", minutes)) - .font(FontId::monospace(tsize)) - .color(color); - let seconds = RichText::new(format!("{:02}", seconds)) - .font(FontId::monospace(tsize)) - .color(color); - strip.strip(|strip| { - strip - .sizes(Size::relative(0.33), 3) - .cell_layout(Layout::centered_and_justified(Direction::TopDown)) - .horizontal(|mut strip| { - strip.cell(|ui| { - ui.label(hours); - }); - strip.cell(|ui| { - ui.label(minutes); - }); - strip.cell(|ui| { - ui.label(seconds); - }); - }); - }); + + display_digits(&mut strip, remaining, color, tsize); }); if remaining.is_zero() { @@ -265,36 +185,118 @@ impl Timer { let alarm_file = alarm_file.to_owned(); std::thread::spawn(move || alarm(alarm_file)); } - self.state = State::Unstarted; + self.state = TimerState::Finished; return; } - let cs = ChronoState { remaining, started }; + let cs = ChronoState { + remaining, + updated: Instant::now(), + }; if is_paused { - if self.state == State::Unstarted { + // did we click the reset button? + if self.state == TimerState::Unstarted { return; } - self.state = State::Paused(cs); + self.state = TimerState::Paused(cs); } else { - self.state = State::Running(cs); + self.state = TimerState::Running(cs); } } -} - -fn alarm(path: String) { - use rodio::{source::Source, Decoder, OutputStream}; - use std::fs::File; - use std::io::BufReader; - - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let file = BufReader::new(File::open(path).unwrap()); - let source = Decoder::new(file).unwrap(); - let dur = if let Some(dur) = source.total_duration() { - dur - } else { - Duration::from_secs(10) - }; - - let _ = stream_handle.play_raw(source.convert_samples()); - std::thread::sleep(dur); + + fn paused(&mut self, ui: &mut Ui, vsize: f32) { + let elapsed; + let mut is_running = false; + let tsize = vsize * 0.3; + if let TimerState::Paused(cs) = self.state { + elapsed = cs.remaining; + } else { + unreachable!() + } + + let remaining = elapsed; + StripBuilder::new(ui) + .size(Size::relative(0.33333)) + .size(Size::remainder()) + .cell_layout(Layout::centered_and_justified(Direction::LeftToRight)) + .vertical(|mut strip| { + strip.strip(|pstrip| { + pstrip + .sizes(Size::remainder(), 2) + .cell_layout(Layout::centered_and_justified(Direction::TopDown)) + .horizontal(|mut pstrip| { + pstrip.cell(|ui| { + let resume = RichText::new("RESUME") + .color(Color32::GREEN) + .font(FontId::monospace(tsize)); + if ui.button(resume).clicked() { + is_running = true; + } + }); + pstrip.cell(|ui| { + let reset = RichText::new("RESET") + .color(Color32::RED) + .font(FontId::monospace(tsize)); + if ui.button(reset).clicked() { + self.state = TimerState::Unstarted; + } + }); + }); + }); + let color = { + let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0; + if blink { + Color32::BLACK + } else { + Color32::DARK_GRAY + } + }; + display_digits(&mut strip, remaining, color, vsize * 0.7); + }); + + if is_running { + let cs = ChronoState { + remaining, + updated: Instant::now(), + }; + self.state = TimerState::Running(cs); + } + } + + fn finished(&mut self, ui: &mut Ui, vsize: f32) { + let remaining = match self.direction { + CountDirection::Up => self.duration, + CountDirection::Down => Duration::from_nanos(0), + }; + StripBuilder::new(ui) + .size(Size::relative(0.33333)) + .size(Size::remainder()) + .cell_layout(Layout::centered_and_justified(Direction::LeftToRight)) + .vertical(|mut strip| { + strip.strip(|pstrip| { + pstrip + .size(Size::remainder()) + .cell_layout(Layout::centered_and_justified(Direction::TopDown)) + .horizontal(|mut pstrip| { + pstrip.cell(|ui| { + let reset = RichText::new("RESET") + .color(Color32::GOLD) + .font(FontId::monospace(vsize * 0.3)); + if ui.button(reset).clicked() { + self.state = TimerState::Unstarted; + } + }); + }); + }); + let color = { + let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0; + if blink { + Color32::BLACK + } else { + Color32::DARK_GRAY + } + }; + display_digits(&mut strip, remaining, color, vsize * 0.7); + }); + } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..e5d0e22 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,54 @@ +use std::time::Duration; + +use egui::{Color32, Direction, FontId, Layout, RichText}; +use egui_extras::{Size, Strip}; + +pub fn format_digits(digits: u64, size: f32, color: Color32) -> RichText { + RichText::new(format!("{:02}", digits)) + .font(FontId::monospace(size)) + .color(color) +} + +pub fn alarm(path: String) { + use rodio::{source::Source, Decoder, OutputStream}; + use std::fs::File; + use std::io::BufReader; + + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let file = BufReader::new(File::open(path).unwrap()); + let source = Decoder::new(file).unwrap(); + let dur = if let Some(dur) = source.total_duration() { + dur + } else { + Duration::from_secs(10) + }; + + let _ = stream_handle.play_raw(source.convert_samples()); + std::thread::sleep(dur); +} + +pub(crate) fn display_digits(strip: &mut Strip, dur: Duration, color: Color32, size: f32) { + let hours = dur.as_secs() / 3600; + let minutes = (dur.as_secs() / 60) % 60; + let seconds = dur.as_secs() % 60; + let hours = format_digits(hours, size, color); + let minutes = format_digits(minutes, size, color); + let seconds = format_digits(seconds, size, color); + + strip.strip(|strip| { + strip + .sizes(Size::relative(0.33), 3) + .cell_layout(Layout::centered_and_justified(Direction::TopDown)) + .horizontal(|mut strip| { + strip.cell(|ui| { + ui.label(hours); + }); + strip.cell(|ui| { + ui.label(minutes); + }); + strip.cell(|ui| { + ui.label(seconds); + }); + }); + }); +}