Compare commits
No commits in common. "3b71b0d5c07fcf0d031953ee21f001a5b7fa6c49" and "628a8e73d6322b9cc3e5509ba1ca275c755b0d3f" have entirely different histories.
3b71b0d5c0
...
628a8e73d6
15 changed files with 887 additions and 1325 deletions
1407
Cargo.lock
generated
1407
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
@ -1,19 +1,16 @@
|
|||
[package]
|
||||
name = "katabastird"
|
||||
version = "1.6.1"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.61"
|
||||
|
||||
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"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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" }
|
||||
# naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
|
||||
# wgpu = { version = "0.14", features = ["naga", "spirv"] }
|
||||
wgpu = { version = "0.14", features = ["naga", "spirv"] }
|
||||
|
|
15
README.md
15
README.md
|
@ -6,20 +6,15 @@ Katabastird is a simple countdown timer that is configured and launched from the
|
|||
Usage: katabastird [OPTIONS]
|
||||
|
||||
Options:
|
||||
-h, --hours <HOURS> Hours to count down
|
||||
-m, --minutes <MINUTES> Minutes to count down
|
||||
-s, --seconds <SECONDS> Seconds to count down
|
||||
--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]
|
||||
-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 this help
|
||||
-h, --help Print help 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
1
VERSION
|
@ -1 +0,0 @@
|
|||
1.61
|
|
@ -1,32 +0,0 @@
|
|||
# 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,13 +1,5 @@
|
|||
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 const AIRHORN: &[u8] = include_bytes!("../airhorn_alarm.mp3");
|
||||
pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf");
|
||||
|
||||
pub mod cli;
|
||||
pub mod timer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use eframe::egui::Vec2;
|
||||
use egui::Vec2;
|
||||
use katabastird::timer::Timer;
|
||||
|
||||
fn main() {
|
||||
|
|
316
src/timer.rs
Normal file
316
src/timer.rs
Normal file
|
@ -0,0 +1,316 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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
257
src/timer/mod.rs
|
@ -1,257 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
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 eframe::egui::{Color32, Direction, FontId, Layout, RichText};
|
||||
use egui::{Color32, Direction, FontId, Layout, RichText};
|
||||
use egui_extras::{Size, Strip};
|
||||
use rodio::{source::Source, Decoder, OutputStream};
|
||||
|
||||
|
|
Loading…
Reference in a new issue