Skip to content

feat: add Windows sound playback support#178

Open
mekwall wants to merge 1 commit into
pantheon-org:mainfrom
mekwall:feat/windows-sound-playback
Open

feat: add Windows sound playback support#178
mekwall wants to merge 1 commit into
pantheon-org:mainfrom
mekwall:feat/windows-sound-playback

Conversation

@mekwall

@mekwall mekwall commented Feb 15, 2026

Copy link
Copy Markdown

This PR adds full Windows sound playback support to the Warcraft notifications plugin, enabling the plugin to play Warcraft II sounds on Windows systems.

Changes

  • Windows sound playback: Use PowerShell's System.Media.SoundPlayer to play bundled Warcraft II sounds
  • Windows fallback sound: Play system notification sound when bundled files are missing
  • Tests: Added test coverage for Windows command construction

Details

Previously, the plugin only supported macOS (afplay) and Linux (paplay/aplay/canberra-gtk-play). On Windows, it would log a warning and skip sound playback.
This change implements:

  • Primary: powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new('path').Play()"
  • Fallback: powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"
    The plugin now has full cross-platform sound support.

Testing

  • All 28 notification behavior tests pass
  • Tests verify PowerShell command construction and Windows fallback sound

- Add PowerShell SoundPlayer for playing Warcraft sounds on Windows
- Add Windows fallback sound using SystemSounds when files are missing
- Add tests for Windows command construction
Copilot AI review requested due to automatic review settings February 15, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Windows support for playing Warcraft II notification sounds using PowerShell's System.Media.SoundPlayer. Previously, the plugin only supported macOS and Linux, logging a warning and skipping sound playback on Windows.

Changes:

  • Implemented Windows sound playback using PowerShell's System.Media.SoundPlayer with path escaping for single quotes
  • Added Windows system sound fallback using System.Media.SystemSounds.Exclamation
  • Added test coverage for Windows command construction and fallback behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/notification.ts Replaces Windows warning logs with PowerShell commands for playing sounds and system fallback sounds
src/notification.behavior.test.ts Adds tests for Windows PowerShell command construction and system sound fallback

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/notification.ts
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()"`;

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path escaping is incomplete and could lead to command injection vulnerabilities. The current implementation only escapes single quotes, but PowerShell has many other special characters that could break the command context, including backticks (`), dollar signs ($), double quotes ("), and semicolons (;).

For example, a path like C:\test'; Stop-Process -Name powershell; ' would be escaped to C:\test''; Stop-Process -Name powershell; '' which still allows command injection.

Consider using a more robust escaping approach or passing the path as a parameter to avoid injection risks. One safer alternative would be to use the -File parameter with a temporary PowerShell script file, or use proper PowerShell parameter binding.

Suggested change
// 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 uses AI. Check for mistakes.
Comment thread src/notification.ts
// 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()"`;

Copilot AI Feb 15, 2026

Copy link

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.

Suggested change
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 uses AI. Check for mistakes.
Comment thread src/notification.ts
} else if (process.platform === 'win32') {
log.warn('Windows sound playback not yet supported', { soundPath });
log.warn('Primary sound not found, using system sound', { soundPath });
await $`powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"`;

Copilot AI Feb 15, 2026

Copy link

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.

Copilot uses AI. Check for mistakes.
expect(command).toBe(
'powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Media.SoundPlayer]::new(\'C:\\Users\\Test\\sound.wav\').Play()"',
);
});

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test verifies the command construction but doesn't actually test path escaping edge cases that could lead to command injection vulnerabilities. The test uses a simple Windows path without special characters.

Consider adding tests for paths with special PowerShell characters like single quotes, backticks, dollar signs, semicolons, etc. to ensure the escaping logic is robust. For example:

  • Path with single quotes: C:\test's folder\sound.wav
  • Path with semicolons: C:\test; rm -rf \sound.wav
  • Path with backticks: C:\testfolder\sound.wav`
Suggested change
});
});
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()\"",
);
});

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +275
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()"',
);

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test contains the same incorrect assembly reference issue as the implementation. The test should verify the correct PowerShell command without the unnecessary Add-Type -AssemblyName System.Windows.Forms; part, and should use PlaySync() instead of Play().

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +243
const fallbackCommand = `powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"`;
expect(fallbackCommand).toBe(
'powershell -Command "[System.Media.SystemSounds]::Exclamation.Play()"',
);

Copilot AI Feb 15, 2026

Copy link

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() uses PlaySync() instead of Play() to match the correct implementation. The asynchronous Play() method will cause the sound to be cut off when the PowerShell process exits immediately.

Copilot uses AI. Check for mistakes.
@mekwall

mekwall commented Feb 27, 2026

Copy link
Copy Markdown
Author

@copilot open a new pull request to apply changes based on the comments in this thread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants