Coalesce Ghostty background notifications to latest value

This commit is contained in:
Lawrence Chen 2026-02-23 00:29:16 -08:00
parent 790f3c8287
commit afba0fb459
2 changed files with 122 additions and 16 deletions

View file

@ -124,6 +124,48 @@ enum TerminalOpenURLTarget: Equatable {
}
}
/// Coalesces Ghostty background notifications so consumers only observe
/// the latest runtime background for a burst of updates.
final class GhosttyDefaultBackgroundNotificationDispatcher {
private let coalescer: NotificationBurstCoalescer
private let postNotification: ([AnyHashable: Any]) -> Void
private var pendingUserInfo: [AnyHashable: Any]?
init(
delay: TimeInterval = 1.0 / 30.0,
postNotification: @escaping ([AnyHashable: Any]) -> Void = { userInfo in
NotificationCenter.default.post(
name: .ghosttyDefaultBackgroundDidChange,
object: nil,
userInfo: userInfo
)
}
) {
coalescer = NotificationBurstCoalescer(delay: delay)
self.postNotification = postNotification
}
func signal(backgroundColor: NSColor, opacity: Double) {
let signalOnMain = { [self] in
pendingUserInfo = [
GhosttyNotificationKey.backgroundColor: backgroundColor,
GhosttyNotificationKey.backgroundOpacity: opacity
]
coalescer.signal { [self] in
guard let userInfo = pendingUserInfo else { return }
pendingUserInfo = nil
postNotification(userInfo)
}
}
if Thread.isMainThread {
signalOnMain()
} else {
DispatchQueue.main.async(execute: signalOnMain)
}
}
}
func resolveTerminalOpenURLTarget(_ rawValue: String) -> TerminalOpenURLTarget? {
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
@ -203,6 +245,7 @@ class GhosttyApp {
}()
private let backgroundLogURL = GhosttyApp.resolveBackgroundLogURL()
private var appObservers: [NSObjectProtocol] = []
private let defaultBackgroundNotificationDispatcher = GhosttyDefaultBackgroundNotificationDispatcher()
// Scroll lag tracking
private(set) var isScrolling = false
@ -631,22 +674,10 @@ class GhosttyApp {
}
private func notifyDefaultBackgroundDidChange() {
let userInfo: [AnyHashable: Any] = [
GhosttyNotificationKey.backgroundColor: defaultBackgroundColor,
GhosttyNotificationKey.backgroundOpacity: defaultBackgroundOpacity
]
let post = {
NotificationCenter.default.post(
name: .ghosttyDefaultBackgroundDidChange,
object: nil,
userInfo: userInfo
)
}
if Thread.isMainThread {
post()
} else {
DispatchQueue.main.async(execute: post)
}
defaultBackgroundNotificationDispatcher.signal(
backgroundColor: defaultBackgroundColor,
opacity: defaultBackgroundOpacity
)
}
private func performOnMain<T>(_ work: @MainActor () -> T) -> T {

View file

@ -340,6 +340,81 @@ final class NotificationBurstCoalescerTests: XCTestCase {
}
}
final class GhosttyDefaultBackgroundNotificationDispatcherTests: XCTestCase {
func testSignalCoalescesBurstToLatestBackground() {
guard let dark = NSColor(hex: "#272822"),
let light = NSColor(hex: "#FDF6E3") else {
XCTFail("Expected valid test colors")
return
}
let expectation = expectation(description: "coalesced notification")
expectation.expectedFulfillmentCount = 1
var postedUserInfos: [[AnyHashable: Any]] = []
let dispatcher = GhosttyDefaultBackgroundNotificationDispatcher(delay: 0.01) { userInfo in
postedUserInfos.append(userInfo)
expectation.fulfill()
}
DispatchQueue.main.async {
dispatcher.signal(backgroundColor: dark, opacity: 0.95)
dispatcher.signal(backgroundColor: light, opacity: 0.75)
}
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(postedUserInfos.count, 1)
XCTAssertEqual(
(postedUserInfos[0][GhosttyNotificationKey.backgroundColor] as? NSColor)?.hexString(),
"#FDF6E3"
)
XCTAssertEqual(
postedOpacity(from: postedUserInfos[0][GhosttyNotificationKey.backgroundOpacity]),
0.75,
accuracy: 0.0001
)
}
func testSignalAcrossSeparateBurstsPostsMultipleNotifications() {
guard let dark = NSColor(hex: "#272822"),
let light = NSColor(hex: "#FDF6E3") else {
XCTFail("Expected valid test colors")
return
}
let expectation = expectation(description: "two notifications")
expectation.expectedFulfillmentCount = 2
var postedHexes: [String] = []
let dispatcher = GhosttyDefaultBackgroundNotificationDispatcher(delay: 0.01) { userInfo in
let hex = (userInfo[GhosttyNotificationKey.backgroundColor] as? NSColor)?.hexString() ?? "nil"
postedHexes.append(hex)
expectation.fulfill()
}
DispatchQueue.main.async {
dispatcher.signal(backgroundColor: dark, opacity: 1.0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
dispatcher.signal(backgroundColor: light, opacity: 1.0)
}
}
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(postedHexes, ["#272822", "#FDF6E3"])
}
private func postedOpacity(from value: Any?) -> Double {
if let value = value as? Double {
return value
}
if let value = value as? NSNumber {
return value.doubleValue
}
XCTFail("Expected background opacity payload")
return -1
}
}
final class RecentlyClosedBrowserStackTests: XCTestCase {
func testPopReturnsEntriesInLIFOOrder() {
var stack = RecentlyClosedBrowserStack(capacity: 20)