├── .gitignore ├── .idea ├── .gitignore ├── .name ├── AirPods Sanity.iml ├── codeStyles │ └── codeStyleConfig.xml ├── dbnavigator.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── xcode.xml ├── .vscode └── settings.json ├── AirPods Sanity.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── tobias.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcuserdata │ └── tobias.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── AirPods Sanity (Release).xcscheme │ ├── AirPods Sanity.xcscheme │ └── xcschememanagement.plist ├── AirPods Sanity ├── AirPodsObserver.swift ├── AirPodsSanityApp.swift ├── AirPods_Sanity.entitlements ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── appicon.png │ │ ├── icon16.png │ │ └── icon32.png │ └── Contents.json ├── ContentView.swift ├── DevicesObserver.swift ├── Events │ ├── Event.swift │ ├── EventHandlerWrapper.swift │ ├── IDisposable.swift │ └── IInvocable.swift ├── MenuBar.swift ├── Preferences.swift ├── PreferencesFile.swift ├── PreferencesLoader.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── airpods-icon.png └── airpods-icon@2x.png ├── AirPods-Sanity-Info.plist ├── README.md ├── de.lproj └── Localizable.strings └── en.lproj └── Localizable.strings /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | .DS_Store 7 | buildServer.json 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AirPods Sanity -------------------------------------------------------------------------------- /.idea/AirPods Sanity.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dbnavigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/xcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": { 3 | "comments": "off", 4 | "strings": "off", 5 | "other": "off" 6 | } 7 | } -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CC3C7243988D2BD3ABE984FE /* EventHandlerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */; }; 11 | CC3C72DF8E2CA00596ACCC72 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C76D635F1C824B78A4181 /* MenuBar.swift */; }; 12 | CC3C751346DA2A1EC6EEFA12 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */; }; 13 | CC3C76F52EBC16FD13B1D764 /* DevicesObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7A1237F3148E067F970D /* DevicesObserver.swift */; }; 14 | CC3C77633D8431061E9F93D3 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C70AF31CDB8C76C345F6D /* Event.swift */; }; 15 | CC3C77FDBEAD90BF9EFE680A /* PreferencesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */; }; 16 | CC3C7CA3469FFA756A55A8EE /* IInvocable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */; }; 17 | CC3C7CDDC79C66F63827F4A6 /* IDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7D5383B08626365DF7DD /* IDisposable.swift */; }; 18 | F63BA8282BEBEA6000AC8EBA /* PreferencesFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */; }; 19 | F6B118522BEB93B400B991EF /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = F6B118512BEB93B400B991EF /* LaunchAtLogin */; }; 20 | F6B9613328B82761001E1D00 /* AirPodsSanityApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */; }; 21 | F6B9613528B82761001E1D00 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B9613428B82761001E1D00 /* ContentView.swift */; }; 22 | F6B9613728B82762001E1D00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F6B9613628B82762001E1D00 /* Assets.xcassets */; }; 23 | F6B9613A28B82762001E1D00 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F6B9613928B82762001E1D00 /* Preview Assets.xcassets */; }; 24 | F6BCD64428BA781500C56ACE /* airpods-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64328BA781500C56ACE /* airpods-icon.png */; }; 25 | F6BCD64628BA782100C56ACE /* airpods-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */; }; 26 | F6BCD64928BA8F5100C56ACE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64B28BA8F5100C56ACE /* Localizable.strings */; }; 27 | F6F5183B28B84239000552D3 /* AirPodsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F5183A28B84239000552D3 /* AirPodsObserver.swift */; }; 28 | F6F5183D28B84B00000552D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F5183C28B84B00000552D3 /* AppDelegate.swift */; }; 29 | F6F5184328B85EF9000552D3 /* SimplyCoreAudio in Frameworks */ = {isa = PBXBuildFile; productRef = F6F5184228B85EF9000552D3 /* SimplyCoreAudio */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | CC3C70AF31CDB8C76C345F6D /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 34 | CC3C76D635F1C824B78A4181 /* MenuBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; }; 35 | CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesLoader.swift; sourceTree = ""; }; 36 | CC3C7A1237F3148E067F970D /* DevicesObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DevicesObserver.swift; sourceTree = ""; }; 37 | CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IInvocable.swift; sourceTree = ""; }; 38 | CC3C7D5383B08626365DF7DD /* IDisposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDisposable.swift; sourceTree = ""; }; 39 | CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventHandlerWrapper.swift; sourceTree = ""; }; 40 | CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 41 | F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesFile.swift; sourceTree = ""; }; 42 | F6B9612F28B82761001E1D00 /* AirPods Sanity.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AirPods Sanity.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPodsSanityApp.swift; sourceTree = ""; }; 44 | F6B9613428B82761001E1D00 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 45 | F6B9613628B82762001E1D00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | F6B9613928B82762001E1D00 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 47 | F6B9613B28B82762001E1D00 /* AirPods_Sanity.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AirPods_Sanity.entitlements; sourceTree = ""; }; 48 | F6BCD64328BA781500C56ACE /* airpods-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "airpods-icon.png"; sourceTree = ""; }; 49 | F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "airpods-icon@2x.png"; sourceTree = ""; }; 50 | F6BCD64A28BA8F5100C56ACE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 51 | F6BCD64C28BA8F9900C56ACE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 52 | F6F5183A28B84239000552D3 /* AirPodsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPodsObserver.swift; sourceTree = ""; }; 53 | F6F5183C28B84B00000552D3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | F6F5184028B85D23000552D3 /* AirPods-Sanity-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = "AirPods-Sanity-Info.plist"; sourceTree = SOURCE_ROOT; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | F6B9612C28B82761001E1D00 /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | F6B118522BEB93B400B991EF /* LaunchAtLogin in Frameworks */, 63 | F6F5184328B85EF9000552D3 /* SimplyCoreAudio in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | CC3C7F472754E14F32187B3D /* Events */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | CC3C7D5383B08626365DF7DD /* IDisposable.swift */, 74 | CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */, 75 | CC3C70AF31CDB8C76C345F6D /* Event.swift */, 76 | CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */, 77 | ); 78 | path = Events; 79 | sourceTree = ""; 80 | }; 81 | F6B9612628B82761001E1D00 = { 82 | isa = PBXGroup; 83 | children = ( 84 | F6BCD64B28BA8F5100C56ACE /* Localizable.strings */, 85 | F6B9613128B82761001E1D00 /* AirPods Sanity */, 86 | F6B9613028B82761001E1D00 /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | F6B9613028B82761001E1D00 /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | F6B9612F28B82761001E1D00 /* AirPods Sanity.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | F6B9613128B82761001E1D00 /* AirPods Sanity */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | F6F5184028B85D23000552D3 /* AirPods-Sanity-Info.plist */, 102 | F6BCD64328BA781500C56ACE /* airpods-icon.png */, 103 | F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */, 104 | F6F5183C28B84B00000552D3 /* AppDelegate.swift */, 105 | F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */, 106 | F6B9613428B82761001E1D00 /* ContentView.swift */, 107 | F6F5183A28B84239000552D3 /* AirPodsObserver.swift */, 108 | F6B9613628B82762001E1D00 /* Assets.xcassets */, 109 | F6B9613B28B82762001E1D00 /* AirPods_Sanity.entitlements */, 110 | F6B9613828B82762001E1D00 /* Preview Content */, 111 | CC3C7A1237F3148E067F970D /* DevicesObserver.swift */, 112 | CC3C7F472754E14F32187B3D /* Events */, 113 | CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */, 114 | F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */, 115 | CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */, 116 | CC3C76D635F1C824B78A4181 /* MenuBar.swift */, 117 | ); 118 | path = "AirPods Sanity"; 119 | sourceTree = ""; 120 | }; 121 | F6B9613828B82762001E1D00 /* Preview Content */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | F6B9613928B82762001E1D00 /* Preview Assets.xcassets */, 125 | ); 126 | path = "Preview Content"; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | F6B9612E28B82761001E1D00 /* AirPods Sanity */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = F6B9613E28B82762001E1D00 /* Build configuration list for PBXNativeTarget "AirPods Sanity" */; 135 | buildPhases = ( 136 | F6B9612B28B82761001E1D00 /* Sources */, 137 | F6B9612C28B82761001E1D00 /* Frameworks */, 138 | F6B9612D28B82761001E1D00 /* Resources */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | ); 144 | name = "AirPods Sanity"; 145 | packageProductDependencies = ( 146 | F6F5184228B85EF9000552D3 /* SimplyCoreAudio */, 147 | F6B118512BEB93B400B991EF /* LaunchAtLogin */, 148 | ); 149 | productName = "AirPods Sanity"; 150 | productReference = F6B9612F28B82761001E1D00 /* AirPods Sanity.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | F6B9612728B82761001E1D00 /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | BuildIndependentTargetsInParallel = 1; 160 | LastSwiftUpdateCheck = 1340; 161 | LastUpgradeCheck = 1530; 162 | ORGANIZATIONNAME = "Tobias Punke"; 163 | TargetAttributes = { 164 | F6B9612E28B82761001E1D00 = { 165 | CreatedOnToolsVersion = 13.4.1; 166 | }; 167 | }; 168 | }; 169 | buildConfigurationList = F6B9612A28B82761001E1D00 /* Build configuration list for PBXProject "AirPods Sanity" */; 170 | compatibilityVersion = "Xcode 13.0"; 171 | developmentRegion = en; 172 | hasScannedForEncodings = 0; 173 | knownRegions = ( 174 | en, 175 | de, 176 | Base, 177 | ); 178 | mainGroup = F6B9612628B82761001E1D00; 179 | packageReferences = ( 180 | F64E7EEC28B8290000B70C49 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */, 181 | F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */, 182 | F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 183 | ); 184 | productRefGroup = F6B9613028B82761001E1D00 /* Products */; 185 | projectDirPath = ""; 186 | projectRoot = ""; 187 | targets = ( 188 | F6B9612E28B82761001E1D00 /* AirPods Sanity */, 189 | ); 190 | }; 191 | /* End PBXProject section */ 192 | 193 | /* Begin PBXResourcesBuildPhase section */ 194 | F6B9612D28B82761001E1D00 /* Resources */ = { 195 | isa = PBXResourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | F6BCD64928BA8F5100C56ACE /* Localizable.strings in Resources */, 199 | F6BCD64628BA782100C56ACE /* airpods-icon@2x.png in Resources */, 200 | F6B9613A28B82762001E1D00 /* Preview Assets.xcassets in Resources */, 201 | F6BCD64428BA781500C56ACE /* airpods-icon.png in Resources */, 202 | F6B9613728B82762001E1D00 /* Assets.xcassets in Resources */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXResourcesBuildPhase section */ 207 | 208 | /* Begin PBXSourcesBuildPhase section */ 209 | F6B9612B28B82761001E1D00 /* Sources */ = { 210 | isa = PBXSourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | F6F5183D28B84B00000552D3 /* AppDelegate.swift in Sources */, 214 | F6B9613528B82761001E1D00 /* ContentView.swift in Sources */, 215 | F6F5183B28B84239000552D3 /* AirPodsObserver.swift in Sources */, 216 | F6B9613328B82761001E1D00 /* AirPodsSanityApp.swift in Sources */, 217 | CC3C76F52EBC16FD13B1D764 /* DevicesObserver.swift in Sources */, 218 | F63BA8282BEBEA6000AC8EBA /* PreferencesFile.swift in Sources */, 219 | CC3C7CDDC79C66F63827F4A6 /* IDisposable.swift in Sources */, 220 | CC3C7243988D2BD3ABE984FE /* EventHandlerWrapper.swift in Sources */, 221 | CC3C77633D8431061E9F93D3 /* Event.swift in Sources */, 222 | CC3C7CA3469FFA756A55A8EE /* IInvocable.swift in Sources */, 223 | CC3C751346DA2A1EC6EEFA12 /* Preferences.swift in Sources */, 224 | CC3C77FDBEAD90BF9EFE680A /* PreferencesLoader.swift in Sources */, 225 | CC3C72DF8E2CA00596ACCC72 /* MenuBar.swift in Sources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXSourcesBuildPhase section */ 230 | 231 | /* Begin PBXVariantGroup section */ 232 | F6BCD64B28BA8F5100C56ACE /* Localizable.strings */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | F6BCD64A28BA8F5100C56ACE /* en */, 236 | F6BCD64C28BA8F9900C56ACE /* de */, 237 | ); 238 | name = Localizable.strings; 239 | sourceTree = ""; 240 | }; 241 | /* End PBXVariantGroup section */ 242 | 243 | /* Begin XCBuildConfiguration section */ 244 | F6B9613C28B82762001E1D00 /* Debug */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | ALWAYS_SEARCH_USER_PATHS = NO; 248 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 249 | CLANG_ANALYZER_NONNULL = YES; 250 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 252 | CLANG_ENABLE_MODULES = YES; 253 | CLANG_ENABLE_OBJC_ARC = YES; 254 | CLANG_ENABLE_OBJC_WEAK = YES; 255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 256 | CLANG_WARN_BOOL_CONVERSION = YES; 257 | CLANG_WARN_COMMA = YES; 258 | CLANG_WARN_CONSTANT_CONVERSION = YES; 259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 268 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 270 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 271 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 272 | CLANG_WARN_STRICT_PROTOTYPES = YES; 273 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 274 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 275 | CLANG_WARN_UNREACHABLE_CODE = YES; 276 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 277 | COPY_PHASE_STRIP = NO; 278 | DEAD_CODE_STRIPPING = YES; 279 | DEBUG_INFORMATION_FORMAT = dwarf; 280 | ENABLE_STRICT_OBJC_MSGSEND = YES; 281 | ENABLE_TESTABILITY = YES; 282 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 283 | GCC_C_LANGUAGE_STANDARD = gnu11; 284 | GCC_DYNAMIC_NO_PIC = NO; 285 | GCC_NO_COMMON_BLOCKS = YES; 286 | GCC_OPTIMIZATION_LEVEL = 0; 287 | GCC_PREPROCESSOR_DEFINITIONS = ( 288 | "DEBUG=1", 289 | "$(inherited)", 290 | ); 291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 292 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 293 | GCC_WARN_UNDECLARED_SELECTOR = YES; 294 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 295 | GCC_WARN_UNUSED_FUNCTION = YES; 296 | GCC_WARN_UNUSED_VARIABLE = YES; 297 | MACOSX_DEPLOYMENT_TARGET = 13.0; 298 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 299 | MTL_FAST_MATH = YES; 300 | ONLY_ACTIVE_ARCH = YES; 301 | SDKROOT = macosx; 302 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 304 | }; 305 | name = Debug; 306 | }; 307 | F6B9613D28B82762001E1D00 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 312 | CLANG_ANALYZER_NONNULL = YES; 313 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 314 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_ENABLE_OBJC_WEAK = YES; 318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 319 | CLANG_WARN_BOOL_CONVERSION = YES; 320 | CLANG_WARN_COMMA = YES; 321 | CLANG_WARN_CONSTANT_CONVERSION = YES; 322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 324 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INFINITE_RECURSION = YES; 328 | CLANG_WARN_INT_CONVERSION = YES; 329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 334 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 335 | CLANG_WARN_STRICT_PROTOTYPES = YES; 336 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 337 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 338 | CLANG_WARN_UNREACHABLE_CODE = YES; 339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 340 | COPY_PHASE_STRIP = NO; 341 | DEAD_CODE_STRIPPING = YES; 342 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 343 | ENABLE_NS_ASSERTIONS = NO; 344 | ENABLE_STRICT_OBJC_MSGSEND = YES; 345 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu11; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | MACOSX_DEPLOYMENT_TARGET = 13.0; 355 | MTL_ENABLE_DEBUG_INFO = NO; 356 | MTL_FAST_MATH = YES; 357 | SDKROOT = macosx; 358 | SWIFT_COMPILATION_MODE = wholemodule; 359 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 360 | }; 361 | name = Release; 362 | }; 363 | F6B9613F28B82762001E1D00 /* Debug */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 368 | CODE_SIGN_ENTITLEMENTS = "AirPods Sanity/AirPods_Sanity.entitlements"; 369 | CODE_SIGN_IDENTITY = "-"; 370 | CODE_SIGN_STYLE = Automatic; 371 | COMBINE_HIDPI_IMAGES = YES; 372 | CURRENT_PROJECT_VERSION = 1.0.4.2; 373 | DEAD_CODE_STRIPPING = YES; 374 | DEVELOPMENT_ASSET_PATHS = "\"AirPods Sanity/Preview Content\""; 375 | DEVELOPMENT_TEAM = 2P9YZ8CQSH; 376 | ENABLE_HARDENED_RUNTIME = YES; 377 | ENABLE_PREVIEWS = YES; 378 | GENERATE_INFOPLIST_FILE = YES; 379 | INFOPLIST_FILE = "AirPods-Sanity-Info.plist"; 380 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 381 | LD_RUNPATH_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "@executable_path/../Frameworks", 384 | ); 385 | MARKETING_VERSION = 1.0; 386 | PRODUCT_BUNDLE_IDENTIFIER = "eu.punke.AirPods-Sanity"; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SWIFT_EMIT_LOC_STRINGS = YES; 389 | SWIFT_VERSION = 5.0; 390 | }; 391 | name = Debug; 392 | }; 393 | F6B9614028B82762001E1D00 /* Release */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 398 | CODE_SIGN_ENTITLEMENTS = "AirPods Sanity/AirPods_Sanity.entitlements"; 399 | CODE_SIGN_IDENTITY = "-"; 400 | CODE_SIGN_STYLE = Automatic; 401 | COMBINE_HIDPI_IMAGES = YES; 402 | CURRENT_PROJECT_VERSION = 1.0.4.2; 403 | DEAD_CODE_STRIPPING = YES; 404 | DEVELOPMENT_ASSET_PATHS = "\"AirPods Sanity/Preview Content\""; 405 | DEVELOPMENT_TEAM = 2P9YZ8CQSH; 406 | ENABLE_HARDENED_RUNTIME = YES; 407 | ENABLE_PREVIEWS = YES; 408 | GENERATE_INFOPLIST_FILE = YES; 409 | INFOPLIST_FILE = "AirPods-Sanity-Info.plist"; 410 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 411 | LD_RUNPATH_SEARCH_PATHS = ( 412 | "$(inherited)", 413 | "@executable_path/../Frameworks", 414 | ); 415 | MARKETING_VERSION = 1.0; 416 | PRODUCT_BUNDLE_IDENTIFIER = "eu.punke.AirPods-Sanity"; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | SWIFT_EMIT_LOC_STRINGS = YES; 419 | SWIFT_VERSION = 5.0; 420 | }; 421 | name = Release; 422 | }; 423 | /* End XCBuildConfiguration section */ 424 | 425 | /* Begin XCConfigurationList section */ 426 | F6B9612A28B82761001E1D00 /* Build configuration list for PBXProject "AirPods Sanity" */ = { 427 | isa = XCConfigurationList; 428 | buildConfigurations = ( 429 | F6B9613C28B82762001E1D00 /* Debug */, 430 | F6B9613D28B82762001E1D00 /* Release */, 431 | ); 432 | defaultConfigurationIsVisible = 0; 433 | defaultConfigurationName = Release; 434 | }; 435 | F6B9613E28B82762001E1D00 /* Build configuration list for PBXNativeTarget "AirPods Sanity" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | F6B9613F28B82762001E1D00 /* Debug */, 439 | F6B9614028B82762001E1D00 /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | /* End XCConfigurationList section */ 445 | 446 | /* Begin XCRemoteSwiftPackageReference section */ 447 | F64E7EEC28B8290000B70C49 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */ = { 448 | isa = XCRemoteSwiftPackageReference; 449 | repositoryURL = "https://github.com/rnine/SimplyCoreAudio.git"; 450 | requirement = { 451 | kind = upToNextMajorVersion; 452 | minimumVersion = 4.0.0; 453 | }; 454 | }; 455 | F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { 456 | isa = XCRemoteSwiftPackageReference; 457 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; 458 | requirement = { 459 | branch = main; 460 | kind = branch; 461 | }; 462 | }; 463 | F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */ = { 464 | isa = XCRemoteSwiftPackageReference; 465 | repositoryURL = "https://github.com/rnine/SimplyCoreAudio.git"; 466 | requirement = { 467 | branch = develop; 468 | kind = branch; 469 | }; 470 | }; 471 | /* End XCRemoteSwiftPackageReference section */ 472 | 473 | /* Begin XCSwiftPackageProductDependency section */ 474 | F6B118512BEB93B400B991EF /* LaunchAtLogin */ = { 475 | isa = XCSwiftPackageProductDependency; 476 | package = F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; 477 | productName = LaunchAtLogin; 478 | }; 479 | F6F5184228B85EF9000552D3 /* SimplyCoreAudio */ = { 480 | isa = XCSwiftPackageProductDependency; 481 | package = F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */; 482 | productName = SimplyCoreAudio; 483 | }; 484 | /* End XCSwiftPackageProductDependency section */ 485 | }; 486 | rootObject = F6B9612728B82761001E1D00 /* Project object */; 487 | } 488 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "309fd4cfd1c6d504fa689a1a6dc9fef048deacc2409e6da59ab5ec38e372b412", 3 | "pins" : [ 4 | { 5 | "identity" : "launchatlogin-modern", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc" 11 | } 12 | }, 13 | { 14 | "identity" : "simplycoreaudio", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/rnine/SimplyCoreAudio.git", 17 | "state" : { 18 | "branch" : "develop", 19 | "revision" : "343d463cffef1f30458d02ce2dc441138e9e0134" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-atomics", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-atomics.git", 26 | "state" : { 27 | "revision" : "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f", 28 | "version" : "0.0.3" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/AirPods Sanity (Release).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 55 | 56 | 57 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/AirPods Sanity.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 56 | 57 | 58 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AirPods Sanity.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | AirPods Sanity (Release).xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AirPods Sanity/AirPodsObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableSCA.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 25.08.22. 6 | // 7 | 8 | import Foundation 9 | import SimplyCoreAudio 10 | 11 | class AirPodsObserver: ObservableObject 12 | { 13 | private let _Preferences: Preferences 14 | private let _Simply: SimplyCoreAudio 15 | private let _NotificationCenter: NotificationCenter 16 | 17 | private var _Observers: [NSObjectProtocol] 18 | 19 | private var _DefaultInputDeviceName: String? 20 | 21 | init() 22 | { 23 | self._Preferences = Preferences.Instance 24 | self._Simply = SimplyCoreAudio() 25 | self._NotificationCenter = NotificationCenter.default 26 | self._Observers = [] 27 | 28 | self.UpdateDefaultInputDevice() 29 | self.AddObservers() 30 | } 31 | 32 | deinit 33 | { 34 | self.RemoveObservers() 35 | } 36 | } 37 | 38 | private extension AirPodsObserver 39 | { 40 | func UpdateDefaultInputDevice() 41 | { 42 | guard let __DefaultInputDevice = self._Simply.defaultInputDevice else { return } 43 | guard let _ = self._Preferences.AirPodsDeviceNames.filter({ $0 == __DefaultInputDevice.name }).first else 44 | { 45 | self._DefaultInputDeviceName = __DefaultInputDevice.name 46 | return 47 | } 48 | 49 | if !self._Preferences.IsEnabled 50 | { 51 | return 52 | } 53 | 54 | guard let __NewInputDeviceName = self._DefaultInputDeviceName != nil ? self._DefaultInputDeviceName : self._Preferences.InputDeviceName else { return } 55 | guard let __InputDevice = self._Simply.allInputDevices.filter({ $0.name == __NewInputDeviceName }).first else { return } 56 | 57 | if __DefaultInputDevice.id != __InputDevice.id 58 | { 59 | self.RemoveObservers() 60 | __InputDevice.isDefaultInputDevice = true 61 | self.AddObservers() 62 | } 63 | 64 | let __Seconds = 10.0 65 | 66 | DispatchQueue.main.asyncAfter(deadline: .now() + __Seconds) 67 | { 68 | guard let __DefaultOutputDevice = self._Simply.defaultOutputDevice else { return } 69 | guard let __SampleRates = __DefaultOutputDevice.nominalSampleRates?.sorted(by: { $0 > $1 }) else { return } 70 | 71 | self.RemoveObservers() 72 | __DefaultOutputDevice.setNominalSampleRate(__SampleRates[0]) 73 | self.AddObservers() 74 | } 75 | } 76 | 77 | func AddObservers() 78 | { 79 | self._Observers.append(contentsOf:[ 80 | self._NotificationCenter.addObserver(forName: .defaultInputDeviceChanged, object: nil, queue: .main) { (_) in 81 | self.UpdateDefaultInputDevice() 82 | }, 83 | ]) 84 | } 85 | 86 | func RemoveObservers() 87 | { 88 | for __Observer in self._Observers 89 | { 90 | self._NotificationCenter.removeObserver(__Observer) 91 | } 92 | 93 | self._Observers.removeAll() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /AirPods Sanity/AirPodsSanityApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirPodsSanityApp.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 25.08.22. 6 | // 7 | // https://sarunw.com/posts/swiftui-menu-bar-app/ 8 | // https://sarunw.com/posts/how-to-make-macos-menu-bar-app/ 9 | // https://github.com/rnine/SimplyCoreAudio 10 | // 11 | 12 | import SwiftUI 13 | 14 | @main 15 | struct AirPodsSanityApp: App 16 | { 17 | @NSApplicationDelegateAdaptor(AppDelegate.self) private var _AppDelegate 18 | 19 | @StateObject private var _AirPodsObserver = AirPodsObserver() 20 | 21 | var body: some Scene 22 | { 23 | WindowGroup 24 | { 25 | ContentView() 26 | .environmentObject(self._AirPodsObserver) 27 | .hidden() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /AirPods Sanity/AirPods_Sanity.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.device.audio-input 10 | 11 | com.apple.security.device.usb 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AirPods Sanity/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 26.08.22. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | import SimplyCoreAudio 11 | 12 | class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject 13 | { 14 | private let NotificationName = Notification.Name("eu.punke.AirPods-Sanity.AppLaunched") 15 | 16 | private let _Preferences: Preferences 17 | 18 | private var _Devices: DevicesObserver! 19 | private var _MenuBar: MenuBar! 20 | private var _Subscription: IDisposable! 21 | 22 | override init() 23 | { 24 | self._Preferences = Preferences.Instance 25 | } 26 | 27 | func applicationDidFinishLaunching(_ notification: Notification) 28 | { 29 | if self.IsAlreadyRunning() 30 | { 31 | self.SendLaunchNotification() 32 | NSApp.terminate(nil) 33 | } 34 | else 35 | { 36 | SetupApplication() 37 | } 38 | } 39 | func applicationDidBecomeActive(_ notification: Notification) 40 | { 41 | } 42 | 43 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool 44 | { 45 | // https://stackoverflow.com/a/59003304/2239781 46 | let __AppleEvent: NSAppleEventDescriptor? = NSAppleEventManager.shared().currentAppleEvent 47 | let __SenderName: String = __AppleEvent?.attributeDescriptor(forKeyword: keyAddressAttr)?.stringValue ?? "" 48 | // let __SenderPID: Int32? = __AppleEvent?.attributeDescriptor(forKeyword: keySenderPIDAttr)?.int32Value ?? 0 49 | 50 | if __SenderName != "Dock" 51 | { 52 | self.PerformSecondLaunchActions() 53 | } 54 | else if !self._Preferences.ShowInDock 55 | { 56 | self.PerformSecondLaunchActions() 57 | } 58 | 59 | return false 60 | } 61 | 62 | func applicationWillTerminate(_ notification: Notification) 63 | { 64 | if self._Subscription != nil 65 | { 66 | self._Subscription.dispose() 67 | } 68 | } 69 | 70 | private func IsAlreadyRunning() -> Bool 71 | { 72 | let __RunningApps = NSWorkspace.shared.runningApplications 73 | let __CurrentApp = Bundle.main.bundleIdentifier! 74 | 75 | return __RunningApps.filter { $0.bundleIdentifier == __CurrentApp }.count > 1 76 | } 77 | 78 | private func SendLaunchNotification() 79 | { 80 | DistributedNotificationCenter.default().post(name: NotificationName, object: nil) 81 | } 82 | 83 | private func SetupLaunchNotificationListener() 84 | { 85 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(HandleSecondLaunch), name: NotificationName, object: nil) 86 | } 87 | 88 | @objc private func HandleSecondLaunch() 89 | { 90 | self.PerformSecondLaunchActions() 91 | } 92 | 93 | private func PerformSecondLaunchActions() 94 | { 95 | if self._MenuBar.IsVisible 96 | { 97 | return 98 | } 99 | 100 | self._MenuBar.Show() 101 | 102 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) 103 | { 104 | self._MenuBar.Show() 105 | } 106 | 107 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) 108 | { 109 | self._MenuBar.Show() 110 | } 111 | 112 | DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) 113 | { 114 | if !self._Preferences.ShowInMenuBar 115 | { 116 | self._MenuBar.Hide() 117 | } 118 | } 119 | } 120 | 121 | private func SetupApplication() 122 | { 123 | // Hide the window when the application finishes launching 124 | if let window = NSApplication.shared.windows.first 125 | { 126 | window.orderOut(self) 127 | } 128 | 129 | self._MenuBar = MenuBar() 130 | self._Devices = DevicesObserver() 131 | self._Subscription = self._Devices.InputDevicesChanged.addHandler(target: self, handler: AppDelegate.OnInputDevicesChanged) 132 | 133 | self._MenuBar.CreateMenu() 134 | 135 | self.SetupLaunchNotificationListener() 136 | } 137 | 138 | func OnInputDevicesChanged(data: [AudioDevice]) 139 | { 140 | let __IsMenuVisible = self._MenuBar.IsVisible 141 | self._MenuBar.CreateMenu() 142 | 143 | if __IsMenuVisible 144 | { 145 | self._MenuBar.Show() 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "idiom" : "mac", 17 | "size" : "32x32", 18 | "scale" : "1x" 19 | }, 20 | { 21 | "idiom" : "mac", 22 | "size" : "32x32", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "idiom" : "mac", 27 | "size" : "128x128", 28 | "scale" : "1x" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "size" : "128x128", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "size" : "256x256", 38 | "scale" : "1x" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "size" : "256x256", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "512x512", 47 | "idiom" : "mac", 48 | "filename" : "appicon.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "idiom" : "mac", 53 | "size" : "512x512", 54 | "scale" : "2x" 55 | } 56 | ], 57 | "info" : { 58 | "version" : 1, 59 | "author" : "xcode" 60 | } 61 | } -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/AppIcon.appiconset/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/appicon.png -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon16.png -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon32.png -------------------------------------------------------------------------------- /AirPods Sanity/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AirPods Sanity/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 25.08.22. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | struct ContentView: View 12 | { 13 | var body: some View 14 | { 15 | Text("Keep your sanity in check!") 16 | .padding() 17 | } 18 | } 19 | 20 | struct ContentView_Previews: PreviewProvider 21 | { 22 | static var previews: some View 23 | { 24 | ContentView() 25 | .environmentObject(AirPodsObserver()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /AirPods Sanity/DevicesObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableSCA.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 25.08.22. 6 | // 7 | 8 | import Foundation 9 | import SimplyCoreAudio 10 | 11 | class DevicesObserver: ObservableObject 12 | { 13 | let InputDevicesChanged = Event<[AudioDevice]>() 14 | 15 | private let _Simply = SimplyCoreAudio() 16 | private var _Observers = [NSObjectProtocol]() 17 | private let _NotificationCenter = NotificationCenter.default 18 | 19 | init() 20 | { 21 | self.UpdateDefaultInputDevice() 22 | self.UpdateDefaultOutputDevice() 23 | self.UpdateDefaultSystemDevice() 24 | 25 | self.AddObservers() 26 | } 27 | 28 | deinit 29 | { 30 | self.RemoveObservers() 31 | } 32 | } 33 | 34 | internal extension DevicesObserver 35 | { 36 | func UpdateDefaultInputDevice() 37 | { 38 | } 39 | 40 | func UpdateDefaultOutputDevice() 41 | { 42 | } 43 | 44 | func UpdateDefaultSystemDevice() 45 | { 46 | } 47 | 48 | private func OnDeviceListChanged() 49 | { 50 | self.InputDevicesChanged.raise(data: self._Simply.allInputDevices) 51 | } 52 | 53 | func AddObservers() 54 | { 55 | self._Observers.append(contentsOf: [ 56 | self._NotificationCenter.addObserver(forName: .deviceListChanged, object: nil, queue: .main) { (notification) in 57 | self.OnDeviceListChanged() 58 | }, 59 | 60 | self._NotificationCenter.addObserver(forName: .defaultInputDeviceChanged, object: nil, queue: .main) { (_) in 61 | self.UpdateDefaultInputDevice() 62 | }, 63 | 64 | self._NotificationCenter.addObserver(forName: .defaultOutputDeviceChanged, object: nil, queue: .main) { (_) in 65 | self.UpdateDefaultOutputDevice() 66 | }, 67 | 68 | self._NotificationCenter.addObserver(forName: .defaultSystemOutputDeviceChanged, object: nil, queue: .main) { (_) in 69 | self.UpdateDefaultSystemDevice() 70 | }, 71 | 72 | self._NotificationCenter.addObserver(forName: .deviceNominalSampleRateDidChange, object: nil, queue: .main) { (notification) in 73 | }, 74 | 75 | self._NotificationCenter.addObserver(forName: .deviceClockSourceDidChange, object: nil, queue: .main) { (notification) in 76 | }, 77 | ]) 78 | } 79 | 80 | func RemoveObservers() 81 | { 82 | for __Observer in self._Observers 83 | { 84 | self._NotificationCenter.removeObserver(__Observer) 85 | } 86 | 87 | self._Observers.removeAll() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /AirPods Sanity/Events/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | public class Event 8 | { 9 | public typealias EventHandler = (T) -> () 10 | 11 | var eventHandlers = [IInvocable]() 12 | 13 | public func raise(data: T) 14 | { 15 | for handler in self.eventHandlers 16 | { 17 | handler.invoke(data: data) 18 | } 19 | } 20 | 21 | public func addHandler(target: U, handler: @escaping (U) -> EventHandler) -> IDisposable 22 | { 23 | let wrapper = EventHandlerWrapper(target: target, handler: handler, event: self) 24 | eventHandlers.append(wrapper) 25 | return wrapper 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /AirPods Sanity/Events/EventHandlerWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | class EventHandlerWrapper : IInvocable, IDisposable 8 | { 9 | weak var target: T? 10 | let handler: (T) -> (U) -> () 11 | let event: Event 12 | 13 | init(target: T?, handler: @escaping (T) -> (U) -> (), event: Event) 14 | { 15 | self.target = target 16 | self.handler = handler 17 | self.event = event; 18 | } 19 | 20 | func invoke(data: Any) -> () 21 | { 22 | if let t = target 23 | { 24 | handler(t)(data as! U) 25 | } 26 | } 27 | 28 | func dispose() 29 | { 30 | event.eventHandlers = event.eventHandlers.filter 31 | { 32 | $0 !== self 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AirPods Sanity/Events/IDisposable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol IDisposable 8 | { 9 | func dispose() 10 | } 11 | -------------------------------------------------------------------------------- /AirPods Sanity/Events/IInvocable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol IInvocable: AnyObject 8 | { 9 | func invoke(data: Any) 10 | } 11 | -------------------------------------------------------------------------------- /AirPods Sanity/MenuBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import AppKit 6 | import Foundation 7 | import LaunchAtLogin 8 | import SimplyCoreAudio 9 | 10 | class MenuBar 11 | { 12 | private let _SimplyCoreAudio: SimplyCoreAudio 13 | private let _Preferences: Preferences 14 | 15 | private var _InputDeviceItems: [NSMenuItem] 16 | private var _OutputDeviceItems: [NSMenuItem] 17 | 18 | private var _StatusBarItem: NSStatusItem 19 | 20 | init() 21 | { 22 | self._SimplyCoreAudio = SimplyCoreAudio() 23 | self._Preferences = Preferences.Instance 24 | 25 | self._InputDeviceItems = [] 26 | self._OutputDeviceItems = [] 27 | 28 | self._StatusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 29 | 30 | self.CreateStatusItem() 31 | self.SetShowInMenuBar() 32 | self.SetShowInDock() 33 | } 34 | 35 | private func CreateStatusItem() 36 | { 37 | let __Image = NSImage(named: "airpods-icon") 38 | 39 | __Image?.isTemplate = true 40 | 41 | if let __Button = self._StatusBarItem.button 42 | { 43 | __Button.toolTip = NSLocalizedString("MenuBar.ToolTip", comment: "") 44 | 45 | if __Image != nil 46 | { 47 | __Button.image = __Image 48 | } 49 | else 50 | { 51 | __Button.title = NSLocalizedString("MenuBar.ToolTip", comment: "") 52 | } 53 | } 54 | } 55 | 56 | public func CreateMenu() 57 | { 58 | self._InputDeviceItems.removeAll() 59 | self._InputDeviceItems = self.CreateInputDeviceItems(simply: self._SimplyCoreAudio, preferences: self._Preferences) 60 | 61 | self._OutputDeviceItems.removeAll() 62 | self._OutputDeviceItems = self.CreateOutputDeviceItems(simply: self._SimplyCoreAudio, preferences: self._Preferences) 63 | 64 | let __Menu = NSMenu() 65 | 66 | __Menu.addItem(self.CreateIsEnabledItem(preferences: self._Preferences)) 67 | 68 | __Menu.addItem(NSMenuItem.separator()) 69 | __Menu.addItem(self.CreateLaunchOnLoginItem(preferences: self._Preferences)) 70 | __Menu.addItem(self.CreateShowInMenuBarItem(preferences: self._Preferences)) 71 | __Menu.addItem(self.CreateShowInDockItem(preferences: self._Preferences)) 72 | 73 | __Menu.addItem(NSMenuItem.separator()) 74 | self.AddItems(menu: __Menu, items: self._InputDeviceItems, label: NSLocalizedString("MenuBar.InputDevices", comment: "")) 75 | 76 | __Menu.addItem(NSMenuItem.separator()) 77 | self.AddItems(menu: __Menu, items: self._OutputDeviceItems, label: NSLocalizedString("MenuBar.OutputDevices", comment: "")) 78 | 79 | __Menu.addItem(NSMenuItem.separator()) 80 | __Menu.addItem(self.CreateQuitApplicationItem()) 81 | 82 | self.SetShowInMenuBar() 83 | self._StatusBarItem.menu = __Menu 84 | } 85 | 86 | public var IsVisible: Bool 87 | { 88 | get 89 | { 90 | return self._StatusBarItem.isVisible 91 | } 92 | } 93 | 94 | public func Show() 95 | { 96 | self._StatusBarItem.isVisible = true; 97 | } 98 | 99 | public func Hide() 100 | { 101 | self._StatusBarItem.isVisible = false; 102 | } 103 | 104 | private func SetShowInMenuBar() 105 | { 106 | self._StatusBarItem.isVisible = self._Preferences.ShowInMenuBar 107 | } 108 | 109 | private func SetLaunchOnLogin() 110 | { 111 | LaunchAtLogin.isEnabled = self._Preferences.LaunchOnLogin 112 | } 113 | 114 | private func SetShowInDock() 115 | { 116 | let __Preferences = self._Preferences 117 | 118 | if __Preferences.ShowInDock 119 | { 120 | // The application is an ordinary app that appears in the Dock and may 121 | // have a user interface. 122 | NSApp.setActivationPolicy(.regular) 123 | } 124 | else 125 | { 126 | // The application does not appear in the Dock and may not create 127 | // windows or be activated. 128 | NSApp.setActivationPolicy(.prohibited) 129 | } 130 | } 131 | 132 | private func CreateLaunchOnLoginItem(preferences: Preferences) -> NSMenuItem 133 | { 134 | let __MenuItem = NSMenuItem() 135 | 136 | __MenuItem.title = NSLocalizedString("MenuBar.LaunchOnLogin", comment: "") 137 | __MenuItem.target = self 138 | __MenuItem.action = #selector(OnToggleLaunchOnLogin(_:)) 139 | 140 | if preferences.LaunchOnLogin 141 | { 142 | __MenuItem.state = NSControl.StateValue.on 143 | } 144 | else 145 | { 146 | __MenuItem.state = NSControl.StateValue.off 147 | } 148 | 149 | return __MenuItem 150 | } 151 | 152 | private func CreateShowInMenuBarItem(preferences: Preferences) -> NSMenuItem 153 | { 154 | let __MenuItem = NSMenuItem() 155 | 156 | __MenuItem.title = NSLocalizedString("MenuBar.ShowInMenuBar", comment: "") 157 | __MenuItem.target = self 158 | __MenuItem.action = #selector(OnToggleShowInMenuBar(_:)) 159 | 160 | if preferences.ShowInMenuBar 161 | { 162 | __MenuItem.state = NSControl.StateValue.on 163 | } 164 | else 165 | { 166 | __MenuItem.state = NSControl.StateValue.off 167 | } 168 | 169 | return __MenuItem 170 | } 171 | 172 | private func CreateShowInDockItem(preferences: Preferences) -> NSMenuItem 173 | { 174 | let __MenuItem = NSMenuItem() 175 | 176 | __MenuItem.title = NSLocalizedString("MenuBar.ShowInDock", comment: "") 177 | __MenuItem.target = self 178 | __MenuItem.action = #selector(OnToggleShowInDock(_:)) 179 | 180 | if preferences.ShowInDock 181 | { 182 | __MenuItem.state = NSControl.StateValue.on 183 | } 184 | else 185 | { 186 | __MenuItem.state = NSControl.StateValue.off 187 | } 188 | 189 | return __MenuItem 190 | } 191 | 192 | private func CreateIsEnabledItem(preferences: Preferences) -> NSMenuItem 193 | { 194 | let __MenuItem = NSMenuItem() 195 | 196 | __MenuItem.title = NSLocalizedString("MenuBar.IsEnabled", comment: "") 197 | __MenuItem.target = self 198 | __MenuItem.action = #selector(OnToggleIsEnabled(_:)) 199 | 200 | if preferences.IsEnabled 201 | { 202 | __MenuItem.state = NSControl.StateValue.on 203 | } 204 | else 205 | { 206 | __MenuItem.state = NSControl.StateValue.off 207 | } 208 | 209 | return __MenuItem 210 | } 211 | 212 | private func CreateQuitApplicationItem() -> NSMenuItem 213 | { 214 | let __QuitLabel = NSLocalizedString("MenuBar.Quit", comment: "") 215 | let __QuitShortcut = NSLocalizedString("MenuBar.QuitShortcut", comment: "") 216 | 217 | return NSMenuItem(title: __QuitLabel, action: #selector(NSApplication.terminate(_:)), keyEquivalent: __QuitShortcut) 218 | } 219 | 220 | private func CreateInputDeviceItems(simply: SimplyCoreAudio, preferences: Preferences) -> [NSMenuItem] 221 | { 222 | let __InputDevices = simply.allInputDevices 223 | var __MenuItems: [NSMenuItem] = [] 224 | 225 | for __AudioDevice in __InputDevices 226 | { 227 | let __MenuItem = NSMenuItem() 228 | 229 | __MenuItem.title = __AudioDevice.name 230 | __MenuItem.target = self 231 | __MenuItem.action = #selector(OnSelectInputDevice(_:)) 232 | __MenuItem.state = NSControl.StateValue.off 233 | 234 | if __AudioDevice.name == preferences.InputDeviceName 235 | { 236 | __MenuItem.state = NSControl.StateValue.on 237 | } 238 | 239 | __MenuItems.append(__MenuItem) 240 | } 241 | 242 | return __MenuItems.sorted(by: { $0.title < $1.title }) 243 | } 244 | 245 | private func CreateOutputDeviceItems(simply: SimplyCoreAudio, preferences: Preferences) -> [NSMenuItem] 246 | { 247 | let __InputDevices = simply.allOutputDevices 248 | var __MenuItems: [NSMenuItem] = [] 249 | 250 | for __AudioDevice in __InputDevices 251 | { 252 | let __MenuItem = NSMenuItem() 253 | 254 | __MenuItem.title = __AudioDevice.name 255 | __MenuItem.target = self 256 | __MenuItem.action = #selector(OnSelectOutputDevice(_:)) 257 | __MenuItem.state = NSControl.StateValue.off 258 | 259 | if preferences.AirPodsDeviceNames.contains(__AudioDevice.name) 260 | { 261 | __MenuItem.state = NSControl.StateValue.on 262 | } 263 | 264 | __MenuItems.append(__MenuItem) 265 | } 266 | 267 | return __MenuItems.sorted(by: { $0.title < $1.title }) 268 | } 269 | 270 | private func AddItems(menu: NSMenu, items: [NSMenuItem], label: String) 271 | { 272 | let __Label = NSMenuItem() 273 | 274 | __Label.title = label 275 | __Label.isEnabled = false 276 | 277 | menu.addItem(__Label) 278 | 279 | for __MenuItem in items 280 | { 281 | menu.addItem(__MenuItem) 282 | } 283 | } 284 | 285 | @objc private func OnToggleLaunchOnLogin(_ sender: NSMenuItem) 286 | { 287 | let __Preferences = self._Preferences 288 | let __State = sender.state 289 | 290 | if __State == NSControl.StateValue.on 291 | { 292 | __Preferences.LaunchOnLogin = false 293 | sender.state = NSControl.StateValue.off 294 | } 295 | else if __State == NSControl.StateValue.off 296 | { 297 | __Preferences.LaunchOnLogin = true 298 | sender.state = NSControl.StateValue.on 299 | } 300 | 301 | self.SetLaunchOnLogin() 302 | 303 | self._Preferences.WriteSettings() 304 | } 305 | 306 | @objc private func OnToggleShowInMenuBar(_ sender: NSMenuItem) 307 | { 308 | let __Preferences = self._Preferences 309 | let __State = sender.state 310 | 311 | if __State == NSControl.StateValue.on 312 | { 313 | __Preferences.ShowInMenuBar = false 314 | sender.state = NSControl.StateValue.off 315 | } 316 | else if __State == NSControl.StateValue.off 317 | { 318 | __Preferences.ShowInMenuBar = true 319 | sender.state = NSControl.StateValue.on 320 | } 321 | 322 | self.SetShowInMenuBar() 323 | 324 | self._Preferences.WriteSettings() 325 | } 326 | 327 | @objc private func OnToggleShowInDock(_ sender: NSMenuItem) 328 | { 329 | let __Preferences = self._Preferences 330 | let __State = sender.state 331 | 332 | if __State == NSControl.StateValue.on 333 | { 334 | __Preferences.ShowInDock = false 335 | sender.state = NSControl.StateValue.off 336 | } 337 | else if __State == NSControl.StateValue.off 338 | { 339 | __Preferences.ShowInDock = true 340 | sender.state = NSControl.StateValue.on 341 | } 342 | 343 | self.SetShowInDock() 344 | 345 | self._Preferences.WriteSettings() 346 | } 347 | 348 | @objc private func OnToggleIsEnabled(_ sender: NSMenuItem) 349 | { 350 | let __Preferences = self._Preferences 351 | let __State = sender.state 352 | 353 | if __State == NSControl.StateValue.on 354 | { 355 | __Preferences.IsEnabled = false 356 | sender.state = NSControl.StateValue.off 357 | } 358 | else if __State == NSControl.StateValue.off 359 | { 360 | __Preferences.IsEnabled = true 361 | sender.state = NSControl.StateValue.on 362 | } 363 | 364 | self._Preferences.WriteSettings() 365 | } 366 | 367 | @objc private func OnSelectInputDevice(_ sender: NSMenuItem) 368 | { 369 | let __Preferences = self._Preferences 370 | let __State = sender.state 371 | 372 | for __Item in self._InputDeviceItems 373 | { 374 | __Item.state = NSControl.StateValue.off 375 | } 376 | 377 | if __State == NSControl.StateValue.on 378 | { 379 | __Preferences.InputDeviceName = nil 380 | } 381 | else if __State == NSControl.StateValue.off 382 | { 383 | __Preferences.InputDeviceName = sender.title 384 | sender.state = NSControl.StateValue.on 385 | } 386 | 387 | self._Preferences.WriteSettings() 388 | } 389 | 390 | @objc private func OnSelectOutputDevice(_ sender: NSMenuItem) 391 | { 392 | let __Preferences = self._Preferences 393 | 394 | if sender.state == NSControl.StateValue.on 395 | { 396 | if __Preferences.AirPodsDeviceNames.contains(sender.title) 397 | { 398 | __Preferences.AirPodsDeviceNames = __Preferences.AirPodsDeviceNames.filter { $0 != sender.title } 399 | } 400 | 401 | sender.state = NSControl.StateValue.off 402 | } 403 | else if sender.state == NSControl.StateValue.off 404 | { 405 | if !__Preferences.AirPodsDeviceNames.contains(sender.title) 406 | { 407 | __Preferences.AirPodsDeviceNames.append(sender.title) 408 | } 409 | 410 | sender.state = NSControl.StateValue.on 411 | } 412 | 413 | self._Preferences.WriteSettings() 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /AirPods Sanity/Preferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | class Preferences 8 | { 9 | private static var _Instance: Preferences? 10 | private let _PreferencesFile: PreferencesFile 11 | 12 | private init() 13 | { 14 | self._PreferencesFile = PreferencesLoader.LoadSettings() 15 | } 16 | 17 | static var Instance: Preferences 18 | { 19 | if _Instance == nil 20 | { 21 | _Instance = Preferences() 22 | } 23 | 24 | return _Instance! 25 | } 26 | 27 | public var LaunchOnLogin: Bool 28 | { 29 | get 30 | { 31 | if let __UnWrapped = self._PreferencesFile.LaunchOnLogin 32 | { 33 | return __UnWrapped 34 | } 35 | else 36 | { 37 | return false 38 | } 39 | } 40 | set(value) 41 | { 42 | self._PreferencesFile.LaunchOnLogin = value 43 | } 44 | } 45 | 46 | public var ShowInMenuBar: Bool 47 | { 48 | get 49 | { 50 | if let __UnWrapped = self._PreferencesFile.ShowInMenuBar 51 | { 52 | return __UnWrapped 53 | } 54 | else 55 | { 56 | return true 57 | } 58 | } 59 | set(value) 60 | { 61 | self._PreferencesFile.ShowInMenuBar = value 62 | } 63 | } 64 | 65 | public var ShowInDock: Bool 66 | { 67 | get 68 | { 69 | if let __UnWrapped = self._PreferencesFile.ShowInDock 70 | { 71 | return __UnWrapped 72 | } 73 | else 74 | { 75 | return false 76 | } 77 | } 78 | set(value) 79 | { 80 | self._PreferencesFile.ShowInDock = value 81 | } 82 | } 83 | 84 | public var IsEnabled: Bool 85 | { 86 | get 87 | { 88 | if let __UnWrapped = self._PreferencesFile.IsEnabled 89 | { 90 | return __UnWrapped 91 | } 92 | else 93 | { 94 | return true 95 | } 96 | } 97 | set(value) 98 | { 99 | self._PreferencesFile.IsEnabled = value 100 | } 101 | } 102 | 103 | public var InputDeviceName: String? 104 | { 105 | get 106 | { 107 | return self._PreferencesFile.InputDeviceName 108 | } 109 | set(value) 110 | { 111 | self._PreferencesFile.InputDeviceName = value 112 | } 113 | } 114 | 115 | public var AirPodsDeviceNames: [String] 116 | { 117 | get 118 | { 119 | if let __UnWrapped = self._PreferencesFile.AirPodsDeviceNames 120 | { 121 | return __UnWrapped 122 | } 123 | else 124 | { 125 | return [] 126 | } 127 | } 128 | set(value) 129 | { 130 | self._PreferencesFile.AirPodsDeviceNames = value 131 | } 132 | } 133 | 134 | public func WriteSettings() 135 | { 136 | PreferencesLoader.WriteSettings(preferences: self._PreferencesFile) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /AirPods Sanity/PreferencesFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesFile.swift 3 | // AirPods Sanity 4 | // 5 | // Created by Tobias Punke on 08.05.24. 6 | // Copyright © 2024 Tobias Punke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class PreferencesFile: Codable 12 | { 13 | var LaunchOnLogin: Bool? 14 | var ShowInMenuBar: Bool? 15 | var ShowInDock: Bool? 16 | var IsEnabled: Bool? 17 | var InputDeviceName: String? 18 | var AirPodsDeviceNames: [String]? 19 | } 20 | -------------------------------------------------------------------------------- /AirPods Sanity/PreferencesLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Tobias Punke on 27.08.22. 3 | // 4 | 5 | import Foundation 6 | 7 | class PreferencesLoader 8 | { 9 | static private var PlistURL: URL 10 | { 11 | let __DocumentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 12 | return __DocumentsPath.appendingPathComponent("settings.plist") 13 | } 14 | 15 | static func LoadSettings() -> PreferencesFile 16 | { 17 | let decoder = PropertyListDecoder() 18 | 19 | guard let __Data = try? Data.init(contentsOf: PlistURL), 20 | let __Preferences = try? decoder.decode(PreferencesFile.self, from: __Data) 21 | else 22 | { 23 | return PreferencesFile() 24 | } 25 | 26 | return __Preferences 27 | } 28 | 29 | static func WriteSettings(preferences: PreferencesFile) 30 | { 31 | let __Encoder = PropertyListEncoder() 32 | 33 | if let __Data = try? __Encoder.encode(preferences) 34 | { 35 | if FileManager.default.fileExists(atPath: PlistURL.path) 36 | { 37 | // Update an existing plist 38 | try? __Data.write(to: PlistURL) 39 | } 40 | else 41 | { 42 | // Create a new plist 43 | FileManager.default.createFile(atPath: PlistURL.path, contents: __Data, attributes: nil) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AirPods Sanity/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AirPods Sanity/airpods-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/airpods-icon.png -------------------------------------------------------------------------------- /AirPods Sanity/airpods-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/airpods-icon@2x.png -------------------------------------------------------------------------------- /AirPods-Sanity-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSUIElement 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirPodsSanity 2 | 3 | > Credit: AirPodsSanity is inspired by [milgra/airpodssoundqualityfixer](https://github.com/milgra/airpodssoundqualityfixer) 4 | 5 | Keeps you from loosing your sanity when using AirPods with a Mac. 6 | 7 | You ever wondered, why the audio quality of your beloved AirPods can get as bad as talking to people over some wire that was built during the Apollo missions took place in the 60s? Ask no further, you came to the right place! 8 | 9 | ## Because... reasons 10 | 11 | The technical reason is simple: Bluetooth has a low bandwidth. So when Apple decided to set your AirPods microphone as the one in charge every. single. time. you connect them to your Mac, things go downhill - fast. Only Steve Jobs in his grave can answer the hard questions: Why, Apple? 12 | 13 | ## What it does 14 | 15 | What this app does is super-duper simple and trivial: Mark one or more output device as "AirPods". Whenever those come online, either the selected input device or the current system input device will be maintained. 16 | 17 | ## How it improves your life 18 | 19 | So what that means is, you can live your life in peace, harmony and appreciate the rainbows and unicorns - once this app is installed. 20 | 21 | ## What it doesn't do 22 | 23 | This piece of software is not cloud-native, has no micro service architecture, did not follow DDD principals, contains an algorithm designed by a fool, can not scale (neither vertically nor horizontally) and abuses your sense of humor. 24 | 25 | ## Features 26 | 27 | - Keeps you healthy 28 | - Makes life better 29 | - Protects your sanity 30 | 31 | ## Roadmap 32 | 33 | - Asking Steve in an upcoming session, why this is even a thing 34 | 35 | # Installation 36 | 37 | #### macOS 38 | 39 | - Download the `.dmg` from the release page: [DMG download](https://github.com/Gaulomatic/AirPodsSanity/releases) 40 | - Run the follwing command in the Terminal or iTerm: `xattr -d com.apple.quarantine "/Applications/AirPods Sanity.app"` 41 | 42 | #### Windows 43 | 44 | - You are out of luck. On the other hand, this issue only applies to macOS, so.... 45 | 46 | #### Linux 47 | 48 | - You are good to go. Essentially. 49 | 50 | 51 | __Please feel free to download, fork and/or provide any feedback!__ -------------------------------------------------------------------------------- /de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | AirPods Sanity 4 | 5 | Created by Tobias Punke on 27.08.22. 6 | 7 | */ 8 | "MenuBar.ToolTip" = "AirPods Sanity"; 9 | "MenuBar.LaunchOnLogin" = "Beim Anmelden ausführen"; 10 | "MenuBar.ShowInMenuBar" = "In der Menüleiste anzeigen"; 11 | "MenuBar.ShowInDock" = "Im Dock anzeigen"; 12 | "MenuBar.IsEnabled" = "Auf bevorzugtes Eingabegerät umschalten"; 13 | "MenuBar.InputDevices" = "Bevorzugtes Eingabegerät:"; 14 | "MenuBar.OutputDevices" = "AirPods-Geräte auswählen:"; 15 | "MenuBar.Quit" = "Beenden"; 16 | "MenuBar.QuitShortcut" = "b"; 17 | -------------------------------------------------------------------------------- /en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | AirPods Sanity 4 | 5 | Created by Tobias Punke on 27.08.22. 6 | 7 | */ 8 | "MenuBar.ToolTip" = "AirPods Sanity"; 9 | "MenuBar.LaunchOnLogin" = "Launch on login"; 10 | "MenuBar.ShowInMenuBar" = "Show in Menubar"; 11 | "MenuBar.ShowInDock" = "Show in Dock"; 12 | "MenuBar.IsEnabled" = "Enable Sanitizer"; 13 | "MenuBar.InputDevices" = "Preferred Input Device:"; 14 | "MenuBar.OutputDevices" = "Which are AirPods?"; 15 | "MenuBar.Quit" = "Quit"; 16 | "MenuBar.QuitShortcut" = "q"; 17 | --------------------------------------------------------------------------------