a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: play two-tone chime on break start

+58 -2
+18 -2
src/main.rs
··· 5 5 mod idle; 6 6 mod overlay; 7 7 mod settings; 8 + mod sound; 8 9 mod timer; 9 10 mod tray; 10 11 ··· 62 63 let tray = Rc::new(AppTray::new()?); 63 64 let is_dark = cfg.appearance.overlay_theme == config::OverlayTheme::Dark; 64 65 let is_enforced = active_profile(&cfg).mode == BreakMode::Enforced; 66 + let sound_enabled = cfg.appearance.sound_enabled; 67 + let sound_volume = cfg.appearance.sound_volume; 65 68 66 69 let overlay = Rc::new(RefCell::new(OverlayManager::new(is_dark)?)); 67 70 let settings_mgr = Rc::new(SettingsManager::new(&cfg)?); ··· 149 152 let dur = sched.break_duration; 150 153 *break_start.borrow_mut() = Some((Instant::now(), dur)); 151 154 155 + if sound_enabled { 156 + sound::play_chime(sound_volume); 157 + } 158 + 152 159 let ov = overlay.borrow(); 153 160 ov.window().set_break_label(sched.label.as_str().into()); 154 161 ov.window().set_snooze_visible(!is_enforced); ··· 159 166 } 160 167 TimerEvent::Resumed => { 161 168 *break_start.borrow_mut() = None; 169 + tray.set_paused(false); 170 + tray.set_tooltip("ioma"); 162 171 let ov = overlay.borrow(); 163 172 ov.window().window().set_fullscreen(false); 164 173 ov.window().hide().unwrap_or_default(); 165 174 } 166 - TimerEvent::SnoozePeriodStarting { .. } => { 175 + TimerEvent::SnoozePeriodStarting { remaining_secs } => { 167 176 *break_start.borrow_mut() = None; 177 + tray.set_tooltip(&format!( 178 + "ioma — snoozed, break in {}", 179 + fmt_countdown(remaining_secs) 180 + )); 168 181 let ov = overlay.borrow(); 169 182 ov.window().window().set_fullscreen(false); 170 183 ov.window().hide().unwrap_or_default(); 171 184 } 172 185 TimerEvent::WorkTick { secs_until_break } => { 186 + tray.set_paused(false); 173 187 tray.set_tooltip(&format!( 174 - "ioma — next break in {}", 188 + "ioma — {} — next break in {}", 189 + active_profile_name, 175 190 fmt_countdown(secs_until_break) 176 191 )); 177 192 } 178 193 TimerEvent::Paused => { 179 194 tray.set_paused(true); 195 + tray.set_tooltip("ioma — paused"); 180 196 } 181 197 } 182 198 }
+40
src/sound.rs
··· 1 + use std::time::Duration; 2 + 3 + use rodio::{OutputStream, Sink, Source}; 4 + 5 + /// Play a short two-tone chime at the given volume (0.0–1.0). 6 + /// Spawns a background thread so it never blocks the UI. 7 + pub fn play_chime(volume: f32) { 8 + std::thread::Builder::new() 9 + .name("ioma-chime".into()) 10 + .spawn(move || { 11 + let Ok((_stream, handle)) = OutputStream::try_default() else { 12 + log::debug!("No audio output device available"); 13 + return; 14 + }; 15 + let Ok(sink) = Sink::try_new(&handle) else { 16 + return; 17 + }; 18 + sink.set_volume(volume.clamp(0.0, 1.0)); 19 + 20 + // Two sine tones: high then slightly lower, each 180 ms, 15 ms silence between. 21 + sink.append(tone(880.0, 180)); 22 + sink.append(silence(15)); 23 + sink.append(tone(660.0, 240)); 24 + 25 + sink.sleep_until_end(); 26 + }) 27 + .ok(); 28 + } 29 + 30 + fn tone(freq: f32, millis: u64) -> impl Source<Item = f32> + Send { 31 + rodio::source::SineWave::new(freq) 32 + .take_duration(Duration::from_millis(millis)) 33 + .amplify(0.35) 34 + .fade_in(Duration::from_millis(8)) 35 + } 36 + 37 + fn silence(millis: u64) -> impl Source<Item = f32> + Send { 38 + rodio::source::Zero::<f32>::new(1, 44100) 39 + .take_duration(Duration::from_millis(millis)) 40 + }