SaneBar Development Guide

July 2, 2026 · View on GitHub

README · ARCHITECTURE · DEVELOPMENT · PRIVACY · SECURITY

Everything you need to build, test, and change SaneBar. For how the app works internally, read ARCHITECTURE.md.


Quick Start

git clone https://github.com/sane-apps/SaneBar.git
cd SaneBar
./Scripts/SaneMaster.rb verify     # build + run the unit tests
./Scripts/SaneMaster.rb test_mode  # kill -> build -> launch -> stream logs

Requirements:

  • Xcode 16+ (which needs macOS Sequoia or later to build; the app itself runs on macOS 14.0+)
  • Apple Silicon (arm64 only)
  • Ruby 3+ for the helper scripts (macOS ships one; no gems needed)
  • XcodeGen (brew install xcodegen) — only needed when you add or remove source files; the tracked SaneBar.xcodeproj builds as-is

Scripts/SaneMaster.rb is a thin dispatcher: on the original maintainer's machines it delegates to private SaneApps infrastructure; everywhere else it falls back to Scripts/SaneMaster_standalone.rb, which wraps plain xcodebuild. The standalone commands are build, test, verify, test_mode, launch, and help. No wrapper required if you prefer:

xcodebuild -scheme SaneBar -configuration Debug build

Community builds use the Debug configuration (ad-hoc signing). The Release configuration requires the original maintainer's Developer ID and will not sign for anyone else.

Adding or Removing Files

project.yml is the source of truth; the committed .xcodeproj is a convenience snapshot. CI regenerates the project with xcodegen generate before building, so a file added only through Xcode's GUI will silently vanish from the CI build. After adding or removing source files, run:

xcodegen generate

and commit the regenerated project alongside your change.

Things That Have Burned People

Real failures from this repo's history. Learn from them cheaply:

MistakeLesson
Guessed an Accessibility API shapeVerify AX APIs against Apple docs before coding; several "obvious" properties don't exist
Skipped xcodegen generate after adding a file20 minutes of "file not found" — see the section above
Classified Hidden icons as "offscreen"Hidden vs. Visible is separator-relative: compare an icon's X to the separator item's window frame, not to screen bounds
Deleted an "unused" file a tool flaggedIt was load-bearing (ServiceContainer). Grep for the type name before deleting anything
Trusted codesign --verify --deepIt does not inspect executables inside .zip resources; Apple's notarization does — see docs/NOTARIZATION.md
Modified icon-moving logic casuallyThe CGEvent drag pipeline is battle-tested and fragile. Do not touch it without reading docs/DEBUGGING_MENU_BAR_INTERACTIONS.md

Fragile Zones

The icon-moving, geometry, separator/zone, and recovery code paths look refactorable and are not. Before changing anything there, read:

Testing

  • Unit tests live in Tests/ and use Swift Testing (import Testing, @Test, #expect) — write new tests with it, not XCTest.
  • A test must be able to fail for the real bug. No tautologies, no asserting that source code contains a string and calling it coverage.
  • About the RuntimeGuard*XCTests suites: despite the name they mostly fingerprint source code, not runtime behavior — they are "don't delete the fix" tripwires. If one fails after an intentional edit, update the fingerprint to match your change; don't revert blindly. Several of them XCTSkip without the maintainer's private checkout, so skips on a fresh clone and in CI are expected.
  • Manual end-to-end pass: docs/E2E_TESTING_CHECKLIST.md.
  • Map every UI control to its handler: ruby Scripts/button_map.rb.
  • Stream live app logs: ./Scripts/sanebar_logwatch.sh (or log stream --predicate 'subsystem == "com.sanebar.app"' --level info).

Releases (maintainer history)

SaneBar shipped as a ZIP-first direct-download/Sparkle app: the feed at https://sanebar.com/appcast.xml points at dist.sanebar.com/updates/SaneBar-X.Y.Z.zip, signed and notarized with the maintainer's Developer ID. That pipeline retired with the sunset. Practical consequences:

  • Never touch docs/appcast.xml or docs/_redirects in a PR — they drive live auto-updates and downloads for existing users.
  • GitHub Releases mirrors every shipped ZIP and is the permanent archive.
  • A fork that wants auto-updates must generate its own Sparkle EdDSA keys and set its own SUFeedURL; the private key is not in this repo.

Rollback and Current Proof

Historical maintainer rules, kept for reference: a rollback means the appcast, website download route, and dist object set all point at a known-good ZIP — never republish the same version/build. Release-blocking fixes required runtime receipts with completed scenarios proving live main and separator status-item anchors; Summary-only handoff prose is not enough release proof.

Troubleshooting

ProblemFix
"File not found" after adding a filexcodegen generate
Phantom build errorsDelete DerivedData for SaneBar and rebuild
Menu bar item won't render from an unsigned Debug launchUse ./Scripts/SaneMaster.rb test_mode, which launches the way that works