diff --git a/airhorn_alarm.mp3 b/airhorn_alarm.mp3 new file mode 100644 index 0000000..23d2813 Binary files /dev/null and b/airhorn_alarm.mp3 differ diff --git a/src/cli.rs b/src/cli.rs index 13dd50e..3a417ec 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,5 @@ +use std::ffi::OsString; + use clap::Parser; #[derive(Debug, Parser)] @@ -17,7 +19,10 @@ pub struct Cli { /// Audio file to play at the end of the countdown. #[clap(long, short)] - pub alarm: Option, + pub alarm: Option, + + #[clap(short = 'A', conflicts_with = "alarm")] + pub airhorn: bool, /// Begin countdown immediately. #[clap(long = "immediate", long = "running", short = 'i', short = 'r')] diff --git a/src/lib.rs b/src/lib.rs index cb4f116..4d6ccb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,14 @@ +use std::ffi::OsString; + +pub const AIRHORN: &[u8] = include_bytes!("../airhorn_alarm.mp3"); +pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf"); + pub mod cli; pub mod timer; mod util; + +#[derive(Debug, Clone)] +pub(crate) enum Alarm { + Airhon, + User(OsString), +} diff --git a/src/main.rs b/src/main.rs index 9a3eb55..9703072 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,8 @@ use katabastird::timer::Timer; fn main() { let options = eframe::NativeOptions { renderer: eframe::Renderer::Wgpu, - initial_window_size: Some(Vec2::new(1400.0, 800.0)), + max_window_size: Some(Vec2::new(1400.0, 800.0)), + min_window_size: Some(Vec2::new(966.0, 600.0)), ..Default::default() }; diff --git a/src/timer.rs b/src/timer.rs index fd2be9c..644121b 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -5,10 +5,10 @@ use eframe::{App, CreationContext}; use egui::{Color32, Direction, FontId, Layout, RichText, Ui}; use egui_extras::{Size, StripBuilder}; -use crate::{cli::Cli, util::*}; +use crate::{cli::Cli, util::*, Alarm, PREDATOR_FONT}; -const MIN_REPAINT: Duration = Duration::from_millis(200); // one frame before 200ms -const MAX_REPAINT: Duration = Duration::from_millis(500); +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; @@ -25,7 +25,7 @@ pub struct Timer { duration: Duration, state: TimerState, tstart: Instant, // so we can blink - alarm: Option, + alarm: Option, } #[derive(Debug, Clone, Copy)] @@ -67,9 +67,9 @@ impl App for Timer { let dur = Instant::now() - cs.updated; let dur = MIN_REPAINT.saturating_sub(dur); ctx.request_repaint_after(dur); - self.running(ui, height); + self.running(ui, height, cs); } - TimerState::Paused(_) => self.paused(ui, height), + TimerState::Paused(cs) => self.paused(ui, height, cs), TimerState::Finished => self.finished(ui, height), } // check for quit key @@ -95,11 +95,19 @@ impl Timer { CountDirection::Down }; + let alarm = if let Some(path) = cli.alarm { + Some(Alarm::User(path)) + } else if cli.airhorn { + Some(Alarm::Airhon) + } else { + None + }; + if predator { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "predator".to_owned(), - egui::FontData::from_static(include_bytes!("../Predator.ttf")), + egui::FontData::from_static(PREDATOR_FONT), ); fonts .families @@ -114,7 +122,7 @@ impl Timer { direction, state: TimerState::Unstarted, tstart: Instant::now(), - alarm: cli.alarm, + alarm, }; if cli.running { let cs = ChronoState { @@ -150,23 +158,15 @@ impl Timer { }); } - fn running(&mut self, ui: &mut Ui, size: f32) { + 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; - let remaining; let mut is_paused = false; - if let TimerState::Running(rs) = self.state { - elapsed = Instant::now() - rs.updated; - remaining = rs.remaining; - } else { - unreachable!() - } - - let remaining = remaining.saturating_sub(elapsed); + let elapsed = Instant::now() - cs.updated; + let remaining = cs.remaining.saturating_sub(elapsed); StripBuilder::new(ui) .size(Size::relative(0.3333)) @@ -216,17 +216,11 @@ impl Timer { } } - fn paused(&mut self, ui: &mut Ui, vsize: f32) { - let elapsed; + fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) { let mut is_running = false; let tsize = vsize * TEXT_FACTOR; - if let TimerState::Paused(cs) = self.state { - elapsed = cs.remaining; - } else { - unreachable!() - } - let remaining = elapsed; + let remaining = cs.remaining; StripBuilder::new(ui) .size(Size::relative(0.33333)) .size(Size::remainder()) diff --git a/src/util.rs b/src/util.rs index 9fdc330..3b38cb7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,15 @@ -use std::time::Duration; +use std::{ + ffi::OsStr, + fs::File, + io::{Cursor, Read, Seek}, + time::Duration, +}; use egui::{Color32, Direction, FontId, Layout, RichText}; use egui_extras::{Size, Strip}; +use rodio::{source::Source, Decoder, OutputStream}; + +use crate::{Alarm, AIRHORN}; fn format_digits(digits: u64, size: f32, color: Color32) -> RichText { RichText::new(format!("{:02}", digits)) @@ -9,14 +17,29 @@ fn format_digits(digits: u64, size: f32, color: Color32) -> RichText { .color(color) } -pub fn alarm(path: String) { - use rodio::{source::Source, Decoder, OutputStream}; - use std::fs::File; - use std::io::BufReader; +pub(crate) fn alarm(source: Alarm) { + if let Alarm::User(path) = source { + play_user(&path) + } else { + let source = Cursor::new(AIRHORN.to_owned()); + let decoder = Decoder::new(source).unwrap(); + play_alarm(decoder); + } +} - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let file = BufReader::new(File::open(path).unwrap()); +fn play_user(path: &OsStr) { + let mut f = File::open(path).unwrap_or_else(|_| panic!("Could not open file {:?}", path)); + let metadata = std::fs::metadata(path).expect("unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + f.read_exact(&mut buffer).expect("buffer overflow"); + let file = Cursor::new(buffer); let source = Decoder::new(file).unwrap(); + play_alarm(source); +} + +fn play_alarm(source: Decoder) { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let dur = if let Some(dur) = source.total_duration() { dur } else {