Cache Ghostty config loads on UI path

Fixes CMUXTERM-MACOS-A7
This commit is contained in:
Lawrence Chen 2026-02-25 15:15:34 -08:00
parent 12e9c1e317
commit 4b98feb263
3 changed files with 109 additions and 4 deletions

View file

@ -2,11 +2,14 @@ import Foundation
import AppKit
struct GhosttyConfig {
enum ColorSchemePreference {
enum ColorSchemePreference: Hashable {
case light
case dark
}
private static let loadCacheLock = NSLock()
private static var cachedConfigsByColorScheme: [ColorSchemePreference: GhosttyConfig] = [:]
var fontFamily: String = "Menlo"
var fontSize: CGFloat = 12
var theme: String?
@ -45,7 +48,45 @@ struct GhosttyConfig {
return backgroundColor.darken(by: isLightBackground ? 0.08 : 0.4)
}
static func load() -> GhosttyConfig {
static func load(
preferredColorScheme: ColorSchemePreference? = nil,
useCache: Bool = true,
loadFromDisk: (_ preferredColorScheme: ColorSchemePreference) -> GhosttyConfig = Self.loadFromDisk
) -> GhosttyConfig {
let resolvedColorScheme = preferredColorScheme ?? currentColorSchemePreference()
if useCache, let cached = cachedLoad(for: resolvedColorScheme) {
return cached
}
let loaded = loadFromDisk(resolvedColorScheme)
if useCache {
storeCachedLoad(loaded, for: resolvedColorScheme)
}
return loaded
}
static func invalidateLoadCache() {
loadCacheLock.lock()
cachedConfigsByColorScheme.removeAll()
loadCacheLock.unlock()
}
private static func cachedLoad(for colorScheme: ColorSchemePreference) -> GhosttyConfig? {
loadCacheLock.lock()
defer { loadCacheLock.unlock() }
return cachedConfigsByColorScheme[colorScheme]
}
private static func storeCachedLoad(
_ config: GhosttyConfig,
for colorScheme: ColorSchemePreference
) {
loadCacheLock.lock()
cachedConfigsByColorScheme[colorScheme] = config
loadCacheLock.unlock()
}
private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig {
var config = GhosttyConfig()
// Match Ghostty's default load order on macOS.
@ -64,7 +105,12 @@ struct GhosttyConfig {
// Load theme if specified
if let themeName = config.theme {
config.loadTheme(themeName)
config.loadTheme(
themeName,
environment: ProcessInfo.processInfo.environment,
bundleResourceURL: Bundle.main.resourceURL,
preferredColorScheme: preferredColorScheme
)
}
return config

View file

@ -117,6 +117,7 @@ struct WorkspaceContentView: View {
syncBonsplitNotificationBadges()
}
.onReceive(NotificationCenter.default.publisher(for: .ghosttyConfigDidReload)) { _ in
GhosttyConfig.invalidateLoadCache()
refreshGhosttyAppearanceConfig(reason: "ghosttyConfigDidReload")
}
.onChange(of: colorScheme) { oldValue, newValue in
@ -175,7 +176,7 @@ struct WorkspaceContentView: View {
static func resolveGhosttyAppearanceConfig(
reason: String = "unspecified",
backgroundOverride: NSColor? = nil,
loadConfig: () -> GhosttyConfig = GhosttyConfig.load,
loadConfig: () -> GhosttyConfig = { GhosttyConfig.load() },
defaultBackground: () -> NSColor = { GhosttyApp.shared.defaultBackgroundColor }
) -> GhosttyConfig {
var next = loadConfig()

View file

@ -126,6 +126,64 @@ final class GhosttyConfigTests: XCTestCase {
XCTAssertEqual(rgb255(config.backgroundColor), RGB(red: 253, green: 246, blue: 227))
}
func testLoadCachesPerColorScheme() {
GhosttyConfig.invalidateLoadCache()
defer { GhosttyConfig.invalidateLoadCache() }
var loadCount = 0
let loadFromDisk: (GhosttyConfig.ColorSchemePreference) -> GhosttyConfig = { scheme in
loadCount += 1
var config = GhosttyConfig()
config.fontFamily = "\(scheme)-\(loadCount)"
return config
}
let lightFirst = GhosttyConfig.load(
preferredColorScheme: .light,
loadFromDisk: loadFromDisk
)
let lightSecond = GhosttyConfig.load(
preferredColorScheme: .light,
loadFromDisk: loadFromDisk
)
let darkFirst = GhosttyConfig.load(
preferredColorScheme: .dark,
loadFromDisk: loadFromDisk
)
XCTAssertEqual(loadCount, 2)
XCTAssertEqual(lightFirst.fontFamily, "light-1")
XCTAssertEqual(lightSecond.fontFamily, "light-1")
XCTAssertEqual(darkFirst.fontFamily, "dark-2")
}
func testLoadCacheInvalidationForcesReload() {
GhosttyConfig.invalidateLoadCache()
defer { GhosttyConfig.invalidateLoadCache() }
var loadCount = 0
let loadFromDisk: (GhosttyConfig.ColorSchemePreference) -> GhosttyConfig = { _ in
loadCount += 1
var config = GhosttyConfig()
config.fontFamily = "reload-\(loadCount)"
return config
}
let first = GhosttyConfig.load(
preferredColorScheme: .dark,
loadFromDisk: loadFromDisk
)
GhosttyConfig.invalidateLoadCache()
let second = GhosttyConfig.load(
preferredColorScheme: .dark,
loadFromDisk: loadFromDisk
)
XCTAssertEqual(loadCount, 2)
XCTAssertEqual(first.fontFamily, "reload-1")
XCTAssertEqual(second.fontFamily, "reload-2")
}
func testLegacyConfigFallbackUsesLegacyFileWhenConfigGhosttyIsEmpty() {
XCTAssertTrue(
GhosttyApp.shouldLoadLegacyGhosttyConfig(