Contribute
Set up Xcode 16, run the test suite, regenerate design tokens, and ship a PR to WolfWave. The open-source Apple Music streamer app.
This guide covers everything you need to contribute to WolfWave.
Prerequisites
- macOS 26.0+ (Tahoe)
- Apple Silicon (M1 or later)
- Xcode 16.0+
- Swift 5.9+
- Command Line Tools:
xcode-select --install
Setup
- Fork and clone the repository
- Copy
apps/native/WolfWave/Config.xcconfig.exampletoapps/native/WolfWave/Config.xcconfig - Set your Twitch Client ID and Discord Client ID in
Config.xcconfig - Open the project:
make open-xcode - Resolve dependencies:
make update-deps
Branding overrides
DOCS_URL, COMMUNITY_DISCORD_URL, and COPYRIGHT_HOLDER control the docs link, community Discord invite, and legal entity shown in About + Monthly Wrap. You can change them at three levels, in priority order:
- Per-machine / fork: set them in
Config.xcconfig. Highest priority. Gitignored, so this only affects your local builds. - Project-wide default: edit the fallback strings in
Core/AppConstants.swift(AppInfo.copyrightHolder,URLs.docs,URLs.communityDiscord). This is the source of truth shipped to every user. Change it here if you want to move the upstream default. - CI:
.github/workflows/build_release.ymlandtest.ymlwrite these keys into the generatedConfig.xcconfigso the builtInfo.plistcarries literal values instead of empty placeholders. Keep them in sync with theAppConstantsdefaults if you change the upstream values.
Each key falls back to the AppConstants default when blank or unset, so leaving them out never breaks a build. To rename the app itself, edit CFBundleDisplayName in Info.plist (or set PRODUCT_NAME in an xcconfig overlay).
Development Commands
| Command | Description |
|---|---|
make build | Debug build |
make clean | Clean build artifacts |
make test | Run unit tests (XCTest + Swift Testing) |
make update-deps | Resolve SwiftPM dependencies |
make open-xcode | Open the Xcode project |
make ci | CI-friendly build |
make prod-build | Release build + DMG in builds/ |
make prod-install | Release build + install to /Applications |
make notarize | Notarize the DMG (requires Developer ID) |
make verify-notarize | Verify notarization of DMG |
Code Quality
This project follows Swift best practices with professional-grade documentation:
- Swift 5.9+ with modern concurrency (async/await)
- SwiftUI for user interfaces
@Observablemacro for ViewModels (migrated fromObservableObject/@Published)- Comprehensive Documentation: DocC-style comments throughout with usage examples
- MARK Sections: All files organized with clear section markers for easy navigation
- Separation of Concerns: Clean architecture across Core / Services / Views / Monitors
- Secure Credential Storage: macOS Keychain for all sensitive data
- ScriptingBridge Integration: Direct Apple Music communication without spawning subprocesses
- Robust Error Handling: Typed errors with localized descriptions
- Type Safety: Strongly typed models for all data structures
- Minimal Dependencies: Native Apple frameworks plus Sparkle for auto-updates
Playback Source
Playback is abstracted behind the PlaybackSource protocol so additional sources can plug in later.
AppleMusicSource. ScriptingBridge + distributed notifications, with a 2-second fallback poll.PlaybackSourceManager. Selects + multiplexes sources, exposesPlaybackSourceDelegatefor track updates.
Twitch Chat Service
The bot is implemented with TwitchChatService using Twitch Helix + EventSub (no IRC).
Features
- EventSub WebSocket: Real-time chat message notifications.
- OAuth Device Code Flow: Secure authentication via
TwitchDeviceAuth. - Token Validation: Automatic token validation on app launch.
- Network Path Monitoring: Reconnects automatically when the network comes back.
- Command System: Extensible bot command architecture via
BotCommandDispatcher.
Usage
- Connect with saved credentials:
joinChannel(broadcasterID:botID:token:clientID:)orconnectToChannel(channelName:token:clientID:) - Send chat messages via Helix:
sendMessage(_:)orsendMessage(_:replyTo:) - Supply current track info for commands: set
getCurrentSongInfoon the service - The service respects
commandsEnabledso you can disable all commands from Settings
Bot Commands
BotCommandDispatcher routes incoming chat messages to registered commands. Concrete commands live in Services/Twitch/Commands/:
TrackInfoCommand. Handles both!song/!currentsong/!nowplayingand!last/!lastsong/!prevsongvia shared fixtures.SongRequestCommand,!songrequest/!sr.QueueCommand/MyQueueCommand. Read-only queue inspection.SkipCommand/HoldCommand/ClearQueueCommand. Mod-only controls.CooldownManagerenforces global + per-user cooldowns; mods bypass automatically.
Song Request Service
SongRequestService orchestrates request flow end-to-end.
SongRequestQueue. Queue with hold mode + Music.app-closed buffering.SongSearchResolver/LinkResolverService. Resolve queries via MusicKit and Apple Music URLs.AppleMusicController. Plays tracks via AppleScript while preserving window focus.SongBlocklist. Block tracks, artists, or albums by ID.
Discord Rich Presence Service
DiscordRPCService communicates with Discord via the local IPC Unix domain socket.
Features
- IPC Socket Client: Connects to Discord's local Unix domain socket (
discord-ipc-{0..9}). - Dynamic Artwork: Fetches album art from the iTunes Search API with caching.
- Auto-reconnect: Automatic reconnection with backoff when Discord restarts.
- Sandbox Compatible: Works within the macOS App Sandbox via SBPL entitlements.
- Thread-safe: Serial
ipcQueueconfinement plusenabledLock.
Usage
- Start the service:
connect(). Discovers and connects to Discord's IPC socket. - Update presence:
updatePresence(track:artist:album:duration:elapsed:isPaused:). Sets the listening activity. PassisPaused: truewhen Music.app reportskPSpso the live ticker is suppressed and the pause badge swaps in. - Clear presence:
clearPresence(). Removes the activity from the user's profile.
Paused-state handling
Discord has no native "paused" activity flag. WolfWave works around it with two changes inside buildActivity when isPaused == true:
- The
timestampsblock is omitted. Withoutstart/endthe Discord client stops the progress ticker instead of marching past the real elapsed value. assets.small_imageis swapped fromapple_musictopauseandassets.small_textbecomes"Paused".
The track text, large image, and buttons stay unchanged so chat can still read what's loaded.
Rich Presence art assets (required upload)
small_image / large_image reference asset names registered against your Discord application. The PNGs live in discord-assets/ and are not bundled with the app. You upload them once via the Discord developer portal.
| Asset name | Used when |
|---|---|
apple_music | Default large_image fallback + small_image "source" badge while playing |
pause | small_image badge that replaces apple_music while paused |
Fork maintainers: if you ship WolfWave with your own DISCORD_CLIENT_ID, you must upload both assets to your Discord application's Rich Presence → Art Assets page before Rich Presence will render correctly. See discord-assets/README.md for the upload steps and asset specs.
WebSocket Server Service
WebSocketServerService runs a local WebSocket server using Network.framework to broadcast now-playing data to stream overlay clients.
Features
- Network.framework
NWListener: Native WebSocket server with auto-ping support. - Multi-client Support: Multiple browser sources or tools can connect simultaneously.
- Progress Broadcasting: 1-second interval timer broadcasts elapsed time during playback.
- Auto-retry: Reconnects the listener after failures with configurable delay.
- Loopback-only: Binds to
127.0.0.1. Never reachable from the network.
Usage
- Enable/disable:
setEnabled(_:). Starts or stops theNWListener. - Update port:
updatePort(_:). Restarts the server on a new port if running (default8765). - Track change:
updateNowPlaying(track:artist:album:duration:elapsed:artworkURL:). Broadcastsnow_playingJSON. - Clear:
clearNowPlaying(). Broadcastsplayback_statewithisPlaying: false.
Widget HTTP Service
WidgetHTTPService is a tiny companion HTTP server that serves the bundled widget.html to OBS.
- Loopback-only: Binds to
127.0.0.1:8766. Never exposed to the network. GET /→200 OKwithwidget.htmlbytes from the app bundle.- All other requests →
404 Not Found. - Lifecycle: Owned by
WebSocketServerService; starts/stops with the WS server.
Building the OBS Widget
The bundled widget.html is not hand-written. It's generated from the
apps/widget/ Tailwind + TypeScript workspace. Source files:
apps/widget/src/widget.html. HTML shell with placeholdersapps/widget/src/widget.css. Tailwind directives + custom transition classesapps/widget/src/widget.ts. Runtime (state, transitions, WS, render)apps/widget/build.ts. Bundles JS, runs Tailwind, inlines everything
The build emits a single self-contained file at
apps/native/WolfWave/Resources/widget.html (committed to the repo so
Xcode-only builds keep working).
When to rebuild
Whenever you touch any of these:
- Anything under
apps/widget/src/** design-system/tokens.json(themes / layouts / colors)apps/widget/tailwind.config.tsorbuild.ts
How rebuilds happen
| Trigger | Mechanism |
|---|---|
Cmd+B in Xcode | Pre-build Run Script phase Build OBS Widget (Tailwind → inline) runs bun run --filter widget build automatically |
| CI (test + release) | Bun setup + widget build runs before every xcodebuild invocation |
| Manual | bun run --filter widget build from the repo root |
The Xcode script is tolerant of missing bun. It exits 0 with a warning so
a fresh clone without the JS toolchain still builds (just with whatever
widget.html is committed).
For the full architecture, message contract, theme system, and transition state machine, see the OBS Widget Architecture page.
Sparkle Updater Service
SparkleUpdaterService wraps the Sparkle framework for auto-updates.
- EdDSA-signed appcast: Every update is verified against the public key in
Info.plist. - DEBUG builds: Automatic checks disabled; manual "Check Now" reads the bundled
dev-appcast.xml. - Release builds: Polls the remote
SUFeedURL. - Homebrew installs: Fully disabled. Updates handled by Homebrew.
Testing
WolfWave includes a comprehensive unit test suite using XCTest + Swift Testing. Run all tests with:
make testOr press Cmd+U in Xcode. The test target (WolfWaveTests) is a hosted unit test bundle that runs inside the app process.
Test Coverage
Tests live in apps/native/WolfWaveTests/ and cover the playback parser, command dispatcher, Twitch + Discord services, WebSocket auth handshake, Keychain wrapper, onboarding state machine, listening history pipeline, and the full Sparkle updater.
For the live pass count and per-file coverage, run make test locally or browse the directory on GitHub. Enumerating files here just rots on every PR.
Adding New Tests
Test files are auto-discovered via PBXFileSystemSynchronizedRootGroup. Just add .swift files to apps/native/WolfWaveTests/. Use @testable import WolfWave to access internal types.
CI/CD
The project includes these GitHub Actions workflows:
- Test (
.github/workflows/test.yml): Runsxcodebuild teston every push and pull request tomain. Automatically creates a placeholderConfig.xcconfigfor builds. - Build & Release (
.github/workflows/build_release.yml): On tag push (v*), builds a signed and notarized DMG in CI, then creates a draft GitHub Release with the DMG attached. The developer reviews the draft and publishes it. - Update Homebrew (
.github/workflows/update_homebrew.yml): After a published Release, auto-bumps the Homebrew cask. - Docs (
.github/workflows/docs.yml): Deploys the Fumadocs site to GitHub Pages.
Required GitHub Secrets
| Secret | Description |
|---|---|
TWITCH_CLIENT_ID | Twitch application Client ID |
DISCORD_CLIENT_ID | Discord application ID |
DEVELOPER_ID_CERT_P12 | Base64-encoded Developer ID certificate |
DEVELOPER_ID_CERT_PASSWORD | Password for the P12 certificate |
APPLE_ID | Apple ID for notarization |
APPLE_TEAM_ID | Apple Developer Team ID |
APPLE_APP_PASSWORD | App-specific password for notarization |
SPARKLE_PRIVATE_KEY | EdDSA private key for appcast signing |
Code signing, notarization, and DMG creation all happen in the release workflow. GITHUB_TOKEN is provided automatically by GitHub Actions.
Triggering a Release
git tag v2.0.0
git push origin v2.0.0This triggers the release workflow, which builds, signs, notarizes, and uploads the DMG to a draft GitHub Release. Once the workflow completes, review the draft and publish it.
For a complete step-by-step release checklist, see PUBLISH.md in the repository root.
Support development
WolfWave is built and maintained by one person. If the project saves you time or earns you viewers, please consider sponsoring. It directly funds development.
Build from Source
Build WolfWave from source on macOS. Xcode 16, Swift 5.9, macOS 26. Configure Twitch + Discord client IDs and run the open-source menu bar app locally in minutes.
Architecture
How WolfWave is built. MVVM + service-oriented Swift. ScriptingBridge → Apple Music, EventSub WebSocket → Twitch, IPC → Discord, WebSocket → overlay.