aboutsummaryrefslogtreecommitdiff
path: root/src/sound.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 01:36:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 01:36:51 +0300
commitfb56e64537de5f93786ed121f703da2a4105badc (patch)
tree67f6e100141dbee05c75ec421927d62e54d23ec6 /src/sound.rs
parent416827872a6e2c820bd58209d267685d4c07cb7d (diff)
fix: improve windows pane behavior
Diffstat (limited to 'src/sound.rs')
-rw-r--r--src/sound.rs90
1 files changed, 81 insertions, 9 deletions
diff --git a/src/sound.rs b/src/sound.rs
index ab6c11c5..ef45ccc7 100644
--- a/src/sound.rs
+++ b/src/sound.rs
@@ -1,7 +1,8 @@
//! Sound notifications for agent state changes.
//!
//! Embeds mp3 files in the binary and plays them via system audio tools.
-//! Uses afplay (macOS) or decoder-capable Linux audio players — no Rust audio dependencies.
+//! Uses afplay (macOS), Windows MediaPlayer, or decoder-capable Linux audio
+//! players — no Rust audio dependencies.
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -89,23 +90,81 @@ fn temp_sound_path() -> PathBuf {
std::env::temp_dir().join(format!("herdr-sound-{}-{id}.mp3", std::process::id()))
}
+#[cfg(windows)]
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 {
- run_linux_player(path)
- }
+ run_windows_player(path)
+}
+
+#[cfg(target_os = "macos")]
+fn run_player(path: &Path) -> Result<Output, String> {
+ Command::new("afplay")
+ .arg(path)
+ .output()
+ .map_err(|e| format!("no audio player available: {e}"))
+}
+
+#[cfg(not(any(windows, target_os = "macos")))]
+fn run_player(path: &Path) -> Result<Output, String> {
+ run_linux_player(path)
}
+#[cfg(any(windows, test))]
+fn windows_media_player_script() -> &'static str {
+ r#"
+param([string]$Path)
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName PresentationCore
+$resolved = (Resolve-Path -LiteralPath $Path).ProviderPath
+$player = [System.Windows.Media.MediaPlayer]::new()
+$script:done = $false
+$script:failed = $null
+$player.add_MediaEnded({ $script:done = $true })
+$player.add_MediaFailed({
+ param($sender, $eventArgs)
+ $script:failed = $eventArgs.ErrorException
+ $script:done = $true
+})
+$player.Open([Uri]::new($resolved))
+$deadline = [DateTime]::UtcNow.AddSeconds(15)
+while (-not $script:done -and -not $player.NaturalDuration.HasTimeSpan -and [DateTime]::UtcNow -lt $deadline) {
+ Start-Sleep -Milliseconds 25
+}
+if ($script:failed) { throw $script:failed }
+$player.Play()
+while (-not $script:done -and [DateTime]::UtcNow -lt $deadline) {
+ Start-Sleep -Milliseconds 50
+}
+$player.Close()
+if ($script:failed) { throw $script:failed }
+if (-not $script:done) { throw 'sound playback timed out' }
+"#
+}
+
+#[cfg(windows)]
+fn run_windows_player(path: &Path) -> Result<Output, String> {
+ Command::new("powershell.exe")
+ .args([
+ "-NoLogo",
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ windows_media_player_script(),
+ ])
+ .arg(path)
+ .output()
+ .map_err(|e| format!("Windows MediaPlayer playback failed: {e}"))
+}
+
+#[cfg(not(any(windows, target_os = "macos")))]
#[derive(Debug, Clone, Copy)]
struct AudioPlayer {
program: &'static str,
args: &'static [&'static str],
}
+#[cfg(not(any(windows, target_os = "macos")))]
impl AudioPlayer {
fn output(self, path: &Path) -> std::io::Result<Output> {
Command::new(self.program)
@@ -115,6 +174,7 @@ impl AudioPlayer {
}
}
+#[cfg(not(any(windows, target_os = "macos")))]
fn linux_audio_players() -> &'static [AudioPlayer] {
// Do not add bare aplay here. It does not decode MP3 and plays MP3 bytes as raw PCM.
&[
@@ -141,6 +201,7 @@ fn linux_audio_players() -> &'static [AudioPlayer] {
]
}
+#[cfg(not(any(windows, target_os = "macos")))]
fn run_linux_player(path: &Path) -> Result<Output, String> {
let mut errors = Vec::new();
@@ -158,6 +219,7 @@ fn run_linux_player(path: &Path) -> Result<Output, String> {
))
}
+#[cfg(not(any(windows, target_os = "macos")))]
fn player_error(player: AudioPlayer, output: &Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim();
@@ -178,6 +240,7 @@ mod tests {
assert_ne!(temp_sound_path(), temp_sound_path());
}
+ #[cfg(not(any(windows, target_os = "macos")))]
#[test]
fn linux_audio_players_are_mp3_capable() {
let programs: Vec<&str> = linux_audio_players()
@@ -188,4 +251,13 @@ mod tests {
assert_eq!(programs, ["paplay", "pw-play", "ffplay", "mpg123", "mpv"]);
assert!(!programs.contains(&"aplay"));
}
+
+ #[test]
+ fn windows_media_player_script_accepts_literal_path_argument() {
+ let script = windows_media_player_script();
+
+ assert!(script.contains("param([string]$Path)"));
+ assert!(script.contains("Resolve-Path -LiteralPath $Path"));
+ assert!(script.contains("System.Windows.Media.MediaPlayer"));
+ }
}