Adopt upstream PRs #1718 (non-notch clock) and #1668 (Bluetooth accessory banner)#5

open7
main
feat/upstream-prs-bt-clock
opened8d agobyKKainoa Newton
View on GitHubTwo-way sync enabledPR last imported 8d ago

Summary

Adopts two upstream Boring Notch PRs by cherry-pick (-x, original authors kept), plus notes and string-catalog entries.

  • TheBoredTeam/boring.notch#1718 (Driton): optional clock in the closed notch on displays without a notch. Off by default, under Settings > Appearance > Idle state. Applied cleanly.
  • TheBoredTeam/boring.notch#1668 (ludovickjankowski-web): Bluetooth accessory connect/disconnect banner with battery level (AirPods L/R, HID fallback for Magic accessories). Opt-in under Settings > Battery > Accessories. Adds the com.apple.security.device.bluetooth entitlement and NSBluetoothAlwaysUsageDescription. Conflicts (project.pbxproj, ContentView.swift) resolved against our main.
  • English Localizable.xcstrings entries for the new strings.
  • docs/bluetooth-alert-notes.md: comparison of #1668 with DynamicNotch and MewNotch (MewNotch has no Bluetooth feature), gaps in #1668, and a recommendation for generalizing the alert pipeline later for Wi-Fi/VPN/Raycast.
  • scripts/dev.sh cherry-picked from main (was missing from the branch before the merge).

Merged origin/main (CI cleanup, Sparkle disabled, Quit menu) with no conflicts. scripts/dev.sh build succeeds.

Needs rebasing onto compact-only

This PR will need rework once the compact-only PR (#1) merges, because that PR rewrites ContentView.swift into a panel layout. The ContentView hook points to re-apply:

  1. @Default(.showClockWithoutNotch) property.
  2. Closed idle !vm.hasNotch branch: NotchClockView in place of the empty Rectangle when closed and the notch height is non-zero.
  3. Overlay of NotchClockView on the music activity centre when !vm.hasNotch.
  4. case bluetoothStatus in the state enum and its selection after the battery check.
  5. showBluetoothActivity computed property.
  6. chinWidth = 640 branch for showBluetoothActivity.
  7. else if showBluetoothActivity { BluetoothAccessoryView() } right after the PowerStatusNotice branch.

Not verified at runtime

The app was never launched. Bluetooth hardware behaviour (connect/disconnect callbacks, replay for already-connected devices, AirPods per-bud battery, the HID fallback under the sandbox), the non-notch layout, hover and fullscreen are all untested. See docs/bluetooth-alert-notes.md for the gaps in #1668.

How to test

sh
1cd /Users/knewton/.t3/worktrees/not.boring.notch/feat-upstream-prs-bt-clock && scripts/dev.sh

Quit the installed Boring Notch first (it shares the bundle ID). macOS may re-ask for permissions after each rebuild. Bluetooth permission is required for the accessory banner.

  • Clock: Settings > Appearance > Idle state > "Show clock on displays without a notch". On a non-notch display or external monitor the closed notch should show the time (and date if enabled), stay up while music plays, and be covered by notifications.
  • Bluetooth: Settings > Battery > Accessories > enable, allow the prompt. Connect/disconnect AirPods: a ~3 s banner with the name and battery %, then a grey "Disconnected" banner.

🤖 Generated with Claude Code


Summary by cubic

Adopts upstream PRs #1718 (optional clock when the display has no notch) and #1668 (Bluetooth accessory connect/disconnect banner with battery level).

  • #1718: Adds a clock to the closed notch on displays without a physical notch, off by default under Settings > Appearance > Idle state. Stays visible while music plays and is covered by notifications.
  • #1668: Shows a banner when Bluetooth accessories (AirPods, headphones, keyboards, mice) connect or disconnect, with battery level when available. Opt-in under Settings > Battery > Accessories and adds the Bluetooth entitlement plus usage description.
  • Adds English string-catalog entries, a comparison doc for #1668 against DynamicNotch and MewNotch, and a scripts/dev.sh build helper.

Note

  • Not runtime-verified: Bluetooth callbacks, battery fallback, and layout were never tested on hardware.
  • Will need rework once the compact-only PR merges, since that PR rewrites ContentView.swift; the diff lists the hook points to re-apply.

Written for commit 1cbbbe84ec2ec51e898ba5c8a6962a3c06c6c1db. Summary will update on new commits.

View guided diff Turn on auto-fix

Checks

1 failing · 7 successful · 1 neutral
Some checks failed

Failing1

1cbbbe8

Added 7 commits

  • Add English Localizable.xcstrings entries for the clock and Bluetooth strings0b94057
  • Merge remote-tracking branch 'origin/main' into feat/upstream-prs-bt-clock1cbbbe8
  • Keep the clock visible while music is playing463d023
  • Add scripts/dev.sh to build and run a checkout54a3e50
  • Add Bluetooth alert comparison notes (#1668 vs DynamicNotch vs MewNotch)5d78855
  • Show a clock in the closed notch on displays without a notch79696a0
  • Show Bluetooth accessory connections with battery levelf92ea2e
Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 0533f57e-88a6-49a7-9cef-7e598347d81e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ccubic-dev-aiBotreviewed · 7 inline threads8d ago
Synced from GitHub

7 issues found across 13 files

Prompt for AI agents (unresolved issues)
text
1
2Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
3
4
5<file name="docs/bluetooth-alert-notes.md">
6
7<violation number="1" location="docs/bluetooth-alert-notes.md:16">
8P3: The detection section calls connect registration the only trigger, but each connection also installs a disconnect-notification callback that announces disconnects. Describe both notification paths so this summary does not contradict the implementation.</violation>
9
10<violation number="2" location="docs/bluetooth-alert-notes.md:21">
11P3: The notes report the second read at +2.5 s and the maximum banner duration as 5.5 s, but the manager sleeps cumulatively, so the second read can occur at +3.5 s and the banner can last about 6.5 s. Update the repeated timing claims on lines 21, 62, and 65.</violation>
12
13<violation number="3" location="docs/bluetooth-alert-notes.md:22">
14P3: This says the build is unsigned and cannot expose embedded entitlements, but `scripts/dev.sh build` produces an ad-hoc-signed app. Describe the actual signing mode and direct readers to inspect the signed bundle’s entitlements.</violation>
15</file>
16
17<file name="boringNotch/managers/BluetoothAccessoryManager.swift">
18
19<violation number="1" location="boringNotch/managers/BluetoothAccessoryManager.swift:26">
20P2: The parsed charging-case level is never used for display, so case-only battery information appears as merely “Connected.” Include `caseLevel` as the final fallback for `primary`.</violation>
21
22<violation number="2" location="boringNotch/managers/BluetoothAccessoryManager.swift:76">
23P2: A shared event and refresh task drops earlier accessories when multiple devices connect close together. Keep refresh state per address and queue or coalesce events instead of overwriting the single banner.</violation>
24
25<violation number="3" location="boringNotch/managers/BluetoothAccessoryManager.swift:105">
26P3: The Bluetooth manager bypasses `NotchUIEventBus` and directly owns coordinator presentation. Publish an expanding-view event through the existing bus so manager lifecycle and UI presentation remain decoupled.</violation>
27
28<violation number="4" location="boringNotch/managers/BluetoothAccessoryManager.swift:161">
29P2: A valid 0% battery reading is discarded, so an empty accessory is shown as having no battery information. Accept zero by changing the range to `0...100`.</violation>
30</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

……@@ -0,0 +1,90 @@
18+- **Battery.**
19+ - First try: KVC on `IOBluetoothDevice` using the private properties `batteryPercentSingle`, `batteryPercentLeft`, `batteryPercentRight` and `batteryPercentCase`. Each is guarded by `responds(to:)` and range-checked to 1...100.
20+ - Fallback: IORegistry. It walks `AppleDeviceManagementHIDEventService` and matches `DeviceAddress`, then reads `BatteryPercent`. This covers Magic Keyboard, Mouse and Trackpad.
21+ - Refresh: re-read at +1 s and +2.5 s after connect, and re-show the banner if the value changed.
Ccubic-dev-aiBoton line 218d ago

P3: The notes report the second read at +2.5 s and the maximum banner duration as 5.5 s, but the manager sleeps cumulatively, so the second read can occur at +3.5 s and the banner can last about 6.5 s. Update the repeated timing claims on lines 21, 62, and 65.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/bluetooth-alert-notes.md, line 21:
2
3<comment>The notes report the second read at +2.5 s and the maximum banner duration as 5.5 s, but the manager sleeps cumulatively, so the second read can occur at +3.5 s and the banner can last about 6.5 s. Update the repeated timing claims on lines 21, 62, and 65.</comment>
4
5<file context>
6@@ -0,0 +1,90 @@
7+- **Battery.**
8+ - First try: KVC on `IOBluetoothDevice` using the private properties `batteryPercentSingle`, `batteryPercentLeft`, `batteryPercentRight` and `batteryPercentCase`. Each is guarded by `responds(to:)` and range-checked to 1...100.
9+ - Fallback: IORegistry. It walks `AppleDeviceManagementHIDEventService` and matches `DeviceAddress`, then reads `BatteryPercent`. This covers Magic Keyboard, Mouse and Trackpad.
10+ - Refresh: re-read at +1 s and +2.5 s after connect, and re-show the banner if the value changed.
11+- **Permissions.** Sandbox entitlement `com.apple.security.device.bluetooth` plus `NSBluetoothAlwaysUsageDescription`. It is opt-in (`showBluetoothAccessories`, default off), so the prompt only appears once the user enables it. The usage string is in the built Info.plist (checked with PlistBuddy). The entitlement is in `boringNotch.entitlements`, but the unsigned build can't show it embedded.
12+- **Presentation.** A new `SneakContentType.bluetooth` is raised through `BoringViewCoordinator.toggleExpandingView`. `ContentView` renders `BluetoothAccessoryView` in the closed notch with the same layout as the power notice: icon and name on the left, a black spacer the width of the notch, battery or "Connected" or "Disconnected" on the right. The shared `expandingView` timer hides it after 3 s. The notch widens to 640 pt.
13</file context>
……@@ -0,0 +1,205 @@
102+ guard battery != event.battery else { continue }
103+ event.battery = battery
104+ self.lastEvent = event
105+ BoringViewCoordinator.shared.toggleExpandingView(status: true, type: .bluetooth)
Ccubic-dev-aiBoton line 1058d ago

P3: The Bluetooth manager bypasses NotchUIEventBus and directly owns coordinator presentation. Publish an expanding-view event through the existing bus so manager lifecycle and UI presentation remain decoupled.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/managers/BluetoothAccessoryManager.swift, line 105:
2
3<comment>The Bluetooth manager bypasses `NotchUIEventBus` and directly owns coordinator presentation. Publish an expanding-view event through the existing bus so manager lifecycle and UI presentation remain decoupled.</comment>
4
5<file context>
6@@ -0,0 +1,205 @@
7+ guard battery != event.battery else { continue }
8+ event.battery = battery
9+ self.lastEvent = event
10+ BoringViewCoordinator.shared.toggleExpandingView(status: true, type: .bluetooth)
11+ }
12+ }
13</file context>
……@@ -0,0 +1,205 @@
73+ connectNotification = nil
74+ disconnectNotifications.values.forEach { $0.unregister() }
75+ disconnectNotifications.removeAll()
76+ refreshTask?.cancel()
Ccubic-dev-aiBoton line 768d ago

P2: A shared event and refresh task drops earlier accessories when multiple devices connect close together. Keep refresh state per address and queue or coalesce events instead of overwriting the single banner.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/managers/BluetoothAccessoryManager.swift, line 76:
2
3<comment>A shared event and refresh task drops earlier accessories when multiple devices connect close together. Keep refresh state per address and queue or coalesce events instead of overwriting the single banner.</comment>
4
5<file context>
6@@ -0,0 +1,205 @@
7+ connectNotification = nil
8+ disconnectNotifications.values.forEach { $0.unregister() }
9+ disconnectNotifications.removeAll()
10+ refreshTask?.cancel()
11+ lastEvent = nil
12+ }
13</file context>
……@@ -0,0 +1,90 @@
13+
14+Files: `managers/BluetoothAccessoryManager.swift`, `components/LiveActivities/BluetoothAccessoryView.swift`, plus a few hook points.
15+
16+- **Detection.** `IOBluetoothDevice.register(forConnectNotifications:selector:)` is the only trigger. Each connect callback registers a per-device `register(forDisconnectNotification:selector:)`. There is no polling and no power-state handling.
Ccubic-dev-aiBoton line 168d ago

P3: The detection section calls connect registration the only trigger, but each connection also installs a disconnect-notification callback that announces disconnects. Describe both notification paths so this summary does not contradict the implementation.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/bluetooth-alert-notes.md, line 16:
2
3<comment>The detection section calls connect registration the only trigger, but each connection also installs a disconnect-notification callback that announces disconnects. Describe both notification paths so this summary does not contradict the implementation.</comment>
4
5<file context>
6@@ -0,0 +1,90 @@
7+
8+Files: `managers/BluetoothAccessoryManager.swift`, `components/LiveActivities/BluetoothAccessoryView.swift`, plus a few hook points.
9+
10+- **Detection.** `IOBluetoothDevice.register(forConnectNotifications:selector:)` is the only trigger. Each connect callback registers a per-device `register(forDisconnectNotification:selector:)`. There is no polling and no power-state handling.
11+- **Already-connected devices.** The author assumes IOBluetooth replays a connect callback for each already-connected device right after registration. `start()` sets `ignoreConnectionsUntil = now + 2s` so those replays don't show banners but still register their disconnect observers.
12+- **Battery.**
13</file context>
suggestion
1- **Detection.** Detection uses `IOBluetoothDevice.register(forConnectNotifications:selector:)` and per-device `register(forDisconnectNotification:selector:)`; there is no polling or power-state handling.
……@@ -0,0 +1,205 @@
158+ func level(_ key: String) -> Int? {
159+ guard device.responds(to: NSSelectorFromString(key)),
160+ let value = (device.value(forKey: key) as? NSNumber)?.intValue,
161+ (1...100).contains(value) else { return nil }
Ccubic-dev-aiBoton line 1618d ago

P2: A valid 0% battery reading is discarded, so an empty accessory is shown as having no battery information. Accept zero by changing the range to 0...100.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/managers/BluetoothAccessoryManager.swift, line 161:
2
3<comment>A valid 0% battery reading is discarded, so an empty accessory is shown as having no battery information. Accept zero by changing the range to `0...100`.</comment>
4
5<file context>
6@@ -0,0 +1,205 @@
7+ func level(_ key: String) -> Int? {
8+ guard device.responds(to: NSSelectorFromString(key)),
9+ let value = (device.value(forKey: key) as? NSNumber)?.intValue,
10+ (1...100).contains(value) else { return nil }
11+ return value
12+ }
13</file context>
suggestion
1 (0...100).contains(value) else { return nil }
……@@ -0,0 +1,90 @@
19+ - First try: KVC on `IOBluetoothDevice` using the private properties `batteryPercentSingle`, `batteryPercentLeft`, `batteryPercentRight` and `batteryPercentCase`. Each is guarded by `responds(to:)` and range-checked to 1...100.
20+ - Fallback: IORegistry. It walks `AppleDeviceManagementHIDEventService` and matches `DeviceAddress`, then reads `BatteryPercent`. This covers Magic Keyboard, Mouse and Trackpad.
21+ - Refresh: re-read at +1 s and +2.5 s after connect, and re-show the banner if the value changed.
22+- **Permissions.** Sandbox entitlement `com.apple.security.device.bluetooth` plus `NSBluetoothAlwaysUsageDescription`. It is opt-in (`showBluetoothAccessories`, default off), so the prompt only appears once the user enables it. The usage string is in the built Info.plist (checked with PlistBuddy). The entitlement is in `boringNotch.entitlements`, but the unsigned build can't show it embedded.
Ccubic-dev-aiBoton line 228d ago

P3: This says the build is unsigned and cannot expose embedded entitlements, but scripts/dev.sh build produces an ad-hoc-signed app. Describe the actual signing mode and direct readers to inspect the signed bundle’s entitlements.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/bluetooth-alert-notes.md, line 22:
2
3<comment>This says the build is unsigned and cannot expose embedded entitlements, but `scripts/dev.sh build` produces an ad-hoc-signed app. Describe the actual signing mode and direct readers to inspect the signed bundle’s entitlements.</comment>
4
5<file context>
6@@ -0,0 +1,90 @@
7+ - First try: KVC on `IOBluetoothDevice` using the private properties `batteryPercentSingle`, `batteryPercentLeft`, `batteryPercentRight` and `batteryPercentCase`. Each is guarded by `responds(to:)` and range-checked to 1...100.
8+ - Fallback: IORegistry. It walks `AppleDeviceManagementHIDEventService` and matches `DeviceAddress`, then reads `BatteryPercent`. This covers Magic Keyboard, Mouse and Trackpad.
9+ - Refresh: re-read at +1 s and +2.5 s after connect, and re-show the banner if the value changed.
10+- **Permissions.** Sandbox entitlement `com.apple.security.device.bluetooth` plus `NSBluetoothAlwaysUsageDescription`. It is opt-in (`showBluetoothAccessories`, default off), so the prompt only appears once the user enables it. The usage string is in the built Info.plist (checked with PlistBuddy). The entitlement is in `boringNotch.entitlements`, but the unsigned build can't show it embedded.
11+- **Presentation.** A new `SneakContentType.bluetooth` is raised through `BoringViewCoordinator.toggleExpandingView`. `ContentView` renders `BluetoothAccessoryView` in the closed notch with the same layout as the power notice: icon and name on the left, a black spacer the width of the notch, battery or "Connected" or "Disconnected" on the right. The shared `expandingView` timer hides it after 3 s. The notch widens to 640 pt.
12+
13</file context>
suggestion
1- **Permissions.** Sandbox entitlement `com.apple.security.device.bluetooth` plus `NSBluetoothAlwaysUsageDescription`. It is opt-in (`showBluetoothAccessories`, default off), so the prompt only appears once the user enables it. The usage string is in the built Info.plist (checked with PlistBuddy). The entitlement is in `boringNotch.entitlements`; `scripts/dev.sh build` ad-hoc-signs the app, so inspect the signed bundle to verify it.
……@@ -0,0 +1,205 @@
23+ /// The level that best represents the accessory: the lowest earbud for AirPods-style devices.
24+ var primary: Int? {
25+ if let left, let right { return min(left, right) }
26+ return single ?? left ?? right
Ccubic-dev-aiBoton line 268d ago

P2: The parsed charging-case level is never used for display, so case-only battery information appears as merely “Connected.” Include caseLevel as the final fallback for primary.

Prompt for AI agents
text
1Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/managers/BluetoothAccessoryManager.swift, line 26:
2
3<comment>The parsed charging-case level is never used for display, so case-only battery information appears as merely “Connected.” Include `caseLevel` as the final fallback for `primary`.</comment>
4
5<file context>
6@@ -0,0 +1,205 @@
7+ /// The level that best represents the accessory: the lowest earbud for AirPods-style devices.
8+ var primary: Int? {
9+ if let left, let right { return min(left, right) }
10+ return single ?? left ?? right
11+ }
12+}
13</file context>
suggestion
1 return single ?? left ?? right ?? caseLevel