Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Build and Release

on:
push:
branches: [main]
tags: ['v*']
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x

- name: Build
run: dotnet build AudioPriorityTray.slnx -c Release

- name: Test
run: dotnet test --project tests/AudioPriorityTray.Core.Tests -c Release --no-build

- name: Publish
shell: pwsh
run: |
# Tags set the version (v1.2.3 -> 1.2.3); other builds keep the project's version.
$versionArgs = @()
if ($env:GITHUB_REF_TYPE -eq 'tag') { $versionArgs = @("-p:Version=$($env:GITHUB_REF_NAME.TrimStart('v'))") }

New-Item -ItemType Directory -Force release | Out-Null
foreach ($rid in 'win-x64', 'win-arm64') {
dotnet publish src/AudioPriorityTray -c Release -r $rid -p:PublishSingleFile=true --self-contained false @versionArgs -o "publish/$rid"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Copy-Item "publish/$rid/AudioPriorityTray.exe" "release/AudioTray-$rid.exe"
}

- name: Install Inno Setup
shell: pwsh
run: |
if (-not (Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe")) { choco install innosetup --no-progress -y }

- name: Build installers and Store packages
shell: pwsh
env:
# Partner Center > Product identity, as repository variables. Without them no MSIX is built.
AUDIOPRIORITY_MSIX_IDENTITY_NAME: ${{ vars.AUDIOPRIORITY_MSIX_IDENTITY_NAME }}
AUDIOPRIORITY_MSIX_PUBLISHER: ${{ vars.AUDIOPRIORITY_MSIX_PUBLISHER }}
AUDIOPRIORITY_MSIX_PUBLISHER_DISPLAY_NAME: ${{ vars.AUDIOPRIORITY_MSIX_PUBLISHER_DISPLAY_NAME }}
AUDIOPRIORITY_MSIX_DISPLAY_NAME: ${{ vars.AUDIOPRIORITY_MSIX_DISPLAY_NAME }}
run: |
$packageArgs = @{}
if ($env:GITHUB_REF_TYPE -eq 'tag') { $packageArgs.Version = $env:GITHUB_REF_NAME.TrimStart('v') }
if ($env:AUDIOPRIORITY_MSIX_IDENTITY_NAME) { $packageArgs.Msix = $true }
foreach ($rid in 'win-x64', 'win-arm64') { ./packaging/build.ps1 -Runtime $rid @packageArgs }
Copy-Item artifacts/installer/*.exe release/
Get-ChildItem release, artifacts/msix -ErrorAction SilentlyContinue | Format-Table Name, Length

- uses: actions/upload-artifact@v7
with:
name: AudioTray
path: release/*.exe
if-no-files-found: error

# Unsigned by design: upload to Partner Center, which signs Store packages.
- uses: actions/upload-artifact@v7
with:
name: AudioTray-Store-msix
path: artifacts/msix/*.msix
if-no-files-found: ignore

release:
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/download-artifact@v8
with:
name: AudioTray
path: release

- name: Nightly release
if: github.ref == 'refs/heads/main'
run: |
# One rolling pre-release that always points at the latest main.
gh release delete nightly --cleanup-tag --yes || true
gh release create nightly release/*.exe \
--prerelease \
--target "$GITHUB_SHA" \
--title "Nightly build" \
--notes "Automated build of the latest commit on main ($GITHUB_SHA). AudioTraySetup-*.exe installs per user and includes .NET; the portable AudioTray-win-*.exe needs the [.NET 10 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/10.0)."

- name: Tagged release
if: startsWith(github.ref, 'refs/tags/v')
run: |
# Creating a release in the GitHub UI also pushes its tag, so the release may already
# exist; attach the assets to it instead of failing.
if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" release/*.exe --clobber
else
gh release create "$GITHUB_REF_NAME" release/*.exe \
--verify-tag \
--title "$GITHUB_REF_NAME" \
--generate-notes
fi
74 changes: 0 additions & 74 deletions .github/workflows/release.yml

This file was deleted.

79 changes: 12 additions & 67 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,71 +1,16 @@
# Build output
.build/
build/
dist/
DerivedData/
*.xcodeproj/xcuserdata/
*.xcworkspace/xcuserdata/
*.xcodeproj/project.xcworkspace/xcuserdata/

# Xcode private settings
xcuserdata/

# Build products
*.app
*.ipa
*.dSYM.zip
*.dSYM

# Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
.swiftpm/
Package.resolved
Packages/

# CocoaPods (if used)
Pods/

# Carthage (if used)
Carthage/Build/

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
iOSInjectionProject/

# macOS
.DS_Store
.AppleDouble
.LSOverride
._*

# Thumbnails
Thumbs.db
# .NET build output
bin/
obj/
publish/
artifacts/
TestResults/

# IDE
*.swp
*.swo
*~
.vs/
.idea/
*.iml

# Archives
*.xcarchive
*.user
*.suo

# Provisioning profiles
*.mobileprovision
*.provisionprofile

# Index
Index/

# xcode-build-server files
buildServer.json
.compile
# OS
Thumbs.db
.DS_Store
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# AGENTS.md

Native Windows 11 tray app that keeps the highest-priority connected audio device as the Windows
default. Rebuilt from the macOS AudioPriorityBar (github.com/tobi/AudioPriorityBar); the Swift
sources were removed from this repo, and its git history holds them if the original behavior
needs checking.

## Stack

- C# on .NET 10 (`net10.0-windows`), WPF with the built-in Fluent theme (`Application.ThemeMode="System"`).
- No third-party runtime dependencies. Audio uses Windows Core Audio COM interop directly; default
device switching uses the undocumented but stable `IPolicyConfig` interface.
- Tests: xUnit v3 over the Core library only.

## Layout

- `src/AudioPriorityTray.Core/` — UI-free logic: `AudioManager` (auto-switch policy), `SettingsStore`
(JSON in `%LOCALAPPDATA%\AudioPriorityTray\`), `WindowsAudioDeviceService` + `Interop/` (Core Audio).
- `src/AudioPriorityTray/` — WPF app: tray icon (raw `Shell_NotifyIcon`), Acrylic flyout, view models.
- `tests/AudioPriorityTray.Core.Tests/` — behavior tests against `FakeAudioDeviceService`.

## Commands

```bash
dotnet build AudioPriorityTray.slnx
dotnet test --project tests/AudioPriorityTray.Core.Tests
dotnet run --project src/AudioPriorityTray
dotnet publish src/AudioPriorityTray -c Release -r win-x64 -p:PublishSingleFile=true --self-contained false -o publish
```

## Packaging

`packaging/build.ps1 -Runtime win-x64|win-arm64 [-Version x.y.z] [-Msix ...]` publishes self-contained
into `artifacts/publish/`, compiles `packaging/AudioPriorityTray.iss` (Inno Setup, per-user, fixed
AppId) and optionally packs `packaging/AppxManifest.xml` + `packaging/Assets/` with `makeappx`.

- The installer's startup task writes the same `HKCU\...\Run\AudioPriorityTray` value as the app's
toggle, and uninstall always removes it. The AppId GUID must never change.
- Store rules: the MSIX version's fourth field stays `0` and versions must increase; every
DisplayName must equal the Partner Center reserved name; `runFullTrust` needs a justification in
the submission; the MSIX is uploaded unsigned. Packaged builds can't use the Run key
(`LaunchAtLogin.IsManagedByWindows`); the manifest's opt-in `startupTask` is used instead.

## CI and releases

`.github/workflows/build.yml` builds and tests on every PR and push. Pushes to `main` replace the
rolling `nightly` pre-release; pushing a `v*` tag publishes a versioned release (the tag sets the
version). Assets: per-user installers and portable framework-dependent exes for win-x64 and
win-arm64. The Store MSIX is a build artifact only, produced when the `AUDIOPRIORITY_MSIX_*`
repository variables are set.

## Conventions

- `TreatWarningsAsErrors` is on for every project.
- Core Audio callbacks arrive on worker threads; `WindowsAudioDeviceService` coalesces them and
posts to the UI `SynchronizationContext`. `AudioManager` is single-threaded.
- COM interface declarations must match SDK vtable order; only slots up to the last used method are declared.
- UI follows Windows Design System 1.0.0; `DESIGN.md` records the mapping and deliberate
exceptions. Update it with any new role, deviation, or verified gate.
- Colors come from Fluent theme resource keys via `DynamicResource`, never hard-coded, so light/dark
and the accent color track Windows. A brush with a `DynamicResource` color can't be used from
template triggers (it throws at runtime); use `SystemColors.*BrushKey` or an inline element.
- Only one instance runs (`Local\AudioPriorityTray` mutex). Kill the running tray app before `dotnet run`.
Loading