How to Configure Application Preferences in OpenSuperWhisper: A Developer's Guide
OpenSuperWhisper stores all user settings in a singleton AppPreferences object that wraps macOS UserDefaults, exposing values through @UserDefault and @OptionalUserDefault property wrappers for automatic persistence and UI binding.
OpenSuperWhisper is a macOS transcription app that persists user settings using a clean, property-wrapper-based architecture. Whether you are customizing the app through the Settings UI or modifying values programmatically, all configuration flows through the AppPreferences singleton defined in OpenSuperWhisper/Utils/AppPreferences.swift.
Understanding the Preferences Architecture
The preferences system centers on a single source of truth: the AppPreferences class. This singleton manages every user-adjustable setting via type-safe property wrappers that automatically synchronize with UserDefaults.
The AppPreferences Singleton
In OpenSuperWhisper/Utils/AppPreferences.swift, the AppPreferences class creates one shared instance accessible via static let shared. All preference properties are defined on this singleton, ensuring every part of the application reads from and writes to the same underlying storage.
class AppPreferences {
static let shared = AppPreferences()
@UserDefault(key: "selectedEngine", defaultValue: "whisper")
var selectedEngine: String
@UserDefault(key: "whisperLanguage", defaultValue: "en")
var whisperLanguage: String
@OptionalUserDefault(key: "selectedWhisperModelPath")
var selectedWhisperModelPath: String?
}
Property Wrappers: UserDefault vs OptionalUserDefault
OpenSuperWhisper implements two custom property wrappers to handle persistence:
-
@UserDefault<T>– Stores non-optional values with a default fallback. If the key is absent inUserDefaults, it returns the specifieddefaultValue. Implementation is at lines 4-12 ofAppPreferences.swift. -
@OptionalUserDefault<T>– Stores optional values that can benil. When the key is missing, it returnsnilrather than a default. Implementation is at lines 14-22 ofAppPreferences.swift.
These wrappers automatically call UserDefaults.standard.object(forKey:) on read and set(_:forKey:) on write, ensuring immediate persistence without manual synchronization.
Accessing and Modifying Preferences
Any component in the app can read or modify settings by interacting with AppPreferences.shared.
Reading Preferences Programmatically
To read a value, access the property directly on the shared instance:
let currentEngine = AppPreferences.shared.selectedEngine
let modelPath = AppPreferences.shared.selectedWhisperModelPath
let suppressBlank = AppPreferences.shared.suppressBlankAudio
The wrappers guarantee that a value is always available—either the stored value or the default defined in the wrapper declaration.
Writing Preferences Programmatically
To update a setting, assign a new value to the property. The wrapper automatically persists the change to UserDefaults:
AppPreferences.shared.selectedEngine = "fluidaudio"
AppPreferences.shared.fluidAudioModelVersion = "v2"
AppPreferences.shared.modifierOnlyHotkey = "command"
These calls are thread-safe and can be made from anywhere in the application, including background tasks or engine callbacks.
UI Configuration via SettingsViewModel
The SwiftUI interface does not interact with AppPreferences directly. Instead, it uses SettingsViewModel (defined in OpenSuperWhisper/Settings.swift) as a bridge between the UI and persistent storage.
How SettingsViewModel Bridges UI and Storage
The view model mirrors the singleton values using @Published properties. During initialization (lines 64-86), it copies current values from AppPreferences.shared into its own properties:
class SettingsViewModel: ObservableObject {
@Published var selectedEngine: String {
didSet {
AppPreferences.shared.selectedEngine = selectedEngine
}
}
@Published var selectedLanguage: String
@Published var translateToEnglish: Bool
init() {
self.selectedEngine = AppPreferences.shared.selectedEngine
self.selectedLanguage = AppPreferences.shared.whisperLanguage
self.translateToEnglish = AppPreferences.shared.translateToEnglish
}
}
When a user changes a control in the Settings view, the didSet observer immediately writes the value back to AppPreferences.shared, ensuring the singleton and UserDefaults stay synchronized.
Handling Side Effects
Some preference changes require immediate system updates. For example, when the transcription engine changes, the view model triggers a reload:
@Published var selectedEngine: String {
didSet {
AppPreferences.shared.selectedEngine = selectedEngine
Task { @MainActor in
TranscriptionService.shared.reloadEngine()
}
}
}
This pattern ensures that configuration changes in the UI propagate instantly to the active transcription backend.
Preference Migration
OpenSuperWhisper includes a migration strategy for legacy keys. The migrateOldPreferences() method (lines 30-35 in AppPreferences.swift) copies values from old keys to new ones when the app launches.
For example, older versions stored the model path under selectedModelPath. The migration checks for this key and copies the value to selectedWhisperModelPath if the new key is absent:
private func migrateOldPreferences() {
if let oldPath = UserDefaults.standard.string(forKey: "selectedModelPath"),
AppPreferences.shared.selectedWhisperModelPath == nil {
AppPreferences.shared.selectedWhisperModelPath = oldPath
}
}
This ensures seamless upgrades for users updating from earlier versions without losing their configuration.
Adding New Preferences to OpenSuperWhisper
To extend the application with a new setting, follow this three-step pattern:
- Define the wrapper in
AppPreferences.swift:
@UserDefault(key: "enableNoiseReduction", defaultValue: false)
var enableNoiseReduction: Bool
- Expose in
SettingsViewModel:
@Published var enableNoiseReduction: Bool {
didSet {
AppPreferences.shared.enableNoiseReduction = enableNoiseReduction
}
}
// In init():
self.enableNoiseReduction = AppPreferences.shared.enableNoiseReduction
- Bind in
Settings.swift:
Toggle("Noise Reduction", isOn: $viewModel.enableNoiseReduction)
If the new setting replaces a legacy key, add a migration check in migrateOldPreferences() to preserve user data across updates.
Summary
- Centralized Storage: All OpenSuperWhisper preferences live in
AppPreferences.shared, a singleton wrappingUserDefaultslocated inOpenSuperWhisper/Utils/AppPreferences.swift. - Type Safety: Custom
@UserDefaultand@OptionalUserDefaultproperty wrappers provide type-safe access with automatic persistence. - UI Synchronization:
SettingsViewModelbridges SwiftUI controls with the singleton, using@Publishedproperties that write back toAppPreferenceson change. - Legacy Support: The
migrateOldPreferences()method handles key renaming and data migration between app versions. - Extensibility: New settings require only a wrapper property, a view-model publisher, and a UI binding to integrate fully with the existing architecture.
Frequently Asked Questions
Where are OpenSuperWhisper preferences stored on macOS?
OpenSuperWhisper persists all settings via the standard macOS UserDefaults system, accessed through the AppPreferences singleton. The values are stored in the app's sandboxed preferences domain and automatically synchronized by the system. You can access them programmatically via AppPreferences.shared from any file that imports the Utils module.
How do I add a custom setting to OpenSuperWhisper?
To add a custom setting, declare a new property in AppPreferences.swift using either @UserDefault (for non-optional values with defaults) or @OptionalUserDefault (for optional values). Then expose the value as a @Published property in SettingsViewModel, initializing it from the singleton and writing back in didSet. Finally, add a control in Settings.swift bound to the view-model property.
What happens when I change the transcription engine in OpenSuperWhisper settings?
When the user selects a different engine (Whisper or FluidAudio/Parakeet), the SettingsViewModel.selectedEngine setter updates AppPreferences.shared.selectedEngine and immediately calls TranscriptionService.shared.reloadEngine() on the MainActor. This triggers the app to unload the current transcription backend and initialize the newly selected engine with current preference values.
How does OpenSuperWhisper handle legacy preference keys?
The AppPreferences class includes a migrateOldPreferences() method that runs at initialization. It checks for deprecated keys—such as selectedModelPath—and copies their values to the current keys (like selectedWhisperModelPath) if the new key has no value. This ensures users upgrading from older versions retain their settings without manual intervention.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →