Skip to content

fix: apply background-opacity and background-blur to terminal rendering area (#879) - #1858

Merged
lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
martinezhermes:fix/background-opacity-blur-879
Mar 21, 2026
Merged

lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
martinezhermes:fix/background-opacity-blur-879

Conversation

@martinezhermes

@martinezhermes martinezhermes commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Fixes #879. Two independent bugs prevented background-opacity and background-blur from affecting the terminal rendering area.

Root causes & fixes

1. background-opacity — opaque Metal layer (makeBackingLayer)

GhosttyNSView had no makeBackingLayer() override, so AppKit supplied a generic CALayer as the backing layer. libghostty expects the view's own layer to be a CAMetalLayer — evidenced by the (view.layer as? CAMetalLayer) != nil guard already in the file — and when it finds one it uses it directly for Metal rendering. Without it, the Metal surface defaulted to isOpaque = true, making the terminal completely opaque regardless of the configured background-opacity.

Fix: override makeBackingLayer() in GhosttyNSView to return a CAMetalLayer with:

  • isOpaque = false — allows the compositor to blend the layer
  • pixelFormat = .bgra8Unorm — standard BGRA format with alpha channel
  • framebufferOnly = false — lets the macOS compositor read the drawable when blending translucent/blurred window layers; matches standalone Ghostty's SurfaceView

2. background-blur — ghostty_set_window_background_blur never called

ghostty_set_window_background_blur is declared in ghostty.h but was never invoked in cmux. Its Zig implementation calls CGSSetWindowBackgroundBlurRadius, a private CoreGraphics compositor API that sets the blur radius on the window's compositor layer — not an NSVisualEffectView. It reads background-blur and background-opacity from the app config internally, and is a no-op when opacity ≥ 1.0 or blur is disabled/zero. Because it is a direct compositor setter, it is idempotent.

Fix: add applyWindowBlurIfNeeded(_ window: NSWindow) on GhosttyApp that calls ghostty_set_window_background_blur unconditionally (the function self-guards). Called from applyBackgroundToKeyWindow() and applyWindowBackgroundIfActive() whenever the window is set transparent — covering both initial window setup and per-surface activation.

Reviewer notes

  • The ghostty_config_get read for background-blur was removed from an earlier draft. BackgroundBlur is a Zig tagged union whose cval() returns i16; reading it via ghostty_config_get with a Swift integer pointer would silently fail. The Zig function reads its own config, so no Swift-side config read is needed.
  • CGSSetWindowBackgroundBlurRadius is idempotent — repeated calls from focus/tab events do not stack, they overwrite.

Test plan

  • Set background-opacity = 0.8 and background-blur = 1 in ~/Library/Application Support/com.mitchellh.ghostty/config
  • Launch cmux — the terminal rendering area should be semi-transparent with a blurred backdrop
  • Verify the tab bar and terminal area both appear semi-transparent (matching standalone Ghostty behavior)
  • Set background-opacity = 1 — confirm terminal returns to fully opaque with no blur

Summary by CodeRabbit

  • New Features

    • Window background blur is now applied for transparent windows, providing consistent translucent effects.
    • Backing layer updated to improve rendering of transparent/blurred content.
  • Style

    • Enhanced visual quality of terminal windows when using transparent or blurred backgrounds.

@vercel

vercel Bot commented Mar 20, 2026

Copy link
Copy Markdown

@martinezhermes is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 171f7eca-ba3a-43ec-b9de-726dc82df7f6

📥 Commits

Reviewing files that changed from the base of the PR and between 826ce60 and 1235daf.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

📝 Walkthrough

Walkthrough

Adds window blur application and a Metal-backed translucent layer: a new helper applies blur to transparent NSWindows (calling into libghostty), and GhosttyNSView now provides a non-opaque CAMetalLayer for correct translucent rendering.

Changes

Cohort / File(s) Summary
Window blur & backing layer
Sources/GhosttyTerminalView.swift
Added GhosttyApp.applyWindowBlurIfNeeded(_:) and wired it into paths that set window.isOpaque = false; added GhosttyNSView.override makeBackingLayer() to return a non-opaque CAMetalLayer (pixelFormat = .bgra8Unorm, framebufferOnly = false) so terminal view backing matches translucent/blur rendering.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Client/UI
participant App as GhosttyApp
participant View as GhosttyNSView
participant Lib as libghostty
participant Win as NSWindow

UI->>App: decide to use clear window background
App->>Win: set backgroundColor to transparent, set isOpaque = false
App->>App: applyWindowBlurIfNeeded(window)
App->>Lib: ghostty_set_window_background_blur(app, windowPtr)
UI->>View: create/attach view
View->>View: makeBackingLayer() → CAMetalLayer (bgra8Unorm, opaque=false, framebufferOnly=false)
View->>Win: attach layer for translucent rendering

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nudge the glass with twilight fur,
A shimmer wakes where pixels stir,
Metal leaf breathes soft and clear,
Windows hum — the blur is here. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing background-opacity and background-blur application to the terminal rendering area, addressing issue #879.
Description check ✅ Passed The description covers the problem, root causes with detailed fixes, reviewer notes, and a test plan. However, the Demo Video URL section is empty and no testing checklist items are marked complete.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #879: implementing makeBackingLayer() override to enable opacity on the Metal layer and calling ghostty_set_window_background_blur to enable blur effects on terminal rendering.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing background-opacity and background-blur application; no unrelated modifications detected in the two methods added to GhosttyApp and GhosttyNSView.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 1 file


Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

@greptile-apps

greptile-apps Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two root causes that prevented background-opacity and background-blur from working in the cmux terminal rendering area: a missing makeBackingLayer() override that caused AppKit to supply an opaque CALayer instead of a CAMetalLayer, and the absence of any call to ghostty_set_window_background_blur.

Key changes:

  • GhosttyNSView.makeBackingLayer() now returns a CAMetalLayer with isOpaque = false and framebufferOnly = false, giving libghostty the transparent Metal surface it requires.
  • New GhosttyApp.applyWindowBlurIfNeeded(_ window:) reads the background-blur config value and, when non-zero, calls ghostty_set_window_background_blur.
  • The new helper is wired into both applyBackgroundToKeyWindow() and GhosttyNSView.applyWindowBackgroundIfActive().

Issues flagged:

  • ghostty_set_window_background_blur is a legacy C API that may add an NSVisualEffectView on each invocation. Both call sites fire on routine UI events (tab switches, focus changes, config reloads), so the call can occur many times per session without an idempotency guard — risking stacked visual-effect layers and a memory leak.
  • The UInt32 type used when calling ghostty_config_get for background-blur may not match the underlying Zig type (which could be bool or u8 in older libghostty versions). A type mismatch causes ghostty_config_get to return false silently, leaving blur permanently disabled regardless of the config value.

Confidence Score: 2/5

  • The PR has correctness risks that may silently prevent the feature from working and could cause cumulative memory/visual issues in production.
  • Two P1 logic issues: the blur helper lacks an idempotency guard (repeated ghostty_set_window_background_blur calls on every background-apply event), and the UInt32 type used for the background-blur config read may silently fail if the underlying Zig type doesn't match — meaning blur may never actually activate. Both issues could make the core feature broken or degrade stability over time without obvious error messages.
  • Sources/GhosttyTerminalView.swift — specifically the new applyWindowBlurIfNeeded function (lines 2428–2435) and its call sites.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds makeBackingLayer() override on GhosttyNSView to provide an explicit CAMetalLayer for transparency, and adds applyWindowBlurIfNeeded on GhosttyApp to invoke ghostty_set_window_background_blur. Two issues: (1) the blur helper is called on every background-apply event without an idempotency guard, risking stacked NSVisualEffectView layers; (2) the UInt32 type used for the background-blur config read may not match the underlying Zig type, silently preventing blur from ever activating.

Sequence Diagram

sequenceDiagram
    participant AppKit
    participant GhosttyNSView
    participant GhosttyApp
    participant libghostty

    Note over GhosttyNSView: View initialisation
    AppKit->>GhosttyNSView: makeBackingLayer()
    GhosttyNSView-->>AppKit: CAMetalLayer(isOpaque=false, framebufferOnly=false)
    AppKit->>GhosttyNSView: setup() → wantsLayer=true

    Note over GhosttyNSView: Background/opacity change event
    AppKit->>GhosttyNSView: applyWindowBackgroundIfActive()
    GhosttyNSView->>GhosttyNSView: effectiveBackgroundColor()
    alt opacity < threshold (transparent)
        GhosttyNSView->>AppKit: window.isOpaque = false
        GhosttyNSView->>GhosttyApp: applyWindowBlurIfNeeded(window)
        GhosttyApp->>libghostty: ghostty_config_get(config, &blurRadius, "background-blur", ...)
        libghostty-->>GhosttyApp: blurRadius (UInt32)
        alt blurRadius > 0
            GhosttyApp->>libghostty: ghostty_set_window_background_blur(app, window)
            Note over libghostty: Adds NSVisualEffectView ⚠️ no idempotency guard
        end
    else opacity >= threshold (opaque)
        GhosttyNSView->>AppKit: window.isOpaque = true
    end
Loading

Last reviewed commit: "fix: apply backgroun..."

Comment on lines +3862 to +3868
override func makeBackingLayer() -> CALayer {
let metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.isOpaque = false
metalLayer.framebufferOnly = false
return metalLayer
}

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 framebufferOnly = false should be documented

Setting framebufferOnly = false on a CAMetalLayer disables the Metal driver optimisation that lets it treat drawable textures as write-only. This is measurable in GPU-bound scenarios (especially on integrated graphics). While it may be required for the system compositor to read back the drawable during window blending, leaving it undocumented makes future maintainers likely to revert it assuming it was accidentally left off.

Adding a brief comment explaining the necessity would help:

Suggested change
override func makeBackingLayer() -> CALayer {
let metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.isOpaque = false
metalLayer.framebufferOnly = false
return metalLayer
}
override func makeBackingLayer() -> CALayer {
let metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.isOpaque = false
// Required for the macOS compositor to composite this layer with
// the blurred/transparent window backdrop; matches SurfaceView behaviour.
metalLayer.framebufferOnly = false
return metalLayer
}

Comment on lines +2428 to +2435
func applyWindowBlurIfNeeded(_ window: NSWindow) {
guard let app = self.app, let config = self.config else { return }
var blurRadius: UInt32 = 0
let key = "background-blur"
_ = ghostty_config_get(config, &blurRadius, key, UInt(key.lengthOfBytes(using: .utf8)))
guard blurRadius > 0 else { return }
ghostty_set_window_background_blur(app, Unmanaged.passUnretained(window).toOpaque())
}

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.

P1 ghostty_set_window_background_blur called repeatedly without idempotency guard

applyWindowBlurIfNeeded is invoked from both applyBackgroundToKeyWindow() and applyWindowBackgroundIfActive(). Both callers are triggered on routine UI events — tab switches, focus changes, viewDidMoveToWindow, and config reloads — meaning this path can fire many times per session.

The ghostty_set_window_background_blur function is annotated in ghostty.h as a legacy/low-level API ("Don't use these unless you know what you're doing"). If its implementation adds an NSVisualEffectView to the window without first checking whether one already exists, each successive call stacks another view, producing visual artifacts and a steady memory leak.

Consider tracking whether blur has already been applied to a given window (e.g., using a weak NSHashTable<NSWindow> property on GhosttyApp) and skipping the C call if the window is already in the set. This ensures ghostty_set_window_background_blur is called at most once per window instance.

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment on lines +2430 to +2432
var blurRadius: UInt32 = 0
let key = "background-blur"
_ = ghostty_config_get(config, &blurRadius, key, UInt(key.lengthOfBytes(using: .utf8)))

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.

P1 background-blur config type may not match UInt32

ghostty_config_get resolves the requested config field by matching the byte-size and type-tag of the output pointer against the field's Zig type. If the backing Zig type for background-blur is bool (as it was in earlier Ghostty versions) or a narrower integer such as u8, the runtime type check inside Zig will fail, ghostty_config_get returns false, blurRadius stays 0, and the blur call is never reached — silently breaking the feature.

For comparison, the existing config reads in this file use Swift types that directly mirror the underlying Zig types (e.g., Double for background-opacity → Zig f64, Bool for focus-follows-mouse → Zig bool, CUnsignedInt for bell-features → Zig u32).

Before shipping, please confirm the exact Zig typedef for background-blur in the currently-linked libghostty, and update the Swift variable type accordingly. If the type is bool, the check should use a Bool variable rather than a UInt32.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)

2409-2435: ⚠️ Potential issue | 🟠 Major

Clear blur explicitly when it should be off.

Line 2414 and Line 3952 only hit the blur path while the window is transparent, and Line 2433 exits immediately when background-blur is 0. Since the window blur state is sticky, toggling blur off or returning to an opaque background can leave the old blur attached. Please make the blur update unconditional for background changes and include an explicit disable/reset path when blur should be off.

Based on learnings: "CGS window background blur is stateful. Always call cmuxApplyBackgroundBlur(to: NSWindow, radius: Int) on background updates and pass radius 0 when blur should be disabled (opacity >= 1.0 or configured radius == 0). Both apply paths (GhosttyApp.applyBackgroundToKeyWindow and GhosttyNSView.applyWindowBackgroundIfActive) follow this pattern."

Also applies to: 3933-3953

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2409 - 2435,
applyBackgroundToKeyWindow currently only calls applyWindowBlurIfNeeded in the
transparent branch and applyWindowBlurIfNeeded early-returns when the config
blur is 0, leaving the OS blur state sticky; update both
applyBackgroundToKeyWindow and GhosttyNSView.applyWindowBackgroundIfActive to
always invoke the blur update on background changes and change
applyWindowBlurIfNeeded to never return early — read the "background-blur" value
(ghostty_config_get) and compute an effective radius (use 0 when blur is
disabled or when opacity >= 1.0), then always call
ghostty_set_window_background_blur / cmuxApplyBackgroundBlur(to:radius:) passing
that radius so blur is explicitly applied or cleared.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2409-2435: applyBackgroundToKeyWindow currently only calls
applyWindowBlurIfNeeded in the transparent branch and applyWindowBlurIfNeeded
early-returns when the config blur is 0, leaving the OS blur state sticky;
update both applyBackgroundToKeyWindow and
GhosttyNSView.applyWindowBackgroundIfActive to always invoke the blur update on
background changes and change applyWindowBlurIfNeeded to never return early —
read the "background-blur" value (ghostty_config_get) and compute an effective
radius (use 0 when blur is disabled or when opacity >= 1.0), then always call
ghostty_set_window_background_blur / cmuxApplyBackgroundBlur(to:radius:) passing
that radius so blur is explicitly applied or cleared.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b47948ac-8396-47bd-8f55-ff7624483f86

📥 Commits

Reviewing files that changed from the base of the PR and between 730d64b and 826ce60.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

…ng area

Two root causes for issue #879:

1. GhosttyNSView was missing makeBackingLayer(), so AppKit provided a
   generic CALayer as the view's backing layer. libghostty expects
   (view.layer as? CAMetalLayer) != nil to set up Metal rendering on
   the existing layer. Without a CAMetalLayer backing layer, the Metal
   surface defaulted to isOpaque=true, making the terminal area fully
   opaque regardless of background-opacity config.

   Fix: override makeBackingLayer() to return a CAMetalLayer with
   isOpaque=false and bgra8Unorm pixel format (matching standalone
   Ghostty's SurfaceView behavior).

2. ghostty_set_window_background_blur(app, window) is exposed in
   ghostty.h but was never called in cmux. Without this call the
   macOS window never gets the NSVisualEffectView blur backdrop that
   background-blur requires.

   Fix: add applyWindowBlurIfNeeded() on GhosttyApp that reads
   background-blur from config via ghostty_config_get and calls
   ghostty_set_window_background_blur when the value is non-zero.
   Called from applyBackgroundToKeyWindow() and
   applyWindowBackgroundIfActive() whenever the window is made
   transparent.

Fixes #879

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@martinezhermes
martinezhermes force-pushed the fix/background-opacity-blur-879 branch from 826ce60 to 1235daf Compare March 20, 2026 09:16
@lawrencecchen
lawrencecchen merged commit 6ff8157 into manaflow-ai:main Mar 21, 2026
2 of 3 checks passed
@lawrencecchen

Copy link
Copy Markdown
Contributor

Thank you for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

background-opacity not applied to terminal rendering area (only applied to tab bar)

2 participants