Building a Windows Utility in C#:
Hotkeys, Tray Icons, and Admin Rights

MonoStereoToggle screenshot

The Problem: One Click in Windows Settings, Every Single Day

As a video editor who reviews audio mixes, switching between stereo and mono is something that happens dozens of times a day. Windows has a built-in mono audio toggle — it's in Accessibility Settings, buried three clicks deep. Every time you want to check how your mix sounds in mono (which is essential for broadcast and podcast delivery), you navigate to Settings → Accessibility → Audio → Mono Audio. Then you switch back to stereo. Then mono again. Multiply by thirty times a day.

The fix was obvious: a keyboard shortcut. The implementation required Claude Code to go somewhere it had never been — a native Windows desktop application in C#.

The result was MonoStereoToggle: a system tray utility that toggles Windows mono/stereo audio with a single customizable keyboard shortcut. No UI to open, no windows to close. Just press the hotkey, the tray icon updates, a toast notification confirms the change, and you're back to editing.

⌨️
Global Hotkey
Custom keyboard shortcut, works even when another app is focused
🔔
Tray Icon
Shows current audio mode at a glance, right-click to configure
🖥️
Multi-Device
Supports multiple audio output devices independently
🚀
No Install
Single .exe file, runs without installation, startup optional
"Three clicks in Settings, thirty times a day. One keyboard shortcut fixed six months of frustration."

Why C# and Not Another Electron App?

The BeatMarker plugins are built with JavaScript — web technologies running inside Adobe's plugin runtimes. Why not build MonoStereoToggle as an Electron app or a web-based utility? Two reasons.

First, global hotkeys. Registering a keyboard shortcut that works regardless of which application is focused requires Windows API calls — specifically RegisterHotKey() from user32.dll. Electron can call native code, but the plumbing is awkward. C# makes it straightforward.

Second, the audio API. Toggling the Windows mono audio setting requires the Windows Core Audio COM API — specifically the IMMDeviceEnumerator and the IAccessibilityServiceHandler interfaces that control accessibility audio settings. These are native Windows APIs that are directly accessible from C# via P/Invoke and COM interop, with no intermediary required.

The stack: C# 12 + .NET 8 + WinForms. WinForms might seem like a dated choice in 2026, but for a system tray utility with no UI beyond a tray icon and a context menu, it's the right tool: fast startup, tiny executable, full Windows API access, no framework overhead.

Global Hotkeys: RegisterHotKey and the Message Loop

Windows global hotkeys work through the Win32 message loop. When you register a hotkey with RegisterHotKey(), the operating system sends a WM_HOTKEY message to your application's window procedure whenever that key combination is pressed — even if a different application has focus.

// P/Invoke declarations for Win32 hotkey API
[DllImport("user32.dll")]
static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

[DllImport("user32.dll")]
static extern bool UnregisterHotKey(IntPtr hWnd, int id);

const int HOTKEY_ID = 1;
const uint MOD_CTRL = 0x0002;
const uint MOD_ALT  = 0x0001;

// Register Ctrl+Alt+M as the default hotkey
RegisterHotKey(this.Handle, HOTKEY_ID, MOD_CTRL | MOD_ALT, (uint)Keys.M);

// Handle the hotkey in WndProc
protected override void WndProc(ref Message m) {
  const int WM_HOTKEY = 0x0312;
  if (m.Msg == WM_HOTKEY && m.WParam.ToInt32() == HOTKEY_ID) {
    ToggleMonoStereo();
  }
  base.WndProc(ref m);
}

The hotkey is configurable. The user can right-click the tray icon, choose "Set Shortcut," press any key combination, and the new binding is saved to a settings file and re-registered. If another application has already claimed the same hotkey combination, Windows returns false from RegisterHotKey and the app shows an error.

Toggling Mono: The Windows Accessibility API

The Windows mono audio setting lives in the Accessibility APIs — the same ones that power screen readers and other assistive technology. To read and write it programmatically, Claude Code had to interface with the Windows Core Audio COM interfaces.

The key insight: the mono/stereo setting is per-device and stored as an audio endpoint property. Claude Code accesses it via the IMMDeviceEnumerator COM interface to enumerate audio endpoints, then reads/writes the accessibility property:

// Toggle mono audio for the default audio output device
void ToggleMonoStereo() {
  bool currentlyMono = IsMonoEnabled();
  SetMonoAudio(!currentlyMono);
  UpdateTrayIcon(!currentlyMono);
  ShowNotification(!currentlyMono ? "Mono" : "Stereo");
}

// Reading the current state
bool IsMonoEnabled() {
  // Query Windows Audio Accessibility settings
  // Returns true if mono mixing is enabled for the default endpoint
  using var key = Registry.CurrentUser.OpenSubKey(
    @"Software\Microsoft\Multimedia\Audio");
  return key?.GetValue("EnableMonoMixing") is int val && val == 1;
}

// Writing the new state
void SetMonoAudio(bool enable) {
  using var key = Registry.CurrentUser.CreateSubKey(
    @"Software\Microsoft\Multimedia\Audio");
  key.SetValue("EnableMonoMixing", enable ? 1 : 0, RegistryValueKind.DWord);

  // Notify the audio system of the change
  SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
    "ImmersiveColorSet", SMTO_ABORTIFHUNG, 5000, out _);
}
⚠️ Registry Write Writing to HKCU\Software\Microsoft\Multimedia\Audio requires no admin elevation — it's in the current user's hive. The change needs to be broadcast to the system via WM_SETTINGCHANGE to take effect immediately without a reboot.

Admin Elevation: Asking Once, Never Again

MonoStereoToggle requests administrator privileges on first launch. Not because the audio toggle requires it — that's in HKCU, no elevation needed. Elevation is needed for the optional startup integration: adding the app to the Windows startup registry key (HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run) requires writing to HKLM, which requires admin rights.

The application manifest requests elevation:

<!-- app.manifest -->
<requestedExecutionLevel
  level="requireAdministrator"
  uiAccess="false" />

Windows shows the UAC prompt once on first launch. After that, the app remembers the startup preference and doesn't prompt again. For a system tray utility that users want to set-and-forget, this is the right trade-off: one prompt on setup, zero friction after that.

"Admin prompt on first launch. After that, it just works. That's the right balance for a utility."

The System Tray: Persistent, Minimal, Informative

The system tray icon serves two purposes: it shows the current audio state at a glance (different icons for mono vs. stereo), and it provides a right-click context menu for configuration. The app has no main window — the tray is the entire UI.

// System tray setup with WinForms NotifyIcon
var trayIcon = new NotifyIcon {
  Icon = GetIconForCurrentState(),
  Visible = true,
  Text = "MonoStereoToggle"
};

var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("Toggle Mono/Stereo", null, (s, e) => ToggleMonoStereo());
contextMenu.Items.Add("Set Shortcut...",     null, (s, e) => ShowHotkeyDialog());
contextMenu.Items.Add("Start with Windows",  null, (s, e) => ToggleStartup());
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add("Exit", null, (s, e) => Application.Exit());

trayIcon.ContextMenuStrip = contextMenu;
trayIcon.DoubleClick += (s, e) => ToggleMonoStereo();

When the state changes — either via hotkey or via the context menu — a Windows toast notification briefly appears: "Mono" or "Stereo" with a corresponding icon. This gives the user immediate visual feedback even when the tray is hidden behind other system tray icons.

Packaging: One .exe, No Installation

The goal was a single executable that runs without installation. .NET 8 supports self-contained, single-file publishing — the entire runtime is bundled into one .exe. The resulting file is around 12–15 MB, but it requires no .NET installation on the target machine.

dotnet publish -c Release \
  -r win-x64 \
  --self-contained true \
  -p:PublishSingleFile=true \
  -p:EnableCompressionInSingleFile=true \
  -p:DebugType=none

The compression flag reduces the file size significantly. DebugType=none strips debug symbols that would otherwise add several megabytes. The result: a single file that a user downloads, double-clicks, approves the UAC prompt, and the utility is running in their tray immediately.

💡 Self-Contained vs. Framework-Dependent Self-contained publishing requires no .NET installation but produces a larger file (~15 MB). Framework-dependent publishing produces a tiny file (~100 KB) but requires .NET 8 Runtime to be installed. For a utility distributed to non-developer users, self-contained is the right choice — one fewer installation step.

What Claude Code Learned About Windows Development

Building a native Windows utility was a different experience from JavaScript plugin development. The strengths:

The surprises:

"JavaScript for plugins, C# for system utilities. The right tool for the right job — and Claude Code figures it out either way."

The Result

MonoStereoToggle is a single .exe, about 15 MB, that runs on Windows 11, requires no installation, and lets you toggle mono/stereo audio with any keyboard shortcut you choose. It runs in the system tray, shows the current state at a glance, and optionally starts with Windows.

It was built with Claude Code by a video editor who had never written C# before. The entire development — architecture, COM interface research, P/Invoke signatures, packaging — was done through conversation with an AI. That's the recurring theme across all these tools: the technical barrier to building useful software drops dramatically when the implementation is handled by Claude Code and the direction is handled by the person who actually has the problem.

Download and source: github.com/samaBR85/MonoStereoToggle — MIT license, free.