aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/notify.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-19 16:02:57 +0300
committerIgor Soarez <igor@soarez.org>2026-10-19 16:02:57 +0300
commitab33633ceb9e4bea3e292b2371241797d1587d02 (patch)
tree7b03faf43e5714782e43d582a00626a6f0d852f1 /crates/dinggerd/src/notify.rs
initial dingger: tmux agent watcher with herdr's detection engine
extract detect-core from herdr's src/detect (engine + 22 manifests, tests unchanged) and add the dingger binary: tmux capture-pane watcher, process-tree agent identification, herdr's notification policy, embedded notification sounds, @agent_state write-back, and catalog-based manifest refresh. viewer screens are non-observations per the engine contract; reviewed by a three-model panel (opus 5.5, gpt 6.1 sol, kimi k3) with all confirmed findings applied as deletions.
Diffstat (limited to 'crates/dinggerd/src/notify.rs')
-rw-r--r--crates/dinggerd/src/notify.rs76
1 files changed, 76 insertions, 0 deletions
diff --git a/crates/dinggerd/src/notify.rs b/crates/dinggerd/src/notify.rs
new file mode 100644
index 0000000..9fcfcde
--- /dev/null
+++ b/crates/dinggerd/src/notify.rs
@@ -0,0 +1,76 @@
+//! Notifications and sounds, mirroring herdr's macOS delivery.
+
+use std::process::{Command, Stdio};
+use std::sync::atomic::{AtomicU64, Ordering};
+
+use tracing::{debug, 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);
+ }
+ }
+}
+
+pub fn send_notification(agent_label: &str, kind: &NotificationKind, context: &str) {
+ debug!(agent = agent_label, kind = kind.title(), context, "notifying");
+ let title = format!("{agent_label} {}", kind.title());
+ let notification = mac_notification_sys::Notification::new()
+ .title(title.as_str())
+ .message(context)
+ .send();
+ if let Err(err) = notification {
+ warn!(%err, "failed to deliver macOS notification");
+ }
+}