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]
|
[package]
|
||||||
name = "katabastird"
|
name = "katabastird"
|
||||||
version = "1.0.0"
|
version = "1.6.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.61"
|
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]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
|
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
|
||||||
eframe = { version = "0.19", features = ["wgpu"] }
|
eframe = { version = "0.19", features = ["wgpu"] }
|
||||||
egui = { version = "0.19", features = ["bytemuck"] }
|
|
||||||
egui_extras = "0.19"
|
egui_extras = "0.19"
|
||||||
naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
|
|
||||||
rodio = { version = "0.16" }
|
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]
|
Usage: katabastird [OPTIONS]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--hours <HOURS> Hours to count down [default: 0]
|
-h, --hours <HOURS> Hours to count down
|
||||||
-m, --minutes <MINUTES> Minutes to count down [default: 0]
|
-m, --minutes <MINUTES> Minutes to count down
|
||||||
-s, --seconds <SECONDS> Seconds to count down [default: 0]
|
-s, --seconds <SECONDS> Seconds to count down
|
||||||
-a, --alarm <ALARM> Audio file to play at the end of the countdown
|
-a, --alarm <ALARM> Audio file to play at the end of the countdown
|
||||||
|
-A
|
||||||
-r, --running Begin countdown immediately
|
-r, --running Begin countdown immediately
|
||||||
-u, --count-up Count up from zero, actually
|
-u, --count-up Count up from zero, actually
|
||||||
-p, --predator Use the Predator font
|
-p, --predator Use the Predator font
|
||||||
-h, --help Print help information
|
-H, --help Print this help
|
||||||
-V, --version Print version information
|
-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");
|
use std::time::Duration;
|
||||||
pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf");
|
|
||||||
|
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 cli;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
use katabastird::timer::Timer;
|
use katabastird::timer::Timer;
|
||||||
|
|
||||||
fn main() {
|
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 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 egui_extras::{Size, Strip};
|
||||||
use rodio::{source::Source, Decoder, OutputStream};
|
use rodio::{source::Source, Decoder, OutputStream};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue