Skip to content

v1.2.0: fix packaged-app launch crash, global Night Shift toggle, schedule-aware overrides - #11

Merged
adamdexter merged 4 commits into
mainfrom
claude/v1.2.0-crashfix-and-toggle
Jul 3, 2026
Merged

v1.2.0: fix packaged-app launch crash, global Night Shift toggle, schedule-aware overrides#11
adamdexter merged 4 commits into
mainfrom
claude/v1.2.0-crashfix-and-toggle

Conversation

@adamdexter

Copy link
Copy Markdown
Owner

⚠️ Merging this PR auto-releases v1.2.0

The release workflow triggers on the VERSION file change to main (signed + notarized via the existing secrets). Before merging, run the manual regression checklist in CLAUDE.md on real hardware — especially the new cases (menu toggle, schedule firing while an excluded app is focused).

Critical fix: every shipped build crashes on launch

v1.0.0–v1.1.2 (including the signed/notarized v1.1.2) crash instantly on any machine except the dev machine — verified by downloading and running the released DMGs:

Fatal error: could not load resource bundle

Two stacked bugs: the SwiftPM package was still named NightShiftToggle, so the resource bundle didn't match what create-dmg.sh expected — and the script's find couldn't descend the .build/release symlink anyway, silently skipping the copy. Dev machines were masked by a baked-in fallback path to the local checkout. The package rename fixes the name; create-dmg.sh now resolves the bundle by direct path and hard-fails if missing. Verified end-to-end: built the DMG, launched the packaged app.

Features

  • Global Night Shift toggle in the menu bar — same semantics as the System Settings toggle ("Turn Off Until Tomorrow" / "Turn On Until Sunset"). While an excluded app has focus, it only updates the restore intent, so per-app switching keeps working either way.
  • Schedule-aware overrides — the bridge now observes CBBlueLightClient status notifications; if the schedule (or System Settings) turns Night Shift on while an excluded app is focused, it's immediately re-disabled and folded into the restore intent. Previously the display warmed mid-session until the next app switch.

Fixes & infrastructure

  • Quit → "Minimize to Menu Bar" no longer leaves focus monitoring permanently dead
  • FocusMonitor.deinit no longer mutates @Published state during deallocation
  • Bridge: status-fetch dedupe, return-value check, unified [ShiftChange] log prefix
  • State machine hardened against synchronous notification re-entrancy (fake fires handlers synchronously in tests to enforce this)
  • Release workflow: pushes the cask to the live tap repo (adamdexter/homebrew-shiftchange) when a TAP_PUSH_TOKEN secret exists — the tap has been stuck at v1.0.0 because nothing updated it
  • create-dmg.sh: SHIFTCHANGE_SCRATCH_PATH override for iCloud-synced checkouts (documented in CLAUDE.md)
  • 6 new unit tests; suite is 29 tests, all green in CI

Post-merge

  1. Create the TAP_PUSH_TOKEN secret (PAT with write access to the tap repo) so brew users get v1.2.0 — otherwise sync the tap manually.
  2. Delete the temp claude/v1.2.0-ci-check branch (CI-only run from before the workflow-scope fix).

🤖 Generated with Claude Code

adamdexter and others added 4 commits July 3, 2026 00:28
Every distributed build to date (v1.0.0 through the signed v1.1.2)
crashes at launch on any machine except the dev machine, verified by
running the released DMGs directly:

  Fatal error: could not load resource bundle

Two stacked bugs: the SwiftPM package was still named NightShiftToggle,
so the resource bundle was NightShiftToggle_ShiftChange.bundle while
create-dmg.sh looked for ShiftChange_ShiftChange.bundle — and the
script's `find .build/release` couldn't descend into the symlinked
release directory anyway, so the copy was silently skipped regardless.
Without the bundle, Bundle.module hits fatalError during
applicationDidFinishLaunching. Dev machines were masked by the
binary's baked-in fallback path to the local .build checkout.

Changes:
- Package name NightShiftToggle → ShiftChange
  (bundle becomes ShiftChange_ShiftChange.bundle)
- Sources/NightShiftToggle/ → Sources/ShiftChange/,
  NightShiftToggleApp.swift → ShiftChangeApp.swift
- create-dmg.sh: resolve the bundle by direct path and hard-fail if
  missing instead of silently shipping a broken app
- release.yml: update the VERSION path trigger and version check to
  the renamed directory (releases would stop triggering otherwise)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anup

- Choosing "Minimize to Menu Bar" in the quit confirmation no longer
  leaves focus monitoring permanently stopped: quitApp and
  applicationShouldTerminate no longer stop the monitor eagerly;
  teardown happens only in applicationWillTerminate
- FocusMonitor.deinit no longer mutates @published state during
  deallocation (removes the observer and restores directly)
- CBlueLightBridge: dedupe the status-fetch boilerplate into
  fetchStatus:, check the getBlueLightStatus: return value, and unify
  the log prefix to [ShiftChange]
- README: remove stray artifact tags at end of file

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two related additions to the override state machine:

1. Menu bar "Turn On/Off Night Shift" item. Uses CBBlueLightClient
   setEnabled: via the client protocol — the exact call behind the
   System Settings toggle, so with a schedule configured the OS itself
   handles "until tomorrow / until sunset". While an excluded app has
   focus the toggle only updates the restore intent (display untouched),
   so per-app switching keeps working regardless of global state.

2. The bridge now registers CBBlueLightClient's status notification
   block (delivered on the main queue; the block takes no parameters
   and re-queries status rather than trusting the private struct
   layout). If the schedule or the user turns Night Shift on while an
   excluded app is focused, it is immediately re-disabled and folded
   into the restore intent — the display no longer warms mid-session
   in color-critical apps. Previously this went unnoticed until the
   next app switch.

Supporting changes:
- BlueLightControlling gains setStatusChangeHandler so the state
  machine stays fully testable; FakeBlueLightClient fires the handler
  synchronously on self-caused changes (stricter than production) to
  catch re-entrancy bugs
- disableForExcludedApp/restoreIfNeeded update state before calling
  setEnabled, making the state machine safe against synchronous
  notification delivery
- Menu refreshes on open (NSMenuDelegate) and on external changes;
  status line shows On/Off instead of "Following schedule"
- Six new unit tests covering the toggle and external-change paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- release.yml: push the updated cask to the live tap repo
  (adamdexter/homebrew-shiftchange) when the TAP_PUSH_TOKEN secret is
  configured; warn loudly when it isn't. The tap was stuck at v1.0.0
  because nothing ever updated it.
- create-dmg.sh: honor SHIFTCHANGE_SCRATCH_PATH for the swift build
  (iCloud-synced checkouts corrupt .build mid-build; default unchanged)
- CLAUDE.md: renamed paths, resource-bundle naming warning, external
  status changes + global toggle sections, expanded regression
  checklist, tap-repo documentation, iCloud build gotcha
- README: global toggle + schedule-aware feature bullets, renamed
  VERSION path
- Bump version to 1.2.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamdexter
adamdexter merged commit f70f45a into main Jul 3, 2026
4 checks passed
@adamdexter
adamdexter deleted the claude/v1.2.0-crashfix-and-toggle branch July 3, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant