Compare commits
10 commits
628a8e73d6
...
3b71b0d5c0
Author | SHA1 | Date | |
---|---|---|---|
|
3b71b0d5c0 | ||
|
ec3f36779c | ||
|
4957f3f30b | ||
|
033db88b8e | ||
|
4383b703f1 | ||
|
7a8097d035 | ||
|
7a8b950fbd | ||
|
83eb23f1d3 | ||
|
58319dffff | ||
|
4ccc2e4738 |
15 changed files with 1329 additions and 891 deletions
1415
Cargo.lock
generated
1415
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
@ -1,16 +1,19 @@
|
|||
[package]
|
||||
name = "katabastird"
|
||||
version = "1.0.0"
|
||||
version = "1.6.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.61"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
description = "A simple graphical countdown timer that is configured and launched from the commandline."
|
||||
repository = "https://gitlab.com/nebkor/katabastird"
|
||||
readme = "README.md"
|
||||
keywords = ["unix", "commandline", "timer", "gui", "cli"]
|
||||
license-file = "LICENSE.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
|
||||
eframe = { version = "0.19", features = ["wgpu"] }
|
||||
egui = { version = "0.19", features = ["bytemuck"] }
|
||||
egui_extras = "0.19"
|
||||
naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
|
||||
rodio = { version = "0.16" }
|
||||
wgpu = { version = "0.14", features = ["naga", "spirv"] }
|
||||
# naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
|
||||
# wgpu = { version = "0.14", features = ["naga", "spirv"] }
|
||||
|
|
15
README.md
15
README.md
|
@ -6,15 +6,20 @@ Katabastird is a simple countdown timer that is configured and launched from the
|
|||
Usage: katabastird [OPTIONS]
|
||||
|
||||
Options:
|
||||
--hours <HOURS> Hours to count down [default: 0]
|
||||
-m, --minutes <MINUTES> Minutes to count down [default: 0]
|
||||
-s, --seconds <SECONDS> Seconds to count down [default: 0]
|
||||
-h, --hours <HOURS> Hours to count down
|
||||
-m, --minutes <MINUTES> Minutes to count down
|
||||
-s, --seconds <SECONDS> Seconds to count down
|
||||
-a, --alarm <ALARM> Audio file to play at the end of the countdown
|
||||
-A
|
||||
-r, --running Begin countdown immediately
|
||||
-u, --count-up Count up from zero, actually
|
||||
-p, --predator Use the Predator font
|
||||
-h, --help Print help information
|
||||
-H, --help Print this help
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
"[Katabasis](https://en.wikipedia.org/wiki/Katabasis)" is the descent into the Underworld.
|
||||
|
||||
All content is licensed under the Chaos License; see [LICENSE.md](./LICENSE.md) for details.
|
||||
|
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
1.61
|
32
VERSIONING.md
Normal file
32
VERSIONING.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Golden Versioning
|
||||
|
||||
```katabastird``` is versioned under a scheme I call "goldver", as an homage to the
|
||||
vastly inferior [semver](https://semver.org).
|
||||
|
||||
## What does "goldver" mean?
|
||||
|
||||
When projects are versioned with goldver, the first version is "1". Note that it
|
||||
is not "1.0", or, "1.0-prealpha-release-preview", or anything nonsensical like
|
||||
that. As new versions are released, decimals from *phi*, the [Golden
|
||||
Ratio](https://en.wikipedia.org/wiki/Golden_ratio), are appended after an
|
||||
initial decimal point. So the second released version will be "1.6", the third
|
||||
would be "1.61", etc., and on until perfection is asymptotically approached as
|
||||
the number of released versions goes to infinity.
|
||||
|
||||
## Wait, didn't Donald Knuth do this?
|
||||
|
||||
No! He uses [pi for TeX and e for MetaFont](https://texfaq.org/FAQ-TeXfuture),
|
||||
obviously COMPLETELY different.
|
||||
|
||||
## Ok.
|
||||
|
||||
Cool.
|
||||
|
||||
## What version is katabastird now?
|
||||
|
||||
Canonically, see the ```VERSION``` file. Heretically, once there have been
|
||||
at least three releases, the version string in the ```Cargo.toml``` file will
|
||||
always be of the form "1.6.x", where *x* is at least one digit long, starting
|
||||
with "1". Each subsequent release will append the next digit of *phi* to
|
||||
*x*. The number of releases can be calculated by counting the number of digits
|
||||
in *x* and adding 2 to that.
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,5 +1,13 @@
|
|||
pub const AIRHORN: &[u8] = include_bytes!("../airhorn_alarm.mp3");
|
||||
pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf");
|
||||
use std::time::Duration;
|
||||
|
||||
pub const AIRHORN: &[u8] = include_bytes!("../resources/airhorn_alarm.mp3");
|
||||
pub const PREDATOR_FONT: &[u8] = include_bytes!("../resources/Predator.ttf");
|
||||
|
||||
pub(crate) const MIN_REPAINT: Duration = Duration::from_millis(100);
|
||||
pub(crate) const MAX_REPAINT: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) const DIGIT_FACTOR: f32 = 0.4;
|
||||
pub(crate) const TEXT_FACTOR: f32 = 0.2;
|
||||
|
||||
pub mod cli;
|
||||
pub mod timer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use egui::Vec2;
|
||||
use eframe::egui::Vec2;
|
||||
use katabastird::timer::Timer;
|
||||
|
||||
fn main() {
|
||||
|
|
316
src/timer.rs
316
src/timer.rs
|
@ -1,316 +0,0 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use clap::Parser;
|
||||
use eframe::{App, CreationContext};
|
||||
use egui::{Color32, Direction, FontId, Layout, RichText, Ui};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
|
||||
use crate::{cli::Cli, util::*, AIRHORN, PREDATOR_FONT};
|
||||
|
||||
const MIN_REPAINT: Duration = Duration::from_millis(100);
|
||||
const MAX_REPAINT: Duration = Duration::from_millis(250);
|
||||
|
||||
const DIGIT_FACTOR: f32 = 0.4;
|
||||
const TEXT_FACTOR: f32 = 0.2;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CountDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timer {
|
||||
direction: CountDirection,
|
||||
duration: Duration,
|
||||
state: TimerState,
|
||||
tstart: Instant, // so we can blink
|
||||
alarm: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum TimerState {
|
||||
Unstarted,
|
||||
Paused(ChronoState),
|
||||
Running(ChronoState),
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl PartialEq for TimerState {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(Self::Paused(_), Self::Paused(_))
|
||||
| (Self::Running(_), Self::Running(_))
|
||||
| (Self::Unstarted, Self::Unstarted)
|
||||
| (Self::Finished, Self::Finished)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TimerState {}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ChronoState {
|
||||
updated: Instant,
|
||||
remaining: Duration,
|
||||
}
|
||||
|
||||
impl App for Timer {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
ctx.request_repaint_after(MAX_REPAINT);
|
||||
let height = ctx.input().screen_rect().height();
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
match self.state {
|
||||
TimerState::Unstarted => self.unstarted(ui, height),
|
||||
TimerState::Running(cs) => {
|
||||
let dur = Instant::now() - cs.updated;
|
||||
let dur = MIN_REPAINT.saturating_sub(dur);
|
||||
ctx.request_repaint_after(dur);
|
||||
self.running(ui, height, cs);
|
||||
}
|
||||
TimerState::Paused(cs) => self.paused(ui, height, cs),
|
||||
TimerState::Finished => self.finished(ui, height),
|
||||
}
|
||||
// check for quit key
|
||||
if ui.input().key_pressed(egui::Key::Q) || ui.input().key_pressed(egui::Key::Escape) {
|
||||
frame.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn new(ctx: &CreationContext) -> Self {
|
||||
let cli = Cli::parse();
|
||||
let predator = cli.predator;
|
||||
let seconds = cli.hours.unwrap_or(0) * 3600
|
||||
+ cli.minutes.unwrap_or(0) * 60
|
||||
+ cli.seconds.unwrap_or(0);
|
||||
let duration = Duration::from_secs(seconds);
|
||||
|
||||
let direction = if cli.count_up {
|
||||
CountDirection::Up
|
||||
} else {
|
||||
CountDirection::Down
|
||||
};
|
||||
|
||||
let alarm = if let Some(path) = cli.alarm {
|
||||
let buffer = std::fs::read(&path)
|
||||
.unwrap_or_else(|_| panic!("Could not open {:?} for reading.", path));
|
||||
Some(buffer)
|
||||
} else if cli.airhorn {
|
||||
Some(AIRHORN.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if predator {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"predator".to_owned(),
|
||||
egui::FontData::from_static(PREDATOR_FONT),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "predator".to_owned());
|
||||
ctx.egui_ctx.set_fonts(fonts);
|
||||
}
|
||||
ctx.egui_ctx.request_repaint_after(Duration::from_secs(1));
|
||||
let mut timer = Timer {
|
||||
duration,
|
||||
direction,
|
||||
state: TimerState::Unstarted,
|
||||
tstart: Instant::now(),
|
||||
alarm,
|
||||
};
|
||||
if cli.running {
|
||||
let cs = ChronoState {
|
||||
remaining: duration,
|
||||
updated: Instant::now(),
|
||||
};
|
||||
|
||||
timer.state = TimerState::Running(cs);
|
||||
}
|
||||
timer
|
||||
}
|
||||
|
||||
fn unstarted(&mut self, ui: &mut Ui, size: f32) {
|
||||
let tsize = size * 0.5;
|
||||
let start = RichText::new("START")
|
||||
.font(FontId::monospace(tsize))
|
||||
.color(Color32::WHITE)
|
||||
.background_color(Color32::LIGHT_GREEN);
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder())
|
||||
.cell_layout(Layout::centered_and_justified(egui::Direction::TopDown))
|
||||
.horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
if ui.button(start).clicked() {
|
||||
let dur = self.duration;
|
||||
self.state = TimerState::Running(ChronoState {
|
||||
updated: Instant::now(),
|
||||
remaining: dur,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn running(&mut self, ui: &mut Ui, size: f32, cs: ChronoState) {
|
||||
let tsize = size * TEXT_FACTOR;
|
||||
let text = RichText::new("PAUSE")
|
||||
.font(FontId::monospace(tsize))
|
||||
.color(Color32::GOLD);
|
||||
|
||||
let mut is_paused = false;
|
||||
let elapsed = Instant::now() - cs.updated;
|
||||
let remaining = cs.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| {
|
||||
// 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 = Color32::DARK_GRAY;
|
||||
let tsize = size * DIGIT_FACTOR;
|
||||
|
||||
display_digits(&mut strip, remaining, color, tsize);
|
||||
});
|
||||
|
||||
if remaining.is_zero() {
|
||||
if let Some(alarm_file) = &self.alarm {
|
||||
let alarm_file = alarm_file.to_owned();
|
||||
std::thread::spawn(move || alarm(alarm_file));
|
||||
}
|
||||
self.state = TimerState::Finished;
|
||||
return;
|
||||
}
|
||||
|
||||
let cs = ChronoState {
|
||||
remaining,
|
||||
updated: Instant::now(),
|
||||
};
|
||||
if is_paused {
|
||||
// did we click the reset button?
|
||||
if self.state == TimerState::Unstarted {
|
||||
return;
|
||||
}
|
||||
self.state = TimerState::Paused(cs);
|
||||
} else {
|
||||
self.state = TimerState::Running(cs);
|
||||
}
|
||||
}
|
||||
|
||||
fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) {
|
||||
let mut is_running = false;
|
||||
let tsize = vsize * TEXT_FACTOR;
|
||||
|
||||
let remaining = cs.remaining;
|
||||
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
|
||||
}
|
||||
};
|
||||
// if we're counting up, do the right thing
|
||||
let remaining = match self.direction {
|
||||
CountDirection::Down => remaining,
|
||||
CountDirection::Up => self.duration - remaining,
|
||||
};
|
||||
display_digits(&mut strip, remaining, color, vsize * DIGIT_FACTOR);
|
||||
});
|
||||
|
||||
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 tsize = vsize * 0.3;
|
||||
let reset = RichText::new("RESTART")
|
||||
.color(Color32::DARK_GREEN)
|
||||
.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 * DIGIT_FACTOR);
|
||||
});
|
||||
}
|
||||
}
|
57
src/timer/eframe_app.rs
Normal file
57
src/timer/eframe_app.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use eframe::{
|
||||
egui::{self, Frame},
|
||||
epaint::Color32,
|
||||
Frame as EFrame,
|
||||
};
|
||||
|
||||
use super::{state::*, Timer};
|
||||
use crate::{MAX_REPAINT, MIN_REPAINT};
|
||||
|
||||
const STARTING_COLOR: &[f32; 3] = &[10.0, 4.0, 14.0];
|
||||
const UNIT_COLOR: &[f32; 3] = &[0.986, 0.154, 0.055]; // [160, 125, 9].normalize()
|
||||
|
||||
impl eframe::App for Timer {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut EFrame) {
|
||||
ctx.request_repaint_after(MAX_REPAINT);
|
||||
let height = ctx.input().screen_rect().height();
|
||||
|
||||
let t = self.done.powi(3);
|
||||
let color = get_color(t);
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(Frame::none().fill(color))
|
||||
.show(ctx, |ui| {
|
||||
match self.state {
|
||||
TimerState::Unstarted => self.unstarted(ui, height),
|
||||
TimerState::Running(cs) => {
|
||||
let dur = Instant::now() - cs.updated;
|
||||
let dur = MIN_REPAINT.saturating_sub(dur);
|
||||
ctx.request_repaint_after(dur);
|
||||
self.running(ui, height, cs);
|
||||
}
|
||||
TimerState::Paused(cs) => self.paused(ui, height, cs),
|
||||
TimerState::Finished => self.finished(ui, height),
|
||||
}
|
||||
// check for quit key
|
||||
if ui.input().key_pressed(egui::Key::Q) || ui.input().key_pressed(egui::Key::Escape)
|
||||
{
|
||||
frame.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn get_color(t: f32) -> Color32 {
|
||||
let [sr, sg, sb] = STARTING_COLOR;
|
||||
let [ur, ug, ub] = UNIT_COLOR;
|
||||
let mag = t * 162.0;
|
||||
let (r, g, b) = (
|
||||
(sr + (mag * ur).round()) as u8,
|
||||
(sg + (mag * ug).round()) as u8,
|
||||
(sb + (mag * ub).round()) as u8,
|
||||
);
|
||||
// when t is 1.0, then the final color is roughly 170, 29, 23.
|
||||
Color32::from_rgb(r, g, b)
|
||||
}
|
58
src/timer/gui.rs
Normal file
58
src/timer/gui.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use std::{sync::mpsc::Sender, time::Duration};
|
||||
|
||||
use eframe::egui::{Color32, Direction, Layout, RichText, Ui};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
|
||||
use super::state::NextTimerState;
|
||||
use crate::util::display_digits;
|
||||
|
||||
pub(crate) fn two_rows(
|
||||
ui: &mut Ui,
|
||||
buttons: &[(RichText, NextTimerState)],
|
||||
sender: Sender<NextTimerState>,
|
||||
remaining: Duration,
|
||||
digit_color: Color32,
|
||||
digit_size: f32,
|
||||
) {
|
||||
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(), buttons.len())
|
||||
.cell_layout(Layout::centered_and_justified(Direction::TopDown))
|
||||
.horizontal(|mut pstrip| {
|
||||
for (button, signal) in buttons.iter() {
|
||||
pstrip.cell(|ui| {
|
||||
if ui.button(button.clone()).clicked() {
|
||||
sender.send(*signal).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
display_digits(&mut strip, remaining, digit_color, digit_size);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn one_row(
|
||||
ui: &mut Ui,
|
||||
buttons: &[(RichText, NextTimerState)],
|
||||
sender: Sender<NextTimerState>,
|
||||
) {
|
||||
StripBuilder::new(ui)
|
||||
.sizes(Size::remainder(), buttons.len())
|
||||
.cell_layout(Layout::centered_and_justified(Direction::TopDown))
|
||||
.horizontal(|mut strip| {
|
||||
for (button, signal) in buttons.iter() {
|
||||
strip.cell(|ui| {
|
||||
if ui.button(button.clone()).clicked() {
|
||||
sender.send(*signal).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
257
src/timer/mod.rs
Normal file
257
src/timer/mod.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use std::sync::mpsc::channel;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use clap::Parser;
|
||||
use eframe::egui::{self, Color32, FontId, RichText, Ui};
|
||||
|
||||
use crate::{cli::Cli, util::*, AIRHORN, DIGIT_FACTOR, MAX_REPAINT, PREDATOR_FONT, TEXT_FACTOR};
|
||||
|
||||
mod state;
|
||||
use state::{ChronoState, NextTimerState, TimerState};
|
||||
mod eframe_app;
|
||||
mod gui;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CountDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Timer {
|
||||
direction: CountDirection,
|
||||
duration: Duration,
|
||||
state: TimerState,
|
||||
tstart: Instant, // so we can blink
|
||||
alarm: Option<Vec<u8>>,
|
||||
done: f32,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn new(ctx: &eframe::CreationContext) -> Self {
|
||||
let cli = Cli::parse();
|
||||
let predator = cli.predator;
|
||||
let seconds = cli.hours.unwrap_or(0) * 3600
|
||||
+ cli.minutes.unwrap_or(0) * 60
|
||||
+ cli.seconds.unwrap_or(0);
|
||||
let duration = Duration::from_secs(seconds);
|
||||
|
||||
let direction = if cli.count_up {
|
||||
CountDirection::Up
|
||||
} else {
|
||||
CountDirection::Down
|
||||
};
|
||||
|
||||
let alarm = if let Some(path) = cli.alarm {
|
||||
let buffer = std::fs::read(&path).unwrap_or_else(|_| {
|
||||
panic!("Could not open alarm sound file {:?} for reading.", path)
|
||||
});
|
||||
Some(buffer)
|
||||
} else if cli.airhorn {
|
||||
Some(AIRHORN.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if predator {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"predator".to_owned(),
|
||||
egui::FontData::from_static(PREDATOR_FONT),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "predator".to_owned());
|
||||
ctx.egui_ctx.set_fonts(fonts);
|
||||
}
|
||||
ctx.egui_ctx.request_repaint_after(MAX_REPAINT);
|
||||
let updated = Instant::now();
|
||||
let mut timer = Timer {
|
||||
duration,
|
||||
direction,
|
||||
state: TimerState::Unstarted,
|
||||
tstart: updated,
|
||||
alarm,
|
||||
done: 0.0,
|
||||
};
|
||||
if cli.running {
|
||||
let cs = ChronoState {
|
||||
remaining: duration,
|
||||
updated,
|
||||
};
|
||||
|
||||
timer.state = TimerState::Running(cs);
|
||||
}
|
||||
|
||||
timer
|
||||
}
|
||||
|
||||
fn unstarted(&mut self, ui: &mut Ui, size: f32) {
|
||||
let tsize = size * 0.5;
|
||||
let start = RichText::new("START")
|
||||
.font(FontId::monospace(tsize))
|
||||
.color(Color32::WHITE)
|
||||
.background_color(Color32::LIGHT_GREEN);
|
||||
self.done = 0.0;
|
||||
|
||||
let (sender, rx) = channel();
|
||||
gui::one_row(ui, &[(start, NextTimerState::Running)], sender);
|
||||
|
||||
if rx.recv().is_ok() {
|
||||
let dur = self.duration;
|
||||
self.state = TimerState::Running(ChronoState {
|
||||
updated: Instant::now(),
|
||||
remaining: dur,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn running(&mut self, ui: &mut Ui, size: f32, cs: ChronoState) {
|
||||
let tsize = size * TEXT_FACTOR;
|
||||
let text = RichText::new("PAUSE")
|
||||
.font(FontId::monospace(tsize))
|
||||
.color(Color32::GOLD);
|
||||
|
||||
let elapsed = Instant::now() - cs.updated;
|
||||
let remaining = cs.remaining.saturating_sub(elapsed);
|
||||
if remaining.is_zero() {
|
||||
if let Some(alarm_sound) = &self.alarm {
|
||||
let alarm_sound = alarm_sound.to_owned();
|
||||
std::thread::spawn(move || alarm(alarm_sound));
|
||||
}
|
||||
self.state = TimerState::Finished;
|
||||
self.done = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
self.done = 1.0 - (remaining.as_secs_f32() / self.duration.as_secs_f32());
|
||||
|
||||
let (sender, rx) = channel();
|
||||
{
|
||||
// if we're counting up, do the right thing
|
||||
let remaining = match self.direction {
|
||||
CountDirection::Down => remaining,
|
||||
CountDirection::Up => self.duration - remaining,
|
||||
};
|
||||
let color = Color32::DARK_GRAY;
|
||||
let tsize = size * DIGIT_FACTOR;
|
||||
gui::two_rows(
|
||||
ui,
|
||||
&[(text, state::NextTimerState::Paused)],
|
||||
sender,
|
||||
remaining,
|
||||
color,
|
||||
tsize,
|
||||
);
|
||||
}
|
||||
|
||||
let cs = ChronoState {
|
||||
remaining,
|
||||
updated: Instant::now(),
|
||||
};
|
||||
|
||||
if rx.recv().is_ok() {
|
||||
self.state = TimerState::Paused(cs);
|
||||
} else {
|
||||
self.state = TimerState::Running(cs);
|
||||
}
|
||||
}
|
||||
|
||||
fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) {
|
||||
let tsize = vsize * TEXT_FACTOR;
|
||||
|
||||
let remaining = cs.remaining;
|
||||
let resume = RichText::new("RESUME")
|
||||
.color(Color32::GREEN)
|
||||
.font(FontId::monospace(tsize));
|
||||
let reset = RichText::new("RESET")
|
||||
.color(Color32::RED)
|
||||
.font(FontId::monospace(tsize));
|
||||
|
||||
let (sender, rx) = channel();
|
||||
{
|
||||
let color = {
|
||||
let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0;
|
||||
if blink {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::DARK_GRAY
|
||||
}
|
||||
};
|
||||
// if we're counting up, do the right thing
|
||||
let remaining = match self.direction {
|
||||
CountDirection::Down => remaining,
|
||||
CountDirection::Up => self.duration - remaining,
|
||||
};
|
||||
|
||||
gui::two_rows(
|
||||
ui,
|
||||
&[
|
||||
(resume, NextTimerState::Running),
|
||||
(reset, NextTimerState::Unstarted),
|
||||
],
|
||||
sender,
|
||||
remaining,
|
||||
color,
|
||||
vsize * DIGIT_FACTOR,
|
||||
);
|
||||
}
|
||||
|
||||
let cs = ChronoState {
|
||||
remaining,
|
||||
updated: Instant::now(),
|
||||
};
|
||||
if let Ok(s) = rx.recv() {
|
||||
match s {
|
||||
NextTimerState::Running => {
|
||||
self.state = TimerState::Running(cs);
|
||||
}
|
||||
NextTimerState::Unstarted => {
|
||||
self.state = TimerState::Unstarted;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
self.state = TimerState::Paused(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),
|
||||
};
|
||||
|
||||
let tsize = vsize * 0.3;
|
||||
let reset = RichText::new("RESTART")
|
||||
.color(Color32::DARK_GREEN)
|
||||
.font(FontId::monospace(tsize));
|
||||
let color = {
|
||||
let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0;
|
||||
if blink {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::DARK_GRAY
|
||||
}
|
||||
};
|
||||
let (sender, rx) = channel();
|
||||
|
||||
gui::two_rows(
|
||||
ui,
|
||||
&[(reset, NextTimerState::Running)], // technically we can send anything but let's try not to be misleading
|
||||
sender,
|
||||
remaining,
|
||||
color,
|
||||
vsize * DIGIT_FACTOR,
|
||||
);
|
||||
if rx.recv().is_ok() {
|
||||
let cs = ChronoState {
|
||||
remaining: self.duration,
|
||||
updated: Instant::now(),
|
||||
};
|
||||
self.state = TimerState::Running(cs);
|
||||
}
|
||||
}
|
||||
}
|
40
src/timer/state.rs
Normal file
40
src/timer/state.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum TimerState {
|
||||
Unstarted,
|
||||
Paused(ChronoState),
|
||||
Running(ChronoState),
|
||||
Finished,
|
||||
}
|
||||
|
||||
// this is a bit of a hack to deat with not being able to have const instances of the regular
|
||||
// timerstate.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum NextTimerState {
|
||||
Unstarted,
|
||||
Paused,
|
||||
Running,
|
||||
// no need for finished, there will never be a button to click on that says "finished"
|
||||
}
|
||||
|
||||
impl PartialEq for TimerState {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(Self::Paused(_), Self::Paused(_))
|
||||
| (Self::Running(_), Self::Running(_))
|
||||
| (Self::Unstarted, Self::Unstarted)
|
||||
| (Self::Finished, Self::Finished)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TimerState {}
|
||||
impl Eq for NextTimerState {}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct ChronoState {
|
||||
pub updated: Instant,
|
||||
pub remaining: Duration,
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use std::{io::Cursor, time::Duration};
|
||||
|
||||
use egui::{Color32, Direction, FontId, Layout, RichText};
|
||||
use eframe::egui::{Color32, Direction, FontId, Layout, RichText};
|
||||
use egui_extras::{Size, Strip};
|
||||
use rodio::{source::Source, Decoder, OutputStream};
|
||||
|
||||
|
|
Loading…
Reference in a new issue