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
36 changes: 25 additions & 11 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2098,8 +2098,9 @@ class GhosttyApp {
return
}

let fallbackShouldUseHostLayerBackground = usesHostLayerBackground(for: fallbackConfig)
loadInlineGhosttyConfig(
"macos-background-from-layer = true",
"macos-background-from-layer = \(fallbackShouldUseHostLayerBackground)",
into: fallbackConfig,
prefix: "cmux-renderer-bg",
logLabel: "renderer background (fallback)"
Expand All @@ -2112,7 +2113,7 @@ class GhosttyApp {
)
loadCmuxOwnedGhosttyKeybindOverrides(fallbackConfig)
let fallbackRenderingModeChanged = setUsesHostLayerBackground(
true,
fallbackShouldUseHostLayerBackground,
source: "initialize.fallbackConfig"
)
ghostty_config_finalize(fallbackConfig)
Expand Down Expand Up @@ -2248,15 +2249,17 @@ class GhosttyApp {
}
#endif
loadCJKFontFallbackIfNeeded(config)
let shouldUseHostLayerBackground = usesHostLayerBackground(for: config)
let renderingModeChanged = setUsesHostLayerBackground(
true,
shouldUseHostLayerBackground,
source: "loadDefaultConfigFilesWithLegacyFallback"
)
// Let cmux own the window-level backdrop once, while Ghostty keeps
// rendering text, cell backgrounds, and background images. This avoids
// separate translucent fills for terminal and chrome surfaces.
// Let Ghostty paint solid opaque terminal backgrounds so default cells
// and explicit ANSI background cells share one renderer/compositor path.
// Host-layer ownership remains required for translucent and blurred
// terminal backgrounds.
loadInlineGhosttyConfig(
"macos-background-from-layer = true",
"macos-background-from-layer = \(shouldUseHostLayerBackground)",
into: config,
prefix: "cmux-renderer-bg",
logLabel: "renderer background"
Expand Down Expand Up @@ -3214,10 +3217,7 @@ class GhosttyApp {
let resolvedCursorText = ghosttyColorValue(from: config, key: "cursor-text", fallback: baseline.cursorTextColor)
let resolvedSelectionBackground = ghosttyColorValue(from: config, key: "selection-background", fallback: baseline.selectionBackground)
let resolvedSelectionForeground = ghosttyColorValue(from: config, key: "selection-foreground", fallback: baseline.selectionForeground)
var opacity = baseline.backgroundOpacity
let opacityKey = "background-opacity"
_ = ghostty_config_get(config, &opacity, opacityKey, UInt(opacityKey.lengthOfBytes(using: .utf8)))
opacity = min(1.0, max(0.0, opacity))
let opacity = defaultBackgroundOpacityValue(from: config)
let backgroundBlur = defaultBackgroundBlurValue(from: config)
applyDefaultBackground(
color: resolvedColor,
Expand All @@ -3234,6 +3234,20 @@ class GhosttyApp {
)
}

private func defaultBackgroundOpacityValue(from config: ghostty_config_t) -> Double {
var opacity = Self.fallbackAppearanceConfig.backgroundOpacity
let key = "background-opacity"
_ = ghostty_config_get(config, &opacity, key, UInt(key.lengthOfBytes(using: .utf8)))
return Double(WindowAppearanceSnapshot.clampedOpacity(opacity))
}

private func usesHostLayerBackground(for config: ghostty_config_t) -> Bool {
WindowAppearanceSnapshot.usesHostLayerBackground(
backgroundOpacity: defaultBackgroundOpacityValue(from: config),
backgroundBlur: defaultBackgroundBlurValue(from: config)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runtime config changes leave rendering mode stale

Medium Severity

The GHOSTTY_ACTION_CONFIG_CHANGE handler calls updateDefaultBackground (which updates stored opacity/blur) but never recalculates usesHostLayerBackground. Previously this was safe because usesHostLayerBackground was always true. Now that the initialization paths dynamically set it to false for opaque unblurred backgrounds, a runtime config change that transitions opacity from opaque to translucent leaves the flag stale at false, causing WindowAppearanceSnapshot.current(...) to produce a snapshot with terminalRenderingMode = .ghosttyRendererOwnedBackgroundImage when it needs .windowHostBackdrop.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 75b2ece. Configure here.

private func defaultBackgroundBlurValue(from config: ghostty_config_t) -> GhosttyBackgroundBlur {
var value: Int16 = 0
let key = "background-blur"
Expand Down
27 changes: 27 additions & 0 deletions Sources/Windowing/WindowAppearanceSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ struct WindowGlassSettingsSnapshot {
}

struct WindowAppearanceSnapshot {
/// Treat opacity values within floating-point round-trip tolerance of 1.0
/// as opaque so user-configured `1.0` does not accidentally trigger host
/// ownership after Double/CGFloat conversions.
private static let opaqueBackgroundOwnershipThreshold: CGFloat = 0.999

let terminalBackgroundColor: NSColor
let terminalBackgroundOpacity: CGFloat
let terminalBackgroundBlur: GhosttyBackgroundBlur
Expand Down Expand Up @@ -285,6 +290,28 @@ struct WindowAppearanceSnapshot {
usesHostLayerBackground ? .windowHostBackdrop : .ghosttyRendererOwnedBackgroundImage
}

static func usesHostLayerBackground(
backgroundOpacity: Double,
backgroundBlur: GhosttyBackgroundBlur
) -> Bool {
if backgroundBlur != .disabled {
return true
}
return clampedOpacity(backgroundOpacity) < opaqueBackgroundOwnershipThreshold
}

static func terminalRenderingMode(
backgroundOpacity: Double,
backgroundBlur: GhosttyBackgroundBlur
) -> GhosttyTerminalBackdropRenderingMode {
terminalRenderingMode(
usesHostLayerBackground: usesHostLayerBackground(
backgroundOpacity: backgroundOpacity,
backgroundBlur: backgroundBlur
)
)
}

var compositedTerminalBackgroundColor: NSColor {
terminalBackgroundColor.withAlphaComponent(terminalBackgroundOpacity)
}
Expand Down
53 changes: 52 additions & 1 deletion cmuxTests/WindowAppearanceSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,54 @@ final class WindowAppearanceSnapshotTests: XCTestCase {
XCTAssertEqual(plan.windowBackgroundColor.hexString(includeAlpha: true), "#272822FF")
}

func testOpaqueUnblurredTerminalBackgroundIsRendererOwned() {
let snapshot = makeSnapshot(
unifySurfaceBackdrops: false,
backgroundOpacity: 1.0,
backgroundBlur: .disabled
)
let policy = snapshot.policy(for: .windowRoot)

XCTAssertNil(policy.hostLayerBackgroundColor)
guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else {
XCTFail("expected terminal backdrop policy")
return
}
XCTAssertEqual(renderingMode, .ghosttyRendererOwnedBackgroundImage)
}
Comment on lines +113 to +127

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing complementary regression assertions

The new test verifies the opaque+unblurred → renderer-owned direction, but the inverse paths — translucent opacity → host-layer, and backgroundBlur != .disabled → host-layer — are not asserted. Without those, a future refactor of usesHostLayerBackground that accidentally widened the renderer-owned path (e.g., removing the blur check) would not be caught by CI. Consider adding at least one translucent case and one blur-enabled case that assert renderingMode == .windowHostBackdrop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by adding inverse regression assertions for translucent and blurred terminal backgrounds staying host-layer-owned.

— Claude Code


func testTranslucentTerminalBackgroundStaysHostLayerOwned() {
let snapshot = makeSnapshot(
unifySurfaceBackdrops: false,
backgroundOpacity: 0.9,
backgroundBlur: .disabled
)
let policy = snapshot.policy(for: .windowRoot)

XCTAssertNotNil(policy.hostLayerBackgroundColor)
guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else {
XCTFail("expected terminal backdrop policy")
return
}
XCTAssertEqual(renderingMode, .windowHostBackdrop)
}

func testBlurredTerminalBackgroundStaysHostLayerOwned() {
let snapshot = makeSnapshot(
unifySurfaceBackdrops: false,
backgroundOpacity: 1.0,
backgroundBlur: .radius(20)
)
let policy = snapshot.policy(for: .windowRoot)

XCTAssertNotNil(policy.hostLayerBackgroundColor)
guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else {
XCTFail("expected terminal backdrop policy")
return
}
XCTAssertEqual(renderingMode, .windowHostBackdrop)
}

func testDebugBackgroundGlassUsesWindowGlassPhase() {
let snapshot = makeSnapshot(
unifySurfaceBackdrops: false,
Expand Down Expand Up @@ -137,7 +185,10 @@ final class WindowAppearanceSnapshotTests: XCTestCase {
terminalBackgroundColor: NSColor(hex: "#272822") ?? .black,
terminalBackgroundOpacity: backgroundOpacity,
terminalBackgroundBlur: backgroundBlur,
terminalRenderingMode: .windowHostBackdrop,
terminalRenderingMode: WindowAppearanceSnapshot.terminalRenderingMode(
backgroundOpacity: Double(backgroundOpacity),
backgroundBlur: backgroundBlur
),
unifySurfaceBackdrops: unifySurfaceBackdrops,
sidebarSettings: SidebarBackdropSettingsSnapshot(
materialRawValue: SidebarMaterialOption.sidebar.rawValue,
Expand Down
Loading