-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add Windows sound playback support #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -234,6 +234,14 @@ describe('NotificationPlugin Behavior', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.platform === 'darwin' ? '/System/Library/Sounds/Glass.aiff' : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(fallback).toBeUndefined(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should use system sound for Windows fallback', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.defineProperty(process, 'platform', { value: 'win32' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fallbackCommand = `powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(fallbackCommand).toBe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Sound command construction', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -257,10 +265,14 @@ describe('NotificationPlugin Behavior', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(fallbackCommand).toBe('aplay /path/to/sound.wav'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should have no command for Windows', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should use PowerShell for Windows', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.defineProperty(process, 'platform', { value: 'win32' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasCommand = process.platform === 'darwin' || process.platform === 'linux'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(hasCommand).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const soundPath = 'C:\\Users\\Test\\sound.wav'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const escapedPath = soundPath.replace(/'/g, "''"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(command).toBe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new(\'C:\\Users\\Test\\sound.wav\').Play()"', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+272
to
+275
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |
| }); | |
| it('should escape single quotes in Windows PowerShell command', () => { | |
| Object.defineProperty(process, 'platform', { value: 'win32' }); | |
| const soundPath = "C:\\test's folder\\sound.wav"; | |
| const escapedPath = soundPath.replace(/'/g, "''"); | |
| const command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | |
| expect(command).toBe( | |
| "powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('C:\\\\test''s folder\\\\sound.wav').Play()\"", | |
| ); | |
| }); | |
| it('should handle semicolons in Windows PowerShell command', () => { | |
| Object.defineProperty(process, 'platform', { value: 'win32' }); | |
| const soundPath = 'C:\\test; rm -rf \\sound.wav'; | |
| const escapedPath = soundPath.replace(/'/g, "''"); | |
| const command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | |
| expect(command).toBe( | |
| "powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('C:\\\\test; rm -rf \\\\sound.wav').Play()\"", | |
| ); | |
| }); | |
| it('should handle backticks and dollar signs in Windows PowerShell command', () => { | |
| Object.defineProperty(process, 'platform', { value: 'win32' }); | |
| const soundPath = 'C:\\test`folder\\$sound.wav'; | |
| const escapedPath = soundPath.replace(/'/g, "''"); | |
| const command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | |
| expect(command).toBe( | |
| "powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('C:\\\\test`folder\\\\$sound.wav').Play()\"", | |
| ); | |
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -158,7 +158,10 @@ export const NotificationPlugin: Plugin = async (ctx) => { | |||||||||||||||
| await $`aplay ${soundPath}`; | ||||||||||||||||
| } | ||||||||||||||||
| } else if (process.platform === 'win32') { | ||||||||||||||||
| log.warn('Windows sound playback not yet supported', { soundPath }); | ||||||||||||||||
| // Use PowerShell with SoundPlayer - works on all Windows versions | ||||||||||||||||
| // Escape single quotes in path for PowerShell | ||||||||||||||||
| const escapedPath = soundPath.replace(/'/g, "''"); | ||||||||||||||||
| await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | ||||||||||||||||
|
Comment on lines
+162
to
+164
|
||||||||||||||||
| // Escape single quotes in path for PowerShell | |
| const escapedPath = soundPath.replace(/'/g, "''"); | |
| await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | |
| // Pass the path as an argument to avoid manual escaping and injection risks | |
| await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new($args[0]).Play()" -- ${soundPath}`; |
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The System.Media.SoundPlayer class is available in the default assemblies loaded by PowerShell and doesn't require explicitly loading System.Windows.Forms. The Add-Type -AssemblyName System.Windows.Forms; portion can be removed to simplify the command.
Additionally, use PlaySync() instead of Play() to ensure the sound completes before PowerShell exits. The asynchronous Play() method returns immediately, which may cause the sound to be cut off when the PowerShell process terminates.
| await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('${escapedPath}').Play()"`; | |
| await $`powershell -Command "[System.Media.SoundPlayer]::new('${escapedPath}').PlaySync()"`; |
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Play() method is asynchronous and returns immediately without waiting for the sound to finish. This means the PowerShell process may exit before the sound completes playing, resulting in the sound being cut off or not playing at all.
Use PlaySync() instead to ensure the sound plays completely before the PowerShell process exits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test should verify that
SystemSounds.Exclamation.Play()usesPlaySync()instead ofPlay()to match the correct implementation. The asynchronousPlay()method will cause the sound to be cut off when the PowerShell process exits immediately.