Fix tmux notification attention routing

This commit is contained in:
Lawrence Chen 2026-03-20 20:20:54 -07:00
parent d4811650d7
commit 656786fb71
No known key found for this signature in database
11 changed files with 1151 additions and 64 deletions

View file

@ -6470,13 +6470,32 @@ func shouldAllowEnsureFocusWindowActivation(
final class GhosttySurfaceScrollView: NSView {
enum FlashStyle {
case standardFocus
case notificationDismiss
case navigation
case notification
}
static func flashStyle(for reason: WorkspaceAttentionFlashReason) -> FlashStyle {
switch reason {
case .navigation:
return .navigation
case .notificationArrival, .notificationDismiss, .manualUnreadDismiss, .debug:
return .notification
}
}
private static func flashPresentation(for style: FlashStyle) -> WorkspaceAttentionFlashPresentation {
switch style {
case .navigation:
return WorkspaceAttentionCoordinator.flashStyle(for: .navigation)
case .notification:
return WorkspaceAttentionCoordinator.flashStyle(for: .notificationArrival)
}
}
private enum NotificationRingMetrics {
static let inset: CGFloat = 2
static let cornerRadius: CGFloat = 6
static let inset = PanelOverlayRingMetrics.inset
static let cornerRadius = PanelOverlayRingMetrics.cornerRadius
static let lineWidth = PanelOverlayRingMetrics.lineWidth
}
private let backgroundView: NSView
@ -6489,6 +6508,7 @@ final class GhosttySurfaceScrollView: NSView {
private let notificationRingLayer: CAShapeLayer
private let flashOverlayView: GhosttyFlashOverlayView
private let flashLayer: CAShapeLayer
private var lastFlashStyle: FlashStyle = .navigation
private let keyboardCopyModeBadgeContainerView: GhosttyFlashOverlayView
private let keyboardCopyModeBadgeView: GhosttyPassthroughVisualEffectView
private let keyboardCopyModeBadgeIconView: NSImageView
@ -6743,7 +6763,7 @@ final class GhosttySurfaceScrollView: NSView {
notificationRingOverlayView.autoresizingMask = [.width, .height]
notificationRingLayer.fillColor = NSColor.clear.cgColor
notificationRingLayer.strokeColor = NSColor.systemBlue.cgColor
notificationRingLayer.lineWidth = 2.5
notificationRingLayer.lineWidth = NotificationRingMetrics.lineWidth
notificationRingLayer.lineJoin = .round
notificationRingLayer.lineCap = .round
notificationRingLayer.shadowColor = NSColor.systemBlue.cgColor
@ -6759,13 +6779,13 @@ final class GhosttySurfaceScrollView: NSView {
flashOverlayView.layer?.masksToBounds = false
flashOverlayView.autoresizingMask = [.width, .height]
flashLayer.fillColor = NSColor.clear.cgColor
flashLayer.strokeColor = NSColor.systemBlue.cgColor
flashLayer.lineWidth = 3
flashLayer.strokeColor = WorkspaceAttentionCoordinator.flashStyle(for: .navigation).accent.strokeColor.cgColor
flashLayer.lineWidth = NotificationRingMetrics.lineWidth
flashLayer.lineJoin = .round
flashLayer.lineCap = .round
flashLayer.shadowColor = NSColor.systemBlue.cgColor
flashLayer.shadowOpacity = 0.6
flashLayer.shadowRadius = 6
flashLayer.shadowColor = WorkspaceAttentionCoordinator.flashStyle(for: .navigation).accent.strokeColor.cgColor
flashLayer.shadowOpacity = Float(WorkspaceAttentionCoordinator.flashStyle(for: .navigation).glowOpacity)
flashLayer.shadowRadius = WorkspaceAttentionCoordinator.flashStyle(for: .navigation).glowRadius
flashLayer.shadowOffset = .zero
flashLayer.opacity = 0
flashOverlayView.layer?.addSublayer(flashLayer)
@ -7069,7 +7089,8 @@ final class GhosttySurfaceScrollView: NSView {
// which makes interactive width changes arrive a queue turn late on Sequoia.
scrollView.layoutSubtreeIfNeeded()
updateNotificationRingPath()
updateFlashPath(style: .standardFocus)
updateFlashPath(style: lastFlashStyle)
updateFlashAppearance(style: lastFlashStyle)
synchronizeScrollView()
synchronizeSurfaceView()
let didCoreSurfaceChange = synchronizeCoreSurface()
@ -7801,15 +7822,17 @@ final class GhosttySurfaceScrollView: NSView {
}
#endif
func triggerFlash(style: FlashStyle = .standardFocus) {
func triggerFlash(style: FlashStyle = .navigation) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
#if DEBUG
self.lastFlashStyle = style
#if DEBUG
if let surfaceId = self.surfaceView.terminalSurface?.id {
Self.recordFlash(for: surfaceId)
}
#endif
self.updateFlashPath(style: style)
self.updateFlashAppearance(style: style)
self.flashLayer.removeAllAnimations()
self.flashLayer.opacity = 0
let animation = CAKeyframeAnimation(keyPath: "opacity")
@ -8873,10 +8896,7 @@ final class GhosttySurfaceScrollView: NSView {
let inset: CGFloat
let radius: CGFloat
switch style {
case .standardFocus:
inset = CGFloat(FocusFlashPattern.ringInset)
radius = CGFloat(FocusFlashPattern.ringCornerRadius)
case .notificationDismiss:
case .navigation, .notification:
inset = NotificationRingMetrics.inset
radius = NotificationRingMetrics.cornerRadius
}
@ -8888,6 +8908,15 @@ final class GhosttySurfaceScrollView: NSView {
)
}
private func updateFlashAppearance(style: FlashStyle) {
let presentation = Self.flashPresentation(for: style)
let strokeColor = presentation.accent.strokeColor
flashLayer.strokeColor = strokeColor.cgColor
flashLayer.shadowColor = strokeColor.cgColor
flashLayer.shadowOpacity = Float(presentation.glowOpacity)
flashLayer.shadowRadius = presentation.glowRadius
}
private func updateOverlayRingPath(
layer: CAShapeLayer,
bounds: CGRect,
@ -8899,7 +8928,7 @@ final class GhosttySurfaceScrollView: NSView {
layer.path = nil
return
}
let rect = bounds.insetBy(dx: inset, dy: inset)
let rect = PanelOverlayRingMetrics.pathRect(in: bounds)
layer.path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
}