Unify runtime theme reload path and prioritize surface background updates
This commit is contained in:
parent
afba0fb459
commit
cd570dbab2
5 changed files with 307 additions and 62 deletions
|
|
@ -1857,6 +1857,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
if normalizedFlags == [.command], chars == "q" {
|
||||
return handleQuitShortcutWarning()
|
||||
}
|
||||
if normalizedFlags == [.command, .shift],
|
||||
(chars == "," || chars == "<" || event.keyCode == 43) {
|
||||
GhosttyApp.shared.reloadConfiguration(source: "shortcut.cmd_shift_comma")
|
||||
return true
|
||||
}
|
||||
|
||||
// When the terminal has active IME composition (e.g. Korean, Japanese, Chinese
|
||||
// input), don't intercept key events — let them flow through to the input method.
|
||||
|
|
|
|||
|
|
@ -124,15 +124,33 @@ enum TerminalOpenURLTarget: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
enum GhosttyDefaultBackgroundUpdateScope: Int {
|
||||
case unscoped = 0
|
||||
case app = 1
|
||||
case surface = 2
|
||||
|
||||
var logLabel: String {
|
||||
switch self {
|
||||
case .unscoped: return "unscoped"
|
||||
case .app: return "app"
|
||||
case .surface: return "surface"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]?
|
||||
private var pendingEventId: UInt64 = 0
|
||||
private var pendingSource: String = "unspecified"
|
||||
private let logEvent: ((String) -> Void)?
|
||||
|
||||
init(
|
||||
delay: TimeInterval = 1.0 / 30.0,
|
||||
logEvent: ((String) -> Void)? = nil,
|
||||
postNotification: @escaping ([AnyHashable: Any]) -> Void = { userInfo in
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyDefaultBackgroundDidChange,
|
||||
|
|
@ -142,18 +160,29 @@ final class GhosttyDefaultBackgroundNotificationDispatcher {
|
|||
}
|
||||
) {
|
||||
coalescer = NotificationBurstCoalescer(delay: delay)
|
||||
self.logEvent = logEvent
|
||||
self.postNotification = postNotification
|
||||
}
|
||||
|
||||
func signal(backgroundColor: NSColor, opacity: Double) {
|
||||
func signal(backgroundColor: NSColor, opacity: Double, eventId: UInt64, source: String) {
|
||||
let signalOnMain = { [self] in
|
||||
pendingEventId = eventId
|
||||
pendingSource = source
|
||||
pendingUserInfo = [
|
||||
GhosttyNotificationKey.backgroundColor: backgroundColor,
|
||||
GhosttyNotificationKey.backgroundOpacity: opacity
|
||||
GhosttyNotificationKey.backgroundOpacity: opacity,
|
||||
GhosttyNotificationKey.backgroundEventId: NSNumber(value: eventId),
|
||||
GhosttyNotificationKey.backgroundSource: source
|
||||
]
|
||||
logEvent?(
|
||||
"bg notify queued id=\(eventId) source=\(source) color=\(backgroundColor.hexString()) opacity=\(String(format: "%.3f", opacity))"
|
||||
)
|
||||
coalescer.signal { [self] in
|
||||
guard let userInfo = pendingUserInfo else { return }
|
||||
let eventId = pendingEventId
|
||||
let source = pendingSource
|
||||
pendingUserInfo = nil
|
||||
logEvent?("bg notify flushed id=\(eventId) source=\(source)")
|
||||
postNotification(userInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -203,6 +232,11 @@ func resolveTerminalOpenURLTarget(_ rawValue: String) -> TerminalOpenURLTarget?
|
|||
|
||||
class GhosttyApp {
|
||||
static let shared = GhosttyApp()
|
||||
private static let backgroundLogTimestampFormatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private(set) var app: ghostty_app_t?
|
||||
private(set) var config: ghostty_config_t?
|
||||
|
|
@ -245,7 +279,14 @@ class GhosttyApp {
|
|||
}()
|
||||
private let backgroundLogURL = GhosttyApp.resolveBackgroundLogURL()
|
||||
private var appObservers: [NSObjectProtocol] = []
|
||||
private let defaultBackgroundNotificationDispatcher = GhosttyDefaultBackgroundNotificationDispatcher()
|
||||
private var backgroundEventCounter: UInt64 = 0
|
||||
private var defaultBackgroundUpdateScope: GhosttyDefaultBackgroundUpdateScope = .unscoped
|
||||
private var defaultBackgroundScopeSource: String = "initialize"
|
||||
private lazy var defaultBackgroundNotificationDispatcher: GhosttyDefaultBackgroundNotificationDispatcher =
|
||||
GhosttyDefaultBackgroundNotificationDispatcher(logEvent: { [weak self] message in
|
||||
guard let self, self.backgroundLogEnabled else { return }
|
||||
self.logBackground(message)
|
||||
})
|
||||
|
||||
// Scroll lag tracking
|
||||
private(set) var isScrolling = false
|
||||
|
|
@ -361,7 +402,7 @@ class GhosttyApp {
|
|||
// Load default config (includes user config). If this fails hard (e.g. due to
|
||||
// invalid user config), ghostty_app_new may return nil; we fall back below.
|
||||
loadDefaultConfigFilesWithLegacyFallback(primaryConfig)
|
||||
updateDefaultBackground(from: primaryConfig)
|
||||
updateDefaultBackground(from: primaryConfig, source: "initialize.primaryConfig")
|
||||
|
||||
// Create runtime config with callbacks
|
||||
var runtimeConfig = ghostty_runtime_config_s()
|
||||
|
|
@ -483,7 +524,7 @@ class GhosttyApp {
|
|||
}
|
||||
|
||||
ghostty_config_finalize(fallbackConfig)
|
||||
updateDefaultBackground(from: fallbackConfig)
|
||||
updateDefaultBackground(from: fallbackConfig, source: "initialize.fallbackConfig")
|
||||
|
||||
guard let created = ghostty_app_new(&runtimeConfig, fallbackConfig) else {
|
||||
#if DEBUG
|
||||
|
|
@ -543,6 +584,13 @@ class GhosttyApp {
|
|||
return true
|
||||
}
|
||||
|
||||
static func shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: GhosttyDefaultBackgroundUpdateScope,
|
||||
incomingScope: GhosttyDefaultBackgroundUpdateScope
|
||||
) -> Bool {
|
||||
incomingScope.rawValue >= currentScope.rawValue
|
||||
}
|
||||
|
||||
private func loadLegacyGhosttyConfigIfNeeded(_ config: ghostty_config_t) {
|
||||
#if os(macOS)
|
||||
// Ghostty 1.3+ prefers `config.ghostty`, but some users still have their real
|
||||
|
|
@ -590,18 +638,31 @@ class GhosttyApp {
|
|||
}
|
||||
}
|
||||
|
||||
func reloadConfiguration(soft: Bool = false) {
|
||||
guard let app else { return }
|
||||
func reloadConfiguration(soft: Bool = false, source: String = "unspecified") {
|
||||
guard let app else {
|
||||
logThemeAction("reload skipped source=\(source) soft=\(soft) reason=no_app")
|
||||
return
|
||||
}
|
||||
logThemeAction("reload begin source=\(source) soft=\(soft)")
|
||||
resetDefaultBackgroundUpdateScope(source: "reloadConfiguration(source=\(source))")
|
||||
if soft, let config {
|
||||
ghostty_app_update_config(app, config)
|
||||
NotificationCenter.default.post(name: .ghosttyConfigDidReload, object: nil)
|
||||
logThemeAction("reload end source=\(source) soft=\(soft) mode=soft")
|
||||
return
|
||||
}
|
||||
|
||||
guard let newConfig = ghostty_config_new() else { return }
|
||||
guard let newConfig = ghostty_config_new() else {
|
||||
logThemeAction("reload skipped source=\(source) soft=\(soft) reason=config_alloc_failed")
|
||||
return
|
||||
}
|
||||
loadDefaultConfigFilesWithLegacyFallback(newConfig)
|
||||
ghostty_app_update_config(app, newConfig)
|
||||
updateDefaultBackground(from: newConfig)
|
||||
updateDefaultBackground(
|
||||
from: newConfig,
|
||||
source: "reloadConfiguration(source=\(source))",
|
||||
scope: .unscoped
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
self.applyBackgroundToKeyWindow()
|
||||
}
|
||||
|
|
@ -610,18 +671,7 @@ class GhosttyApp {
|
|||
}
|
||||
config = newConfig
|
||||
NotificationCenter.default.post(name: .ghosttyConfigDidReload, object: nil)
|
||||
}
|
||||
|
||||
func reloadConfiguration(for surface: ghostty_surface_t, soft: Bool = false) {
|
||||
if soft, let config {
|
||||
ghostty_surface_update_config(surface, config)
|
||||
return
|
||||
}
|
||||
|
||||
guard let newConfig = ghostty_config_new() else { return }
|
||||
loadDefaultConfigFilesWithLegacyFallback(newConfig)
|
||||
ghostty_surface_update_config(surface, newConfig)
|
||||
ghostty_config_free(newConfig)
|
||||
logThemeAction("reload end source=\(source) soft=\(soft) mode=full")
|
||||
}
|
||||
|
||||
func openConfigurationInTextEdit() {
|
||||
|
|
@ -643,15 +693,30 @@ class GhosttyApp {
|
|||
return String(decoding: buffer, as: UTF8.self)
|
||||
}
|
||||
|
||||
private func updateDefaultBackground(from config: ghostty_config_t?) {
|
||||
guard let config else { return }
|
||||
let previousHex = defaultBackgroundColor.hexString()
|
||||
let previousOpacity = defaultBackgroundOpacity
|
||||
private func resetDefaultBackgroundUpdateScope(source: String) {
|
||||
let previousScope = defaultBackgroundUpdateScope
|
||||
let previousScopeSource = defaultBackgroundScopeSource
|
||||
defaultBackgroundUpdateScope = .unscoped
|
||||
defaultBackgroundScopeSource = "reset:\(source)"
|
||||
if backgroundLogEnabled {
|
||||
logBackground(
|
||||
"default background scope reset source=\(source) previousScope=\(previousScope.logLabel) previousSource=\(previousScopeSource)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDefaultBackground(
|
||||
from config: ghostty_config_t?,
|
||||
source: String,
|
||||
scope: GhosttyDefaultBackgroundUpdateScope = .unscoped
|
||||
) {
|
||||
guard let config else { return }
|
||||
|
||||
var resolvedColor = defaultBackgroundColor
|
||||
var color = ghostty_config_color_s()
|
||||
let bgKey = "background"
|
||||
if ghostty_config_get(config, &color, bgKey, UInt(bgKey.lengthOfBytes(using: .utf8))) {
|
||||
defaultBackgroundColor = NSColor(
|
||||
resolvedColor = NSColor(
|
||||
red: CGFloat(color.r) / 255,
|
||||
green: CGFloat(color.g) / 255,
|
||||
blue: CGFloat(color.b) / 255,
|
||||
|
|
@ -659,24 +724,99 @@ class GhosttyApp {
|
|||
)
|
||||
}
|
||||
|
||||
var opacity: Double = 1.0
|
||||
var opacity = defaultBackgroundOpacity
|
||||
let opacityKey = "background-opacity"
|
||||
_ = ghostty_config_get(config, &opacity, opacityKey, UInt(opacityKey.lengthOfBytes(using: .utf8)))
|
||||
applyDefaultBackground(
|
||||
color: resolvedColor,
|
||||
opacity: opacity,
|
||||
source: source,
|
||||
scope: scope
|
||||
)
|
||||
}
|
||||
|
||||
private func applyDefaultBackground(
|
||||
color: NSColor,
|
||||
opacity: Double,
|
||||
source: String,
|
||||
scope: GhosttyDefaultBackgroundUpdateScope
|
||||
) {
|
||||
let previousScope = defaultBackgroundUpdateScope
|
||||
let previousScopeSource = defaultBackgroundScopeSource
|
||||
guard Self.shouldApplyDefaultBackgroundUpdate(currentScope: previousScope, incomingScope: scope) else {
|
||||
if backgroundLogEnabled {
|
||||
logBackground(
|
||||
"default background skipped source=\(source) incomingScope=\(scope.logLabel) currentScope=\(previousScope.logLabel) currentSource=\(previousScopeSource) color=\(color.hexString()) opacity=\(String(format: "%.3f", opacity))"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defaultBackgroundUpdateScope = scope
|
||||
defaultBackgroundScopeSource = source
|
||||
|
||||
let previousHex = defaultBackgroundColor.hexString()
|
||||
let previousOpacity = defaultBackgroundOpacity
|
||||
defaultBackgroundColor = color
|
||||
defaultBackgroundOpacity = opacity
|
||||
let hasChanged = previousHex != defaultBackgroundColor.hexString() ||
|
||||
abs(previousOpacity - defaultBackgroundOpacity) > 0.0001
|
||||
if hasChanged {
|
||||
notifyDefaultBackgroundDidChange()
|
||||
notifyDefaultBackgroundDidChange(source: source)
|
||||
}
|
||||
if backgroundLogEnabled {
|
||||
logBackground("default background updated color=\(defaultBackgroundColor) opacity=\(String(format: "%.3f", defaultBackgroundOpacity))")
|
||||
logBackground(
|
||||
"default background updated source=\(source) scope=\(scope.logLabel) previousScope=\(previousScope.logLabel) previousScopeSource=\(previousScopeSource) previousColor=\(previousHex) previousOpacity=\(String(format: "%.3f", previousOpacity)) color=\(defaultBackgroundColor) opacity=\(String(format: "%.3f", defaultBackgroundOpacity)) changed=\(hasChanged)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyDefaultBackgroundDidChange() {
|
||||
defaultBackgroundNotificationDispatcher.signal(
|
||||
backgroundColor: defaultBackgroundColor,
|
||||
opacity: defaultBackgroundOpacity
|
||||
private func nextBackgroundEventId() -> UInt64 {
|
||||
precondition(Thread.isMainThread, "Background event IDs must be generated on main thread")
|
||||
backgroundEventCounter &+= 1
|
||||
return backgroundEventCounter
|
||||
}
|
||||
|
||||
private func notifyDefaultBackgroundDidChange(source: String) {
|
||||
let signal = { [self] in
|
||||
let eventId = nextBackgroundEventId()
|
||||
defaultBackgroundNotificationDispatcher.signal(
|
||||
backgroundColor: defaultBackgroundColor,
|
||||
opacity: defaultBackgroundOpacity,
|
||||
eventId: eventId,
|
||||
source: source
|
||||
)
|
||||
}
|
||||
if Thread.isMainThread {
|
||||
signal()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: signal)
|
||||
}
|
||||
}
|
||||
|
||||
private func logThemeAction(_ message: String) {
|
||||
guard backgroundLogEnabled else { return }
|
||||
logBackground("theme action \(message)")
|
||||
}
|
||||
|
||||
private func actionLabel(for action: ghostty_action_s) -> String {
|
||||
switch action.tag {
|
||||
case GHOSTTY_ACTION_RELOAD_CONFIG:
|
||||
return "reload_config"
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
return "config_change"
|
||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||
return "color_change"
|
||||
default:
|
||||
return String(describing: action.tag)
|
||||
}
|
||||
}
|
||||
|
||||
private func logAction(_ action: ghostty_action_s, target: ghostty_target_s, tabId: UUID?, surfaceId: UUID?) {
|
||||
guard backgroundLogEnabled else { return }
|
||||
let targetLabel = target.tag == GHOSTTY_TARGET_SURFACE ? "surface" : "app"
|
||||
logBackground(
|
||||
"action event target=\(targetLabel) action=\(actionLabel(for: action)) tab=\(tabId?.uuidString ?? "nil") surface=\(surfaceId?.uuidString ?? "nil")"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -725,6 +865,12 @@ class GhosttyApp {
|
|||
|
||||
private func handleAction(target: ghostty_target_s, action: ghostty_action_s) -> Bool {
|
||||
if target.tag != GHOSTTY_TARGET_SURFACE {
|
||||
if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG ||
|
||||
action.tag == GHOSTTY_ACTION_CONFIG_CHANGE ||
|
||||
action.tag == GHOSTTY_ACTION_COLOR_CHANGE {
|
||||
logAction(action, target: target, tabId: nil, surfaceId: nil)
|
||||
}
|
||||
|
||||
if action.tag == GHOSTTY_ACTION_DESKTOP_NOTIFICATION {
|
||||
let actionTitle = action.action.desktop_notification.title
|
||||
.flatMap { String(cString: $0) } ?? ""
|
||||
|
|
@ -752,8 +898,9 @@ class GhosttyApp {
|
|||
|
||||
if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG {
|
||||
let soft = action.action.reload_config.soft
|
||||
logThemeAction("reload request target=app soft=\(soft)")
|
||||
performOnMain {
|
||||
GhosttyApp.shared.reloadConfiguration(soft: soft)
|
||||
GhosttyApp.shared.reloadConfiguration(soft: soft, source: "action.reload_config.app")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -761,16 +908,18 @@ class GhosttyApp {
|
|||
if action.tag == GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
action.action.color_change.kind == GHOSTTY_ACTION_COLOR_KIND_BACKGROUND {
|
||||
let change = action.action.color_change
|
||||
defaultBackgroundColor = NSColor(
|
||||
let resolvedColor = NSColor(
|
||||
red: CGFloat(change.r) / 255,
|
||||
green: CGFloat(change.g) / 255,
|
||||
blue: CGFloat(change.b) / 255,
|
||||
alpha: 1.0
|
||||
)
|
||||
if backgroundLogEnabled {
|
||||
logBackground("OSC background change (app target) color=\(defaultBackgroundColor)")
|
||||
}
|
||||
notifyDefaultBackgroundDidChange()
|
||||
applyDefaultBackground(
|
||||
color: resolvedColor,
|
||||
opacity: defaultBackgroundOpacity,
|
||||
source: "action.color_change.app",
|
||||
scope: .app
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
GhosttyApp.shared.applyBackgroundToKeyWindow()
|
||||
}
|
||||
|
|
@ -778,7 +927,11 @@ class GhosttyApp {
|
|||
}
|
||||
|
||||
if action.tag == GHOSTTY_ACTION_CONFIG_CHANGE {
|
||||
updateDefaultBackground(from: action.action.config_change.config)
|
||||
updateDefaultBackground(
|
||||
from: action.action.config_change.config,
|
||||
source: "action.config_change.app",
|
||||
scope: .app
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
GhosttyApp.shared.applyBackgroundToKeyWindow()
|
||||
}
|
||||
|
|
@ -789,6 +942,16 @@ class GhosttyApp {
|
|||
}
|
||||
guard let userdata = ghostty_surface_userdata(target.target.surface) else { return false }
|
||||
let surfaceView = Unmanaged<GhosttyNSView>.fromOpaque(userdata).takeUnretainedValue()
|
||||
if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG ||
|
||||
action.tag == GHOSTTY_ACTION_CONFIG_CHANGE ||
|
||||
action.tag == GHOSTTY_ACTION_COLOR_CHANGE {
|
||||
logAction(
|
||||
action,
|
||||
target: target,
|
||||
tabId: surfaceView.tabId,
|
||||
surfaceId: surfaceView.terminalSurface?.id
|
||||
)
|
||||
}
|
||||
|
||||
switch action.tag {
|
||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||
|
|
@ -998,19 +1161,26 @@ class GhosttyApp {
|
|||
}
|
||||
return true
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
updateDefaultBackground(from: action.action.config_change.config)
|
||||
updateDefaultBackground(
|
||||
from: action.action.config_change.config,
|
||||
source: "action.config_change.surface tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil")",
|
||||
scope: .surface
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.applyWindowBackgroundIfActive()
|
||||
}
|
||||
return true
|
||||
case GHOSTTY_ACTION_RELOAD_CONFIG:
|
||||
let soft = action.action.reload_config.soft
|
||||
logThemeAction(
|
||||
"reload request target=surface tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil") soft=\(soft)"
|
||||
)
|
||||
return performOnMain {
|
||||
if let surface = surfaceView.terminalSurface?.surface {
|
||||
GhosttyApp.shared.reloadConfiguration(for: surface, soft: soft)
|
||||
} else {
|
||||
GhosttyApp.shared.reloadConfiguration(soft: soft)
|
||||
}
|
||||
// Keep all runtime theme/default-background state in the same path.
|
||||
GhosttyApp.shared.reloadConfiguration(
|
||||
soft: soft,
|
||||
source: "action.reload_config.surface tab=\(surfaceView.tabId?.uuidString ?? "nil") surface=\(surfaceView.terminalSurface?.id.uuidString ?? "nil")"
|
||||
)
|
||||
return true
|
||||
}
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
|
|
@ -1102,7 +1272,8 @@ class GhosttyApp {
|
|||
}
|
||||
|
||||
func logBackground(_ message: String) {
|
||||
let line = "cmux bg: \(message)\n"
|
||||
let timestamp = Self.backgroundLogTimestampFormatter.string(from: Date())
|
||||
let line = "\(timestamp) cmux bg: \(message)\n"
|
||||
if let data = line.data(using: .utf8) {
|
||||
if FileManager.default.fileExists(atPath: backgroundLogURL.path) == false {
|
||||
FileManager.default.createFile(atPath: backgroundLogURL.path, contents: nil)
|
||||
|
|
@ -1963,6 +2134,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
|
||||
override func viewDidChangeEffectiveAppearance() {
|
||||
super.viewDidChangeEffectiveAppearance()
|
||||
if GhosttyApp.shared.backgroundLogEnabled {
|
||||
let bestMatch = effectiveAppearance.bestMatch(from: [.darkAqua, .aqua])
|
||||
GhosttyApp.shared.logBackground(
|
||||
"surface appearance changed tab=\(tabId?.uuidString ?? "nil") surface=\(terminalSurface?.id.uuidString ?? "nil") bestMatch=\(bestMatch?.rawValue ?? "nil")"
|
||||
)
|
||||
}
|
||||
applySurfaceColorScheme()
|
||||
}
|
||||
|
||||
|
|
@ -2105,10 +2282,22 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
? GHOSTTY_COLOR_SCHEME_DARK
|
||||
: GHOSTTY_COLOR_SCHEME_LIGHT
|
||||
if !force, appliedColorScheme == scheme {
|
||||
if GhosttyApp.shared.backgroundLogEnabled {
|
||||
let schemeLabel = scheme == GHOSTTY_COLOR_SCHEME_DARK ? "dark" : "light"
|
||||
GhosttyApp.shared.logBackground(
|
||||
"surface color scheme tab=\(tabId?.uuidString ?? "nil") surface=\(terminalSurface?.id.uuidString ?? "nil") bestMatch=\(bestMatch?.rawValue ?? "nil") scheme=\(schemeLabel) force=\(force) applied=false"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
appliedColorScheme = scheme
|
||||
if GhosttyApp.shared.backgroundLogEnabled {
|
||||
let schemeLabel = scheme == GHOSTTY_COLOR_SCHEME_DARK ? "dark" : "light"
|
||||
GhosttyApp.shared.logBackground(
|
||||
"surface color scheme tab=\(tabId?.uuidString ?? "nil") surface=\(terminalSurface?.id.uuidString ?? "nil") bestMatch=\(bestMatch?.rawValue ?? "nil") scheme=\(schemeLabel) force=\(force) applied=true"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
@ -3028,6 +3217,8 @@ enum GhosttyNotificationKey {
|
|||
static let title = "ghostty.title"
|
||||
static let backgroundColor = "ghostty.backgroundColor"
|
||||
static let backgroundOpacity = "ghostty.backgroundOpacity"
|
||||
static let backgroundEventId = "ghostty.backgroundEventId"
|
||||
static let backgroundSource = "ghostty.backgroundSource"
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
|
|
|
|||
|
|
@ -104,11 +104,13 @@ struct WorkspaceContentView: View {
|
|||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { notification in
|
||||
let payloadHex = (notification.userInfo?[GhosttyNotificationKey.backgroundColor] as? NSColor)?.hexString() ?? "nil"
|
||||
let eventId = (notification.userInfo?[GhosttyNotificationKey.backgroundEventId] as? NSNumber)?.uint64Value
|
||||
let source = (notification.userInfo?[GhosttyNotificationKey.backgroundSource] as? String) ?? "nil"
|
||||
// Payload ordering can lag across rapid config/theme updates.
|
||||
// Resolve from GhosttyApp.shared.defaultBackgroundColor to keep tabs aligned
|
||||
// with Ghostty's current runtime theme.
|
||||
refreshGhosttyAppearanceConfig(
|
||||
reason: "ghosttyDefaultBackgroundDidChange:payload=\(payloadHex)"
|
||||
reason: "ghosttyDefaultBackgroundDidChange:event=\(eventId.map(String.init) ?? "nil"):source=\(source):payload=\(payloadHex)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ struct cmuxApp: App {
|
|||
GhosttyApp.shared.openConfigurationInTextEdit()
|
||||
}
|
||||
Button("Reload Configuration") {
|
||||
GhosttyApp.shared.reloadConfiguration()
|
||||
GhosttyApp.shared.reloadConfiguration(source: "menu.reload_configuration")
|
||||
}
|
||||
.keyboardShortcut(",", modifiers: [.command, .shift])
|
||||
Divider()
|
||||
|
|
|
|||
|
|
@ -162,6 +162,39 @@ final class GhosttyConfigTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func testDefaultBackgroundUpdateScopePrioritizesSurfaceOverAppAndUnscoped() {
|
||||
XCTAssertTrue(
|
||||
GhosttyApp.shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: .unscoped,
|
||||
incomingScope: .app
|
||||
)
|
||||
)
|
||||
XCTAssertTrue(
|
||||
GhosttyApp.shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: .app,
|
||||
incomingScope: .surface
|
||||
)
|
||||
)
|
||||
XCTAssertTrue(
|
||||
GhosttyApp.shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: .surface,
|
||||
incomingScope: .surface
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
GhosttyApp.shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: .surface,
|
||||
incomingScope: .app
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
GhosttyApp.shouldApplyDefaultBackgroundUpdate(
|
||||
currentScope: .surface,
|
||||
incomingScope: .unscoped
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testClaudeCodeIntegrationDefaultsToEnabledWhenUnset() {
|
||||
let suiteName = "cmux.tests.claude-hooks.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
|
|
@ -352,14 +385,17 @@ final class GhosttyDefaultBackgroundNotificationDispatcherTests: XCTestCase {
|
|||
expectation.expectedFulfillmentCount = 1
|
||||
var postedUserInfos: [[AnyHashable: Any]] = []
|
||||
|
||||
let dispatcher = GhosttyDefaultBackgroundNotificationDispatcher(delay: 0.01) { userInfo in
|
||||
postedUserInfos.append(userInfo)
|
||||
expectation.fulfill()
|
||||
}
|
||||
let dispatcher = GhosttyDefaultBackgroundNotificationDispatcher(
|
||||
delay: 0.01,
|
||||
postNotification: { userInfo in
|
||||
postedUserInfos.append(userInfo)
|
||||
expectation.fulfill()
|
||||
}
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
dispatcher.signal(backgroundColor: dark, opacity: 0.95)
|
||||
dispatcher.signal(backgroundColor: light, opacity: 0.75)
|
||||
dispatcher.signal(backgroundColor: dark, opacity: 0.95, eventId: 1, source: "test.dark")
|
||||
dispatcher.signal(backgroundColor: light, opacity: 0.75, eventId: 2, source: "test.light")
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 1.0)
|
||||
|
|
@ -373,6 +409,14 @@ final class GhosttyDefaultBackgroundNotificationDispatcherTests: XCTestCase {
|
|||
0.75,
|
||||
accuracy: 0.0001
|
||||
)
|
||||
XCTAssertEqual(
|
||||
(postedUserInfos[0][GhosttyNotificationKey.backgroundEventId] as? NSNumber)?.uint64Value,
|
||||
2
|
||||
)
|
||||
XCTAssertEqual(
|
||||
postedUserInfos[0][GhosttyNotificationKey.backgroundSource] as? String,
|
||||
"test.light"
|
||||
)
|
||||
}
|
||||
|
||||
func testSignalAcrossSeparateBurstsPostsMultipleNotifications() {
|
||||
|
|
@ -386,16 +430,19 @@ final class GhosttyDefaultBackgroundNotificationDispatcherTests: XCTestCase {
|
|||
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()
|
||||
}
|
||||
let dispatcher = GhosttyDefaultBackgroundNotificationDispatcher(
|
||||
delay: 0.01,
|
||||
postNotification: { 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)
|
||||
dispatcher.signal(backgroundColor: dark, opacity: 1.0, eventId: 1, source: "test.dark")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
dispatcher.signal(backgroundColor: light, opacity: 1.0)
|
||||
dispatcher.signal(backgroundColor: light, opacity: 1.0, eventId: 2, source: "test.light")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue