1use std::process::Command;
2#[cfg(target_os = "windows")]
3use std::time::Duration;
4
5use notify_rust::Notification;
6
7fn spawn_sound(f: impl FnOnce() + Send + 'static) {
8 std::thread::spawn(f);
9}
10
11#[derive(Debug, Clone, Copy)]
12pub enum NotifyKind {
13 FocusComplete,
14 BreakComplete,
15 SessionSkipped,
16 Info,
17}
18
19#[cfg(target_os = "windows")]
20fn beep_windows(freq: u32, duration_ms: u32) {
21 let _ = Command::new("powershell")
22 .args([
23 "-NoProfile",
24 "-Command",
25 &format!("[console]::Beep({},{})", freq, duration_ms),
26 ])
27 .output();
28}
29
30#[cfg(target_os = "macos")]
31fn beep_macos() {
32 let _ = Command::new("afplay")
33 .args(["/System/Library/Sounds/Glass.aiff"])
34 .output();
35}
36
37#[cfg(all(unix, not(target_os = "macos")))]
38fn beep_linux() {
39 let _ = Command::new("sh").args(["-c", "printf '\\a'"]).output();
40}
41
42pub fn play_finish() {
43 spawn_sound(|| {
44 #[cfg(target_os = "windows")]
45 {
46 beep_windows(880, 200);
47 std::thread::sleep(Duration::from_millis(120));
48 beep_windows(1175, 350);
49 }
50 #[cfg(target_os = "macos")]
51 {
52 beep_macos();
53 }
54 #[cfg(all(unix, not(target_os = "macos")))]
55 {
56 beep_linux();
57 let _ = Command::new("paplay")
58 .args(["/usr/share/sounds/freedesktop/stereo/complete.oga"])
59 .output();
60 }
61 });
62}
63
64pub fn play_pause() {
65 spawn_sound(|| {
66 #[cfg(target_os = "windows")]
67 {
68 beep_windows(440, 120);
69 }
70 #[cfg(target_os = "macos")]
71 {
72 let _ = Command::new("afplay")
73 .args(["/System/Library/Sounds/Tink.aiff"])
74 .output();
75 }
76 #[cfg(all(unix, not(target_os = "macos")))]
77 {
78 let _ = Command::new("printf").arg("%b").arg("\u{7}").output();
79 }
80 });
81}
82
83pub fn play_start() {
84 spawn_sound(|| {
85 #[cfg(target_os = "windows")]
86 {
87 beep_windows(660, 100);
88 }
89 #[cfg(target_os = "macos")]
90 {
91 let _ = Command::new("afplay")
92 .args(["/System/Library/Sounds/Pop.aiff"])
93 .output();
94 }
95 #[cfg(all(unix, not(target_os = "macos")))]
96 {
97 let _ = Command::new("printf").arg("%b").arg("\u{7}").output();
98 }
99 });
100}
101
102pub fn notify(title: &str, body: &str) {
103 notify_typed(NotifyKind::Info, title, body);
104}
105
106pub fn notify_typed(kind: NotifyKind, title: &str, body: &str) {
107 let title = title.to_string();
108 let body = body.to_string();
109 std::thread::spawn(move || {
110 let mut n = Notification::new();
111 n.summary(&title).body(&body).timeout(8000);
112
113 #[cfg(target_os = "macos")]
114 {
115 let subtitle = match kind {
116 NotifyKind::FocusComplete => "Focus session",
117 NotifyKind::BreakComplete => "Break time",
118 NotifyKind::SessionSkipped => "Session skipped",
119 NotifyKind::Info => "Void",
120 };
121 n.subtitle(subtitle);
122 }
123
124 #[cfg(all(unix, not(target_os = "macos")))]
125 {
126 use notify_rust::Urgency;
127 n.appname("Void");
128 match kind {
129 NotifyKind::FocusComplete => {
130 n.urgency(Urgency::Normal);
131 }
132 NotifyKind::BreakComplete | NotifyKind::SessionSkipped => {
133 n.urgency(Urgency::Low);
134 }
135 NotifyKind::Info => {}
136 }
137 }
138
139 #[cfg(target_os = "windows")]
140 let _ = kind;
141
142 if let Err(e) = n.show() {
143 eprintln!("Void notification error: {e}");
144 }
145 });
146}