1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
//! Notifications and sounds, mirroring herdr's macOS delivery.
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::{info, warn};
pub enum NotificationKind {
NeedsAttention,
Finished,
}
impl NotificationKind {
pub fn title(&self) -> &'static str {
match self {
Self::NeedsAttention => "needs attention",
Self::Finished => "finished",
}
}
/// herdr's bundled notification sounds; `request` for blockers, `done`
/// for completions.
fn sound_bytes(&self) -> &'static [u8] {
match self {
Self::NeedsAttention => include_bytes!("../assets/request.mp3"),
Self::Finished => include_bytes!("../assets/done.mp3"),
}
}
}
static SOUND_SEQ: AtomicU64 = AtomicU64::new(0);
pub fn play_sound(kind: &NotificationKind) {
// Sounds are embedded; afplay needs a file, so stage the bytes in the
// system temp dir.
let path = std::env::temp_dir().join(format!(
"dingger-{}-{}.mp3",
std::process::id(),
SOUND_SEQ.fetch_add(1, Ordering::Relaxed)
));
if let Err(err) = std::fs::write(&path, kind.sound_bytes()) {
warn!(%err, "failed to stage notification sound");
return;
}
match Command::new("afplay")
.arg(&path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
// Fire and forget: never let audio latency block the watch loop.
Ok(mut child) => {
std::thread::spawn(move || {
let _ = child.wait();
let _ = std::fs::remove_file(&path);
});
}
Err(err) => {
warn!(%err, "failed to play notification sound");
let _ = std::fs::remove_file(&path);
}
}
}
/// Deliver a macOS notification without blocking the caller: the delivery
/// call waits on Notification Center for seconds, and the watcher must keep
/// updating the UI meanwhile.
pub fn send_notification(agent_label: &str, kind: &NotificationKind, context: &str) {
info!(agent = agent_label, kind = kind.title(), context, "notifying");
let title = format!("{agent_label} {}", kind.title());
let context = context.to_string();
std::thread::spawn(move || {
// Test seam: deliver through a command instead (tests/e2e.sh).
if let Ok(command) = std::env::var("DINGGER_NOTIFIER") {
let _ = std::process::Command::new("sh")
.args(["-c", &command, &title, &context])
.status();
return;
}
let notification = mac_notification_sys::Notification::new()
.title(title.as_str())
.message(context.as_str())
.send();
if let Err(err) = notification {
warn!(%err, "failed to deliver macOS notification");
}
});
}
|