# How OpenSuperWhisper Dynamically Updates Available Microphones in the macOS Status Bar

> Discover how OpenSuperWhisper dynamically updates macOS status bar microphones. Learn about its Combine-based publisher-subscriber pattern for real-time audio device changes.

- Repository: [Starmel/OpenSuperWhisper](https://github.com/Starmel/OpenSuperWhisper)
- Tags: internals
- Published: 2026-07-07

---

**OpenSuperWhisper uses a Combine-based publisher-subscriber pattern where `MicrophoneService` monitors system audio devices via `AVCaptureDevice` notifications, publishes changes to `availableMicrophones`, and triggers `AppDelegate` to rebuild the status bar menu whenever microphones are connected or disconnected.**

OpenSuperWhisper is a macOS transcription application that keeps its status bar menu synchronized with the system's current audio input devices in real-time. The app dynamically updates the list of available microphones using a reactive architecture built on Apple's **Combine** framework and **AVFoundation** notifications. This ensures users always see an accurate, up-to-date device list without requiring manual refreshes or app restarts.

## The Three-Component Architecture

The dynamic status bar menu relies on three tightly-coupled components that separate data management from UI presentation.

### MicrophoneService – The Central Source of Truth

Located in [`OpenSuperWhisper/MicrophoneService.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/MicrophoneService.swift), this service class manages audio device discovery and maintains application state. It exposes two critical `@Published` properties: `availableMicrophones` and `selectedMicrophone`. 

The service registers for system notifications through its `setupDeviceMonitoring()` method, listening for `AVCaptureDeviceWasConnected` and `AVCaptureDeviceWasDisconnected`. When hardware changes occur, `refreshAvailableMicrophones()` queries the current device list and updates the published array, automatically notifying all Combine subscribers.

### AppDelegate – The Status Bar Observer

The `AppDelegate` class in [`OpenSuperWhisper/OpenSuperWhisperApp.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/OpenSuperWhisperApp.swift) hosts the `NSStatusItem` and manages the menu lifecycle. During `applicationDidFinishLaunching`, it calls `setupStatusBarItem()` and establishes a Combine subscription to `MicrophoneService.shared.$availableMicrophones`.

This subscription, stored as an `AnyCancellable` named `microphoneObserver`, executes `rebuildMicrophoneMenu()` whenever the microphone array changes, ensuring the UI always reflects the latest hardware configuration.

### rebuildMicrophoneMenu – The UI Synchronization Method

This private method in `AppDelegate` completely reconstructs the **Microphones** submenu to reflect current system state. It clears existing items using `removeAllItems()`, iterates over `MicrophoneService.shared.availableMicrophones`, and creates `NSMenuItem` instances for each device.

Each menu item uses `device.displayName` as its title and stores the device object in `representedObject`. The currently selected microphone displays a checkmark by setting `item.state = .on` when the device ID matches `selectedMicrophone?.id`.

## Complete Data Flow from Hardware to UI

1. **System Event**: macOS detects a hardware change (USB mic plugged/unplugged) and posts `AVCaptureDeviceWasConnected` or `AVCaptureDeviceWasDisconnected`.
2. **Service Update**: `MicrophoneService` receives the notification, calls `refreshAvailableMicrophones()`, and updates its `@Published var availableMicrophones`.
3. **Publisher Emission**: The Combine framework emits the new array to all subscribers.
4. **Menu Rebuild**: `AppDelegate`'s subscription receives the update on the main run loop and executes `rebuildMicrophoneMenu()`.
5. **UI Refresh**: The status bar menu clears old items and repopulates with current devices, preserving the selected microphone's checkmark state.

## Implementation Details

### Subscribing to Microphone Changes

The observer pattern uses Combine to bridge the service and UI layers:

```swift
private func observeMicrophoneChanges() {
    microphoneObserver = MicrophoneService.shared.$availableMicrophones
        .receive(on: RunLoop.main)
        .sink { [weak self] _ in
            self?.rebuildMicrophoneMenu()
        }
}

```

### Rebuilding the Microphone Submenu

The menu construction handles dynamic creation and state synchronization:

```swift
private func rebuildMicrophoneMenu() {
    // Remove any old microphone items
    microphoneSubmenu?.removeAllItems()

    // Create a fresh submenu if needed
    if microphoneSubmenu == nil {
        microphoneSubmenu = NSMenu(title: "Microphones")
        statusItem?.menu?.addItem(
            NSMenuItem(title: "Microphones", action: nil, keyEquivalent: "")
                .then { $0.submenu = microphoneSubmenu }
        )
    }

    for device in MicrophoneService.shared.availableMicrophones {
        let item = NSMenuItem(title: device.displayName,
                              action: #selector(selectMicrophone(_:)),
                              keyEquivalent: "")
        item.target = self
        item.representedObject = device
        item.state = (device.id == MicrophoneService.shared.selectedMicrophone?.id) ? .on : .off
        microphoneSubmenu?.addItem(item)
    }
}

```

### Handling User Selection

When a user clicks a microphone name, the action updates the service state:

```swift
@objc private func selectMicrophone(_ sender: NSMenuItem) {
    if let device = sender.representedObject as? MicrophoneService.AudioDevice {
        MicrophoneService.shared.selectMicrophone(device)
    }
}

```

## Key Source Files

- **[`OpenSuperWhisper/MicrophoneService.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/MicrophoneService.swift)**: Core discovery logic, `@Published` properties, and `setupDeviceMonitoring()`.
- **[`OpenSuperWhisper/OpenSuperWhisperApp.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/OpenSuperWhisperApp.swift)**: Contains `AppDelegate`, `rebuildMicrophoneMenu()`, and Combine subscription setup.
- **[`OpenSuperWhisper/Utils/AudioUtil.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AudioUtil.swift)**: Helper functions for device classification (e.g., `isBuiltInDevice`).

## Summary

- **Combine Framework**: Enables reactive updates from `MicrophoneService` to `AppDelegate` without tight coupling.
- **AVCaptureDevice Notifications**: System-level events trigger automatic list refreshes via `setupDeviceMonitoring()`.
- **Complete Menu Rebuild**: The `rebuildMicrophoneMenu()` method reconstructs the UI from scratch on every change, ensuring consistency with system state.
- **State Preservation**: Selected microphone checkmarks persist across updates by comparing device IDs against `selectedMicrophone`.

## Frequently Asked Questions

### How does the app detect when a new microphone is plugged in?

`MicrophoneService` registers for `AVCaptureDeviceWasConnected` and `AVCaptureDeviceWasDisconnected` notifications in its `setupDeviceMonitoring()` method. When macOS posts these notifications, the service calls `refreshAvailableMicrophones()` to query the current hardware and update its `@Published` property, triggering a menu rebuild.

### Why does the entire menu rebuild instead of updating individual items?

The `rebuildMicrophoneMenu()` approach clears all existing items and reconstructs the menu from `availableMicrophones`. This ensures the order and content exactly match the system state without complex diffing logic, preventing stale entries when devices are disconnected or renamed.

### What happens if the currently selected microphone is disconnected?

When `refreshAvailableMicrophones()` runs after a disconnection, it updates `availableMicrophones` to exclude the removed device. The Combine subscription fires, `rebuildMicrophoneMenu()` executes, and the menu repopulates without the disconnected mic. The checkmark disappears until the user selects a new available device.

### Is the microphone list updated on a background thread?

Device discovery runs on background threads, but UI updates occur on the main run loop. The subscription uses `.receive(on: RunLoop.main)` to ensure `rebuildMicrophoneMenu()` executes on the main thread, preventing crashes when modifying `NSMenu` items.