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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,13 @@ extension NotificationCenterService {
/// Sets the Dock tile's unread count. Compares with the label it set
/// last: reading `dockTile.badgeLabel` asks the Dock and can block.
func updateDockBadge(_ count: Int) {
dockAttention.unreadChanged(count)
let label = preferences.dockBadge && count > 0 ? String(count) : nil
guard label != dockBadgeLabel else { return }
dockBadgeLabel = label
NSApp.dockTile.badgeLabel = label
}

/// Bounces the Dock icon for an alert that arrived while cmux was inactive
/// (`notifications.dockBounce`): once, or until the user switches to cmux.
func bounceDock(_ mode: DockBounceMode?) {
guard let mode, !NSApp.isActive else { return }
NSApp.requestUserAttention(mode == .untilActive ? .criticalRequest : .informationalRequest)
}

/// The handoff driver over the local daemon and this Mac's install
/// principal (`feed.adopt` is an install-only owner op).
func makeFeedDriver(_ services: AppServices, principal: FeedInstallPrincipal) -> FeedHandoffDriver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CmuxNextAgentPane
import CmuxNextBridge
import CmuxNextCompat
import CmuxNextDaemon
import CmuxNextDesign
import CmuxNextSettings
import CmuxNextTerminal
import CmuxNextWakeups
Expand Down Expand Up @@ -47,6 +48,8 @@ final class NotificationCenterService {
@ObservationIgnored private var lastSeen: UInt64 = 0
/// The Dock badge this service set last (nil: none).
@ObservationIgnored var dockBadgeLabel: String?
/// `notifications.dockBounce`: the running bounce, ended when nothing is unread.
@ObservationIgnored let dockAttention = DockAttention()
/// Hands the daemon's local feed items to the cloud owner (feed.md 9.1).
@ObservationIgnored var feedDriver: FeedHandoffDriver?
/// Agent permission prompts to the person's feed, while
Expand Down Expand Up @@ -283,7 +286,7 @@ final class NotificationCenterService {
feedDriver?.run()
if decision.desktop { post(notification, tab: nil, workspace: nil, sound: decision.sound) }
if !notification.desktop, let sound = decision.sound { NotificationSounds.playWithoutBanner(sound) }
bounceDock(decision.bounce)
if let mode = decision.bounce { dockAttention.bounce(untilActive: mode == .untilActive) }
return
}
if decision.acknowledge {
Expand Down Expand Up @@ -345,7 +348,7 @@ final class NotificationCenterService {
notification.desktop ? NotificationSounds.play(sound) : NotificationSounds.playWithoutBanner(sound)
}
if let seconds = decision.timeout { scheduleTimeout(seconds, tabID: located.tab.id) }
bounceDock(decision.bounce)
if let mode = decision.bounce { dockAttention.bounce(untilActive: mode == .untilActive) }
}

private func post(_ notification: DaemonNotification, tab: TabModel?, workspace: String?, sound: String?,
Expand Down
30 changes: 30 additions & 0 deletions Packages/macOS/CmuxNext/Sources/CmuxNextDesign/DockAttention.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
public import AppKit

/// The Dock icon's bounce (`notifications.dockBounce`, mapped by the App), one request at a
/// time. A bounce that lasts until the user switches to cmux also ends once
/// nothing is unread any more (read on the phone, from the CLI, or by Mark All
/// Read), so an alert that was dealt with elsewhere never keeps bouncing.
@MainActor
public final class DockAttention {
private var request: Int?

public init() {}

/// Bounces for an alert that arrived while cmux was inactive: once, or until the user switches to cmux.
public func bounce(untilActive: Bool) {
guard !NSApp.isActive else { return }
cancel()
let id = NSApp.requestUserAttention(untilActive ? .criticalRequest : .informationalRequest)
request = untilActive ? id : nil
}

/// The unread count changed; at zero a bounce still running ends.
public func unreadChanged(_ count: Int) {
if count == 0 { cancel() }
}

private func cancel() {
if let request { NSApp.cancelUserAttentionRequest(request) }
request = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ public nonisolated struct NotificationPolicy: Sendable {
decision.sound = sound
}
if mode == .timeout { decision.timeout = prefs.timeoutSeconds }
if decision.alerts, !arrival.appActive, prefs.dockBounce != .off { decision.bounce = prefs.dockBounce }
// A poster asking for no banner (`notify --desktop false`) gets no bounce either.
if decision.desktop, !arrival.appActive, prefs.dockBounce != .off { decision.bounce = prefs.dockBounce }
return decision
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ struct DockBouncePolicyTests {
#expect(Self.decide(desktop: .never).bounce == nil)
}

@Test func aPosterAskingForNoBannerGetsNoBounce() {
var arrival = NotificationPolicy.Arrival(source: .cli)
arrival.bannerAllowed = false
#expect(NotificationPolicy(prefs: NotificationPreferences()).decide(arrival).bounce == nil)
}

@Test func quietHoursDoNotBounce() {
var prefs = NotificationPreferences()
prefs.dockBounce = .untilActive
Expand Down
2 changes: 1 addition & 1 deletion schemas/settings-export/settings-schema.sources.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f98f0183248257e005cb1cb431cebc8f5f02c6243890c5b38d6bd77d0e8a6f9c
383309d0b9be0d3a960db70d205bc7649ac655a914e2df225ef1b181eb455e88
Loading