try to clean it up a bit

This commit is contained in:
Joe Ardent 2022-10-15 18:12:56 -07:00
parent 827af8aea3
commit 5c6405d7fb
5 changed files with 260 additions and 224 deletions

33
src/cli.rs Normal file
View file

@ -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<String>,
/// 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,
}

View file

@ -1 +1,3 @@
pub mod cli;
pub mod timer; pub mod timer;
mod util;

View file

@ -1,40 +1,5 @@
use std::time::Duration;
use clap::Parser;
use egui::Vec2; use egui::Vec2;
use katabastird::timer::{CountDirection, Timer}; use katabastird::timer::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<String>,
/// 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,
}
fn main() { fn main() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
@ -43,29 +8,9 @@ fn main() {
..Default::default() ..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( eframe::run_native(
"katabastird", "katabastird",
options, options,
Box::new(move |cc| { Box::new(move |cc| Box::new(Timer::new(cc))),
Box::new(Timer::new(
duration,
direction,
cc,
!cli.running,
cli.predator,
cli.alarm,
))
}),
); );
} }

View file

@ -2,6 +2,9 @@ use egui::{Color32, Direction, FontId, Layout, RichText, Ui};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::{cli::Cli, util::*};
use clap::Parser;
use eframe::{App, CreationContext}; use eframe::{App, CreationContext};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -14,79 +17,73 @@ pub enum CountDirection {
pub struct Timer { pub struct Timer {
direction: CountDirection, direction: CountDirection,
duration: Duration, duration: Duration,
state: State, state: TimerState,
tstart: Instant, tstart: Instant,
alarm: Option<String>, alarm: Option<String>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum State { enum TimerState {
Unstarted, Unstarted,
Paused(ChronoState), // time remaining Paused(ChronoState), // time remaining
Running(ChronoState), // time remaining Running(ChronoState), // time remaining
Finished,
} }
impl PartialEq for State { impl PartialEq for TimerState {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
match (self, other) { matches!(
(Self::Paused(_), Self::Paused(_)) => true, (self, other),
(Self::Running(_), Self::Running(_)) => true, (Self::Paused(_), Self::Paused(_))
(Self::Unstarted, Self::Unstarted) => true, | (Self::Running(_), Self::Running(_))
_ => false, | (Self::Unstarted, Self::Unstarted)
} | (Self::Finished, Self::Finished)
)
} }
} }
impl Eq for State {} impl Eq for TimerState {}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct ChronoState { struct ChronoState {
started: Instant, updated: Instant,
remaining: Duration, 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 { impl App for Timer {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_secs(1)); ctx.request_repaint_after(Duration::from_secs(1));
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let size = ctx.used_size(); let size = ctx.used_size();
let vsize = if size[1].is_normal() { let vsize = if size.x.is_normal() {
size[1].abs() size.x.abs()
} else { } else {
600.0 400.0
}; };
match self.state { match self.state {
State::Unstarted => self.unstarted(ui, vsize), TimerState::Unstarted => self.unstarted(ui, vsize),
_ => self.running(ui, vsize), TimerState::Running(_) => self.running(ui, vsize),
TimerState::Paused(_) => self.paused(ui, vsize),
TimerState::Finished => self.finished(ui, vsize),
} }
}); });
} }
} }
impl Timer { impl Timer {
pub fn new( pub fn new(ctx: &CreationContext) -> Self {
duration: Duration, let cli = Cli::parse();
direction: CountDirection, let predator = cli.predator;
ctx: &CreationContext, let seconds = cli.hours * 3600 + cli.minutes * 60 + cli.seconds;
paused: bool, let duration = Duration::from_secs(seconds);
predator: bool,
alarm: Option<String>, let direction = if cli.count_up {
) -> Self { CountDirection::Up
let tstart = Instant::now(); } else {
CountDirection::Down
};
if predator { if predator {
let mut fonts = egui::FontDefinitions::default(); let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert( fonts.font_data.insert(
@ -101,27 +98,22 @@ impl Timer {
ctx.egui_ctx.set_fonts(fonts); ctx.egui_ctx.set_fonts(fonts);
} }
ctx.egui_ctx.request_repaint_after(Duration::from_secs(1)); ctx.egui_ctx.request_repaint_after(Duration::from_secs(1));
if paused { let mut timer = Timer {
Timer { duration,
duration, direction,
direction, state: TimerState::Unstarted,
state: State::Unstarted, tstart: Instant::now(),
tstart, alarm: cli.alarm,
alarm, };
} if cli.running {
} else {
let cs = ChronoState { let cs = ChronoState {
remaining: duration, remaining: duration,
started: Instant::now(), updated: Instant::now(),
}; };
Timer {
duration, timer.state = TimerState::Running(cs);
direction,
state: State::Running(cs),
tstart,
alarm,
}
} }
timer
} }
fn unstarted(&mut self, ui: &mut Ui, size: f32) { fn unstarted(&mut self, ui: &mut Ui, size: f32) {
@ -137,8 +129,8 @@ impl Timer {
strip.cell(|ui| { strip.cell(|ui| {
if ui.button(start).clicked() { if ui.button(start).clicked() {
let dur = self.duration; let dur = self.duration;
self.state = State::Running(ChronoState { self.state = TimerState::Running(ChronoState {
started: Instant::now(), updated: Instant::now(),
remaining: dur, remaining: dur,
}); });
} }
@ -153,111 +145,39 @@ impl Timer {
.color(Color32::GOLD); .color(Color32::GOLD);
let elapsed; let elapsed;
let started; let remaining;
let mut is_paused; let mut is_paused = false;
if let State::Running(rs) = self.state { if let TimerState::Running(rs) = self.state {
elapsed = Instant::now() - rs.started; elapsed = Instant::now() - rs.updated;
started = rs.started; remaining = rs.remaining;
is_paused = false;
} else if let State::Paused(cs) = self.state {
elapsed = cs.remaining;
started = Instant::now() - (self.duration - elapsed);
is_paused = true;
} else { } else {
unreachable!() unreachable!()
} }
let remaining = if is_paused { let remaining = remaining.saturating_sub(elapsed);
elapsed
} else {
self.duration.saturating_sub(elapsed)
};
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3333)) .size(Size::relative(0.3333))
.size(Size::remainder()) .size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::LeftToRight)) .cell_layout(Layout::centered_and_justified(Direction::LeftToRight))
.vertical(|mut strip| { .vertical(|mut strip| {
if is_paused { // first the pause
strip.strip(|pstrip| { strip.cell(|ui| {
pstrip if ui.button(text).clicked() {
.sizes(Size::remainder(), 2) is_paused = true;
.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;
}
});
}
// if we're counting up, do the right thing // if we're counting up, do the right thing
let remaining = match self.direction { let remaining = match self.direction {
CountDirection::Down => remaining, CountDirection::Down => remaining,
CountDirection::Up => self.duration - remaining, CountDirection::Up => self.duration - remaining,
}; };
// now the numbers // now the numbers
let color = if is_paused { let color = Color32::DARK_GRAY;
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 tsize = size * 0.7; let tsize = size * 0.7;
let hours = RichText::new(format!("{:02}", hours))
.font(FontId::monospace(tsize)) display_digits(&mut strip, remaining, color, 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);
});
});
});
}); });
if remaining.is_zero() { if remaining.is_zero() {
@ -265,36 +185,118 @@ impl Timer {
let alarm_file = alarm_file.to_owned(); let alarm_file = alarm_file.to_owned();
std::thread::spawn(move || alarm(alarm_file)); std::thread::spawn(move || alarm(alarm_file));
} }
self.state = State::Unstarted; self.state = TimerState::Finished;
return; return;
} }
let cs = ChronoState { remaining, started }; let cs = ChronoState {
remaining,
updated: Instant::now(),
};
if is_paused { if is_paused {
if self.state == State::Unstarted { // did we click the reset button?
if self.state == TimerState::Unstarted {
return; return;
} }
self.state = State::Paused(cs); self.state = TimerState::Paused(cs);
} else { } else {
self.state = State::Running(cs); self.state = TimerState::Running(cs);
} }
} }
}
fn paused(&mut self, ui: &mut Ui, vsize: f32) {
fn alarm(path: String) { let elapsed;
use rodio::{source::Source, Decoder, OutputStream}; let mut is_running = false;
use std::fs::File; let tsize = vsize * 0.3;
use std::io::BufReader; if let TimerState::Paused(cs) = self.state {
elapsed = cs.remaining;
let (_stream, stream_handle) = OutputStream::try_default().unwrap(); } else {
let file = BufReader::new(File::open(path).unwrap()); unreachable!()
let source = Decoder::new(file).unwrap(); }
let dur = if let Some(dur) = source.total_duration() {
dur let remaining = elapsed;
} else { StripBuilder::new(ui)
Duration::from_secs(10) .size(Size::relative(0.33333))
}; .size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::LeftToRight))
let _ = stream_handle.play_raw(source.convert_samples()); .vertical(|mut strip| {
std::thread::sleep(dur); 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);
});
}
} }

54
src/util.rs Normal file
View file

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