diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-19 16:02:57 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-19 16:02:57 +0300 |
| commit | ab33633ceb9e4bea3e292b2371241797d1587d02 (patch) | |
| tree | 7b03faf43e5714782e43d582a00626a6f0d852f1 /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.rs | 76 |
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"); + } +} |
