aboutsummaryrefslogtreecommitdiff
path: root/src/sound.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 02:57:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 02:57:52 +0300
commit55c138263e190a1e79d4b997bfb9397306c0ec1f (patch)
tree440e1cca113be67132a6617b4282dc5c1147950d /src/sound.rs
parent951fb1d88be721461783a239c152cf143d741dc4 (diff)
feat: support custom mp3 notification sound overrides
Diffstat (limited to 'src/sound.rs')
-rw-r--r--src/sound.rs58
1 files changed, 45 insertions, 13 deletions
diff --git a/src/sound.rs b/src/sound.rs
index aa6fffe6..a156349b 100644
--- a/src/sound.rs
+++ b/src/sound.rs
@@ -4,7 +4,10 @@
//! Uses afplay (macOS) or paplay/aplay (Linux) — no Rust audio dependencies.
use std::io::Write;
-use std::process::Command;
+use std::path::Path;
+use std::process::{Command, Output};
+
+use tracing::warn;
static SOUND_DONE: &[u8] = include_bytes!("../assets/sounds/done.mp3");
static SOUND_REQUEST: &[u8] = include_bytes!("../assets/sounds/request.mp3");
@@ -20,16 +23,37 @@ pub enum Sound {
/// Play a notification sound in a background thread.
/// Silently does nothing if no audio player is available.
-pub fn play(sound: Sound) {
+pub fn play(sound: Sound, config: &crate::config::SoundConfig) {
+ let custom_path = config.path_for(sound);
std::thread::spawn(move || {
+ if let Some(path) = custom_path {
+ match play_file(&path) {
+ Ok(()) => return,
+ Err(err) => {
+ warn!(path = %path.display(), sound = ?sound, err = %err, "custom sound playback failed, falling back to built-in sound")
+ }
+ }
+ }
+
let data = match sound {
Sound::Done => SOUND_DONE,
Sound::Request => SOUND_REQUEST,
};
- let _ = play_bytes(data);
+
+ if let Err(err) = play_bytes(data) {
+ warn!(sound = ?sound, err = %err, "sound playback failed");
+ }
});
}
+fn play_file(path: &Path) -> Result<(), String> {
+ match run_player(path) {
+ Ok(output) if output.status.success() => Ok(()),
+ Ok(output) => Err(format!("player exited with {}", output.status)),
+ Err(err) => Err(err),
+ }
+}
+
fn play_bytes(data: &[u8]) -> Result<(), String> {
// Write to a temp file (audio players need a file path)
let tmp = std::env::temp_dir().join(format!("herdr-sound-{}.mp3", std::process::id()));
@@ -37,21 +61,29 @@ fn play_bytes(data: &[u8]) -> Result<(), String> {
file.write_all(data).map_err(|e| e.to_string())?;
drop(file);
- let result = if cfg!(target_os = "macos") {
- Command::new("afplay").arg(&tmp).output()
- } else {
- // Try paplay (PulseAudio) first, fall back to aplay (ALSA)
- Command::new("paplay")
- .arg(&tmp)
- .output()
- .or_else(|_| Command::new("aplay").arg(&tmp).output())
- };
+ let result = run_player(&tmp);
let _ = std::fs::remove_file(&tmp);
match result {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => Err(format!("player exited with {}", output.status)),
- Err(e) => Err(format!("no audio player available: {e}")),
+ Err(e) => Err(e),
+ }
+}
+
+fn run_player(path: &Path) -> Result<Output, String> {
+ if cfg!(target_os = "macos") {
+ Command::new("afplay")
+ .arg(path)
+ .output()
+ .map_err(|e| format!("no audio player available: {e}"))
+ } else {
+ // Try paplay (PulseAudio) first, fall back to aplay (ALSA)
+ Command::new("paplay")
+ .arg(path)
+ .output()
+ .or_else(|_| Command::new("aplay").arg(path).output())
+ .map_err(|e| format!("no audio player available: {e}"))
}
}