Skip to content

Add adaptive and configurable terminal backgrounds - #178

Open
OpenSource03 wants to merge 8 commits into
thdxg:mainfrom
OpenSource03:feat/terminal-background-customization
Open

Add adaptive and configurable terminal backgrounds#178
OpenSource03 wants to merge 8 commits into
thdxg:mainfrom
OpenSource03:feat/terminal-background-customization

Conversation

@OpenSource03

@OpenSource03 OpenSource03 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

Adds an opt-in Appearance setting that matches full-screen terminal apps such as OpenCode: a single pane can tint the whole window, while split panes adapt independently and leave the surrounding chrome unchanged.

Why

Full-screen TUIs can paint a background that clashes with transparent terminal padding, the sidebar, titlebar, and Liquid Glass.

Screenshots

Properly adapts to split view

CleanShot 2026-07-21 at 02 09 54 CleanShot 2026-07-21 at 02 10 11

With only single pane, whole app adapts to the TUI color

CleanShot 2026-07-21 at 02 10 39

With feature off

CleanShot 2026-07-21 at 02 11 39

How

  • Keeps the Ghostty config as the source of truth for terminal colors; the adaptive tint is a transient presentation layer on top of it.
  • Prefers explicit OSC 11 background changes, with bounded IOSurface sampling and two-observation stabilization for apps that do not report a color.
  • Samples every visible pane independently. Single-pane tabs publish a window-wide tint; split tabs fill only matching panes.
  • Routes chrome colors through MactermTheme and selects semantic light/dark appearance using WCAG contrast.
  • Performs no sampling and owns no monitoring timer while disabled.

Verified

  • Repository format, lint, and full test scripts pass after merging current upstream main
  • Built and ran the change in the app
  • Confirmed Grok adapts without switching tabs
  • Confirmed split-pane colors remain pane-local while alternating focus
  • Confirmed disabling the setting restores the configured theme
  • Added focused tests for contrast, IOSurface detection, stabilization, and split presentation

Notes for reviewers

The feature is off by default. IOSurface is linked explicitly because sampling reads the renderer-owned BGRA8 surface directly; it does not use screen capture or request additional permissions.

Summary by CodeRabbit

Summary

  • New Features

    • Added adaptive terminal chrome to mirror app background tints, including temporary per-pane adaptive background fills.
    • Added terminal background settings to choose background source (Ghostty-resolved vs custom) and edit a remembered custom override color.
  • Bug Fixes

    • Improved adaptive/background update synchronization with debounced config reloads.
    • Refined unfocused split dimming to keep adaptive-background panes color-accurate.
    • Updated background/color change handling to refresh UI promptly from Ghostty updates.
  • Tests

    • Added/expanded coverage for adaptive detection, override parsing/generation, and theme/contrast math.

@github-actions github-actions Bot added area:ui Views, Settings UI area:terminal Terminal surface, ghostty integration area:state AppState, models, persistence area:config Ghostty config wrappers area:tests Test changes labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds terminal background source preferences, custom color overrides, IOSurface-based adaptive background detection, stabilized pane/window presentation, contrast-aware theming, lifecycle callbacks, and tests for color conversion, detection, stabilization, and theme behavior.

Changes

Adaptive terminal backgrounds

Layer / File(s) Summary
Background override contract and preferences
Macterm/Config/TerminalBackgroundOverride.swift, Macterm/App/Preferences.swift, Macterm/Config/MactermConfig.swift, Macterm/Settings/SettingsView.swift, MactermTests/Config/*
Adds Ghostty and custom terminal background modes, persisted override colors, debounced config reloads, generated override lines, terminal settings controls, and validation coverage.
Background detection and stabilization
Macterm/Ghostty/AdaptiveTerminalBackground.swift, MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift, project.yml
Adds IOSurface sampling, dominant-color detection, quantized stabilization, pane/window presentation rules, framework linkage, and detector tests.
Adaptive chrome orchestration
Macterm/Ghostty/AdaptiveTerminalChrome.swift, Macterm/Ghostty/GhosttyCallbacks.swift, Macterm/Views/Terminal/*, Macterm/Views/MainWindow.swift, Macterm/Views/QuickTerminal.swift, Macterm/App/AppState.swift, Macterm/Model/SplitNode.swift
Connects terminal events to adaptive sampling, verification timers, pane presentation, state propagation, and lifecycle cleanup.
Theme and window presentation
Macterm/Ghostty/GhosttyApp.swift, Macterm/Ghostty/Theme*.swift, Macterm/App/MactermApp.swift, Macterm/Views/WindowAppearance.swift, Macterm/Views/SplitTreeView.swift, Macterm/Views/TerminalPane.swift, MactermTests/Ghostty/ThemeColorMathTests.swift, MactermTests/Ghostty/ThemeTests.swift
Uses adaptive colors for effective backgrounds, contrast-aware foregrounds, color-scheme stability, window tinting, pane fills, split dimming, and color-math tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TerminalPane
  participant GhosttyTerminalNSView
  participant AdaptiveTerminalChrome
  participant AdaptiveTerminalBackgroundDetector
  participant GhosttyApp
  TerminalPane->>AdaptiveTerminalChrome: Report focus and render events
  GhosttyTerminalNSView->>AdaptiveTerminalChrome: Report background and reset events
  AdaptiveTerminalChrome->>AdaptiveTerminalBackgroundDetector: Sample visible terminal surface
  AdaptiveTerminalBackgroundDetector-->>AdaptiveTerminalChrome: Return dominant color
  AdaptiveTerminalChrome->>GhosttyTerminalNSView: Present pane background
  AdaptiveTerminalChrome->>GhosttyApp: Adopt window background
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.74% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main feature: adaptive, configurable terminal backgrounds.
Description check ✅ Passed The description covers What, Why, How, Verified, and Notes with enough detail, despite the missing Closes # link.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@491fd1bda this branch Δ
focused CPU % 1.20 1.30 +8%
Memory (RSS MB) 107.8 109.2 +1%
CPU ms/s (powermetrics) 10.9 11.7 +8%
Wakeups/s (powermetrics) 187.4 149.8 -20%
unfocused CPU % 1.10 1.30 +18%
Memory (RSS MB) 112.2 113.8 +1%
CPU ms/s (powermetrics) 10.3 13.4 +30%
Wakeups/s (powermetrics) 174.2 167.2 -4%
minimized CPU % 0.20 0.20 +0%
Memory (RSS MB) 112.5 110.4 -2%
CPU ms/s (powermetrics) 1.9 1.9 +4%
Wakeups/s (powermetrics) 79.4 72.5 -9%
workload-focused CPU % 2.40 2.30 -4%
Memory (RSS MB) 163.4 164.5 +1%
CPU ms/s (powermetrics) 23.5 22.1 -6%
Wakeups/s (powermetrics) 250.1 259.6 +4%
workload-unfocused CPU % 2.50 2.40 -4%
Memory (RSS MB) 163.7 164.7 +1%
CPU ms/s (powermetrics) 24.1 24.2 +0%
Wakeups/s (powermetrics) 260.8 264.7 +2%
workload-minimized CPU % 0.30 0.40 +33%
Memory (RSS MB) 163.7 164.7 +1%
CPU ms/s (powermetrics) 3.3 3.3 +1%
Wakeups/s (powermetrics) 148.2 138.8 -6%

Reported value is the median of 3×10s windows per state (splitting the window and taking the median keeps one co-scheduled spike from skewing a state); CPU % is the process CPU-time delta over a window. Runs land on different shared runners, so treat small deltas as noise — 🔺/🔻 marks changes ≥25% that also clear the metric's absolute noise floor (CPU % ≥0.5, Memory (RSS MB) ≥25, CPU ms/s ≥5, Wakeups/s ≥50); CPU deltas off a noise-dominated baseline aren't flagged (CPU % baseline ≥1.5, CPU ms/s baseline ≥15). Flagged changes add the benchmark:regression / benchmark:improvement label.

@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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Macterm/Model/SplitNode.swift`:
- Around line 342-345: Route adaptive pane-color changes through AppState
instead of directly mutating Pane.adaptiveBackgroundColor. Add an AppState
mutation API for updating the relevant pane, then access AppState in the
TerminalPane view via `@Environment`(AppState.self) and invoke that API from the
callback.

In `@MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift`:
- Line 6: Annotate the AdaptiveTerminalBackgroundTests test struct with
`@MainActor`, preserving its existing testable import and test implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6818a05a-2e13-40bb-8296-1864954c11b3

📥 Commits

Reviewing files that changed from the base of the PR and between 491fd1b and cf4894b.

📒 Files selected for processing (21)
  • Macterm/App/MactermApp.swift
  • Macterm/App/Preferences.swift
  • Macterm/Config/MactermConfig.swift
  • Macterm/Config/TerminalBackgroundOverride.swift
  • Macterm/Ghostty/AdaptiveTerminalBackground.swift
  • Macterm/Ghostty/AdaptiveTerminalChrome.swift
  • Macterm/Ghostty/GhosttyApp.swift
  • Macterm/Ghostty/GhosttyCallbacks.swift
  • Macterm/Ghostty/Theme.swift
  • Macterm/Ghostty/ThemeColorMath.swift
  • Macterm/Model/SplitNode.swift
  • Macterm/Settings/SettingsView.swift
  • Macterm/Views/MainWindow.swift
  • Macterm/Views/SplitTreeView.swift
  • Macterm/Views/Terminal/GhosttyTerminalNSView.swift
  • Macterm/Views/TerminalPane.swift
  • Macterm/Views/WindowAppearance.swift
  • MactermTests/Config/TerminalBackgroundOverrideTests.swift
  • MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift
  • MactermTests/Ghostty/ThemeColorMathTests.swift
  • project.yml

Comment thread Macterm/Model/SplitNode.swift
Comment thread MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift

@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.

🧹 Nitpick comments (1)
MactermTests/App/AppStateTests.swift (1)

62-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover project scoping and the nil-reset path.

The test only uses an unknown pane ID within the same project. Add a valid pane ID with a different projectID, plus a valid-pane nil update, to protect both ownership isolation and clearing behavior.

Suggested test extension
         state.setAdaptiveBackgroundColor(color, paneID: pane.id, projectID: project.id)
         `#expect`(pane.adaptiveBackgroundColor == color)

+        let otherProject = seedProject(state, name: "other", path: "/tmp/other")
+        state.setAdaptiveBackgroundColor(nil, paneID: pane.id, projectID: otherProject.id)
+        `#expect`(pane.adaptiveBackgroundColor == color)
+
         state.setAdaptiveBackgroundColor(nil, paneID: UUID(), projectID: project.id)
         `#expect`(pane.adaptiveBackgroundColor == color)
+
+        state.setAdaptiveBackgroundColor(nil, paneID: pane.id, projectID: project.id)
+        `#expect`(pane.adaptiveBackgroundColor == nil)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MactermTests/App/AppStateTests.swift` around lines 62 - 75, The
adaptiveBackgroundColor_updatesOwnedPaneOnly test should also cover project
ownership and clearing. Add a valid pane ID belonging to a different project and
verify the original pane remains unchanged, then call setAdaptiveBackgroundColor
with the original pane ID and project ID and nil and verify its
adaptiveBackgroundColor is cleared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@MactermTests/App/AppStateTests.swift`:
- Around line 62-75: The adaptiveBackgroundColor_updatesOwnedPaneOnly test
should also cover project ownership and clearing. Add a valid pane ID belonging
to a different project and verify the original pane remains unchanged, then call
setAdaptiveBackgroundColor with the original pane ID and project ID and nil and
verify its adaptiveBackgroundColor is cleared.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 69dde426-a777-4b3f-83a4-5a41b2d22542

📥 Commits

Reviewing files that changed from the base of the PR and between cf4894b and f53f6f6.

📒 Files selected for processing (7)
  • Macterm/App/AppState.swift
  • Macterm/Views/MainWindow.swift
  • Macterm/Views/QuickTerminal.swift
  • Macterm/Views/SplitTreeView.swift
  • Macterm/Views/TerminalPane.swift
  • MactermTests/App/AppStateTests.swift
  • MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift
  • Macterm/Views/TerminalPane.swift

@github-actions github-actions Bot added the benchmark:improvement CI benchmark: significant resource improvement vs main label Jul 21, 2026
@thdxg

thdxg commented Jul 21, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 Wow this is impressive. Thanks for your work! I applied some fixes for issues I encountered while testing this locally:

  1. Theme-background flash when switching to a TUI tab. pruneState wiped a pane's detected color when its tab went off-screen, so revisiting restarted detection (~0.25–0.75s of configured background). Now remembered colors survive occlusion and re-seed the stabilizer, so a revisited tab presents instantly; a TUI that exited off-screen still clears via the normal two-observation path.

  2. Translucent padding seam around a lone TUI pane. Single-pane tabs only got the window-wide tint, which follows windowOpacity — leaving a see-through gap next to the TUI's opaque pixels. Every detected pane now gets the same opaque fill splits already had; the window tint rule is unchanged.

  3. Regression: Settings window reopening on activation + broken Cmd+D/Cmd+W. The adaptive tint fed .preferredColorScheme, which is scene-level (it also governs the Settings window). It flapped on every tint adopt/clear, destabilizing SwiftUI window management and breaking the cached window identity that gates hotkeys. The app-wide scheme now derives from the config background only (keeping the WCAG classification); window-scoped tint and text contrast are untouched.

thdxg added 2 commits July 21, 2026 17:41
Switching to a tab with a running TUI flashed the configured theme:
pruneState wiped the pane's remembered color, fill, and stabilizer on
occlusion, so every revisit restarted detection through the 0.25s
verification pass (or the 0.5s monitor when focus fired before window
attachment). Remembered colors and fills now survive occlusion and seed
fresh stabilizers, so a revisited tab presents immediately while a TUI
that exited off-screen still clears via two observations.

On translucent windows, a lone TUI pane also showed a see-through seam:
single-pane tabs only got the window-wide tint, which follows
windowOpacity, next to the TUI's opaque pixels. Every detected pane now
gets the same opaque fill splits already had; the window tint rule is
unchanged.
Feeding the adaptive tint into .preferredColorScheme made the app-wide
scheme flap on every tint adopt/clear whenever a TUI's background sat on
the opposite side of the WCAG light/dark split. That setting is
scene-level - it also governs the Settings window - and the runtime
churn destabilized SwiftUI window management: the closed Settings window
reopened on every app activation, and the WindowGroup window lost the
cached identity that gates hotkeys (Cmd+W closed the whole window,
Cmd+D went dead).

The scheme now derives from the config background only, keeping the
WCAG-contrast classification. Window-scoped adaptation (nsBg tint,
nsFg contrast) is untouched.
@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
macterm 5f6c8bf Commit Preview URL

Branch Preview URL
Jul 21 2026, 08:42 AM

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MactermTests/Ghostty/ThemeTests.swift`:
- Around line 18-19: Update the test around
GhosttyApp.shared.adoptAdaptiveBackgroundColor to capture the existing
adaptiveBackgroundColor before applying opposite, then restore the captured
value in defer instead of always passing nil. Preserve the test’s temporary
override while restoring process-wide state exactly as it was.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a50c140-4474-4f90-ab4e-8812f6b4f754

📥 Commits

Reviewing files that changed from the base of the PR and between 5b56eac and 5f6c8bf.

📒 Files selected for processing (6)
  • Macterm/App/MactermApp.swift
  • Macterm/Ghostty/AdaptiveTerminalBackground.swift
  • Macterm/Ghostty/AdaptiveTerminalChrome.swift
  • Macterm/Ghostty/Theme.swift
  • MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift
  • MactermTests/Ghostty/ThemeTests.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Macterm/Ghostty/AdaptiveTerminalBackground.swift
  • Macterm/Ghostty/Theme.swift
  • MactermTests/Ghostty/AdaptiveTerminalBackgroundTests.swift
  • Macterm/Ghostty/AdaptiveTerminalChrome.swift

Comment thread MactermTests/Ghostty/ThemeTests.swift Outdated
@thdxg

thdxg commented Jul 21, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 The improvements are applied but I want to discuss whether we need the background override setting. Ghostty already has a background setting in its config, and I want to avoid having duplicate responsibilities between ghostty and macterm as much as possible. Is there a reason you wanted the override setting?

@github-actions github-actions Bot removed the benchmark:improvement CI benchmark: significant resource improvement vs main label Jul 21, 2026
@OpenSource03

OpenSource03 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@OpenSource03 The improvements are applied but I want to discuss whether we need the background override setting. Ghostty already has a background setting in its config, and I want to avoid having duplicate responsibilities between ghostty and macterm as much as possible. Is there a reason you wanted the override setting?

I could see that from the way it was handled originally, yet I would, personally, much more prefer to have a color picker UI than having to edit Ghostty config. I did deliberately leave option to use Ghostty config, but I find it much easier to edit stuff via UI than via some config file, specially when it comes to colors.

Additionally, if we go with such logic, isn't the background blur something Ghostty controls too?

@thdxg

thdxg commented Jul 21, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 Yes Macterm does have its own opacity and blur settings which could seem redundant with ghostty config. But those are slightly different from ghostty's other appearance settings, because opacity and blur of the terminal surface needed to be always overridden to zero, to completely show through the app window's opacity and blur. Otherwise, the terminal panes' appearance can diverge from rest of the app's.

There are a few more settings ignored from ghostty config (documented here) because Macterm explicitly overrides. Since ghostty config doesn't control these, I added them in the settings UI.

When it comes to background color or any other color customization (e.g. foreground, selection-background, selection-foreground, etc.), Macterm still respects ghostty config for them. So unlike window opacity/blur, creating UI to edit those settings introduces two distinct ways to achieve the same thing.

@thdxg

thdxg commented Jul 22, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 I'm happy to discuss more about the settings. But either way, I think it would have to be a separate discussion from this PR, as this mainly implements appearance adapting to fullscreen TUI. Would you be okay with removing the settings change in this PR and continue discussing in another?

@OpenSource03

Copy link
Copy Markdown
Contributor Author

@OpenSource03 I'm happy to discuss more about the settings. But either way, I think it would have to be a separate discussion from this PR, as this mainly implements appearance adapting to fullscreen TUI. Would you be okay with removing the settings change in this PR and continue discussing in another?

Hey @thdxg, sorry for the late reply. Yes, I can see your point, and it does make sense. I do also see benefit of having the override option with a color picker, but I will leave it up to you to decide.

I will remove the color picker from this PR probably later today or tomorrow.

Ghostty already owns background (and every other color) via its own
config, so a Macterm-side override created a second way to set the same
thing. Unlike window opacity/blur — which Macterm must pin to zero and
therefore has to expose itself — nothing here requires Macterm to take
ownership, so the setting is dropped and this PR stays focused on
appearance adapting to full-screen TUIs.

Removes the background source picker, the custom color picker and its
persisted preference, the generated background/foreground override
lines, and the debounced config reload that existed only to smooth
color-picker drags. Adaptive detection, presentation, and chrome
theming are untouched.
@OpenSource03

Copy link
Copy Markdown
Contributor Author

@thdxg Done! :)

@thdxg thdxg left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@OpenSource03 Thanks! I've left some comments on the diff.

return
}
guard adaptiveBackgroundColor != nil || color != nil else { return }
adaptiveBackgroundColor = color

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: SwiftUI chrome has no observation path to the tint.

Compare adoptResolvedColors a few lines above — it does configVersion += 1 and posts. This only posts.

The notification reaches AppKit observers (WindowAppearance.sync, quick-terminal blur), but SwiftUI chrome reads MactermTheme.bg/fg/bgWithOpacity, which are plain static var computed accessors carrying no observation of their own. AppColorScheme is the only thing establishing that dependency, via _ = GhosttyApp.shared.configVersion.

adaptiveBackgroundColor is observable (not @ObservationIgnored), but nothing in the SwiftUI tree reads it — and MactermApp.swift now carries a comment saying it deliberately does not track it. So the sidebar and palette keep the old theme color until some unrelated event bumps configVersion. The screenshots showing the sidebar adapting suggest it currently works only because render activity keeps firing other config changes, i.e. accidentally.

Either bump configVersion here, or have something in the tree read adaptiveBackgroundColor. Worth saying which you intend, since the existing comment argues against the latter.

return false
}

let candidate: NSColor? = if let surface = view.layer?.contents as? IOSurface {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: this reads a libghostty implementation detail, not a contract.

Macterm hands the raw NSView pointer to ghostty_surface_new (GhosttyTerminalNSView.swift:230) and libghostty creates and owns the layer. This reaches into that layer's contents and assumes an IOSurface in BGRA8. Citing "Ghostty's Metal Target.zig" is honest, but it's referencing upstream Zig internals from Swift across a C ABI that makes no such promise.

Given this repo's history — the fork-sync breakage and the #112 reflow overflow — an upstream change to the drawable path or pixel format makes this silently stop working, or worse, misinterpret bytes. The test suite won't catch it either, since it builds its own synthetic IOSurfaceCreate.

Minimum fix: guard the pixel format with IOSurfaceGetPixelFormat(surface) against BGRA rather than relying only on bytesPerElement >= 4, and record the fragility in CLAUDE.md's Known Limitations.

Better: drop pixel sampling for now and ship the OSC 11 path alone — see the summary comment.

sampleTimer = timer
}

private func startMonitoring() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: this timer never stops while the feature is enabled.

startMonitoring() guards on monitorTimer == nil and is called from preferenceDidEnable, focusDidChange, and terminalDidRender — but nothing tears it down except preferenceDidDisable. So once enabled, the app samples every visible pane twice per second forever, including a fully idle app with no TUI running. Each sample locks the IOSurface and reads ~4000 pixels per pane, contending with the renderer on its own live surface.

This contradicts the design this codebase already settled on for exactly this problem: PollCadence bursts, backs off, and stops entirely when nothing is on screen, specifically to avoid polling a static app.

It's also likely to move the workload-focused/workload-unfocused CPU and wakeups/s metrics in the benchmark harness that just landed (#183) — but since the feature is off by default the benchmark won't flag it, so the regression ships invisibly for anyone who enables it.

Suggest mirroring PollCadence: stop after N consecutive no-change samples, resume on a render or focus event.

if #available(macOS 26.0, *) {
guard let glass = existingGlass(in: window) else { return }
glass.updateKeyStatus(window.isKeyWindow, backgroundColor: GhosttyApp.shared.backgroundColor)
glass.updateKeyStatus(window.isKeyWindow, backgroundColor: MactermTheme.nsBg)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (inconsistency): the glass tint helper still uses the heuristic this PR replaces.

This call now correctly reads MactermTheme.nsBg, but the helper it feeds (WindowAppearance.swift:175-176) computes isLightColor and adjustingSaturation from a plain luminance > 0.5 threshold — the exact heuristic this PR replaces elsewhere with WCAG contrast via ThemeColorMath.prefersDarkForeground.

So with a vivid TUI background, the glass overlay opacity is chosen by the misclassifying branch while nsFg uses the correct one. That's visible on precisely the saturated red/magenta cases semanticForegroundUsesContrastRatherThanRawBrightness was added to cover. Route isLightColor through the new contrast math too.

Separately: syncKeyStatus fires on windowDidBecomeKey/windowDidResignKey, which .mactermConfigDidChange does not trigger — worth confirming the tint is picked up on a plain tint change with no focus event, given the configVersion gap noted on GhosttyApp.swift.

return true
}

private func effectiveCandidate(_ color: NSColor?) -> NSColor? {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Wrong baseline on split themes.

The 0.04 threshold suppresses colors too close to the configured background — but GhosttyApp.shared.backgroundColor is app-global and, per its own doc comment, "always collapse[s] a split to its light side" for theme = light:X,dark:Y. That's exactly why effectiveBackgroundColor/resolvedColors exist.

So on a split theme the baseline can be the wrong side, and a TUI whose background matches the dark configured background won't be suppressed. Compare against the resolved color instead.

Also: no test covers this suppression threshold.

guard coverage >= minimumCoverage else { return nil }

return Match(
red: UInt8(winner.red / winner.count),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Trapping conversion guarded only by a non-obvious invariant.

winner.red / winner.count is Int division, so it truncates before the UInt8 conversion (minor, ±1). The bigger issue is that UInt8(_:) traps on out-of-range. It can't overflow today — every summand is ≤255 and the divisor is the count — but that's a subtle invariant protecting a crashing conversion in a path that reads renderer memory.

UInt8(clamping:) costs nothing and removes the reasoning burden entirely.

/// lone pane. The window-wide tint follows the user's window opacity, so
/// on a translucent window it alone would leave a visible seam between
/// the TUI's opaque pixels and the pane padding around them.
static func paneColors(for paneColors: [NSColor?]) -> [NSColor?] {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This abstraction is an identity function.

paneColors(for:) returns its argument unchanged, and windowColor is a one-line ternary. Two of the new tests assert the behavior of an identity function, which doesn't buy much.

The doc comment explaining why pane colors pass through unchanged (the opaque-fill-vs-translucent-window seam) is genuinely valuable — I'd fold both functions into the call site in refreshPresentation and keep that comment there.

final class AdaptiveTerminalChrome {
static let shared = AdaptiveTerminalChrome()

private var stabilizers: [ObjectIdentifier: AdaptiveTerminalBackgroundStabilizer] = [:]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

ObjectIdentifier keys can be reused after dealloc.

Views are weakly held in liveViews, so a view can dealloc and a fresh one land on the same address, matching a stale stabilizer. pruneState runs every sample so the window is small, but the pane's UUID is stable, already threaded through this code, and would remove the hazard entirely.


var body: some View {
Form {
Section("Terminal") {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Placing this above Section("Window") makes an off-by-default experimental feature the first thing in Appearance settings. Suggest moving it below Window.

The label also reads as an OS full-screen-mode setting rather than "full-screen TUI" — something like "Match terminal app backgrounds" is less ambiguous.

Preferences.shared.adaptiveTerminalChromeEnabled = enabled
}
Text("Matches the whole window for a single pane; in a split, only each full-screen app's pane changes color.")
.font(.system(size: 11))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hardcoded font size — worth checking whether the other Settings descriptions use .caption/.footnote, and matching them.

@thdxg

thdxg commented Aug 2, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 I can fix conflicts and address the comments I made if you're okay with it!

@OpenSource03

OpenSource03 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@OpenSource03 I can fix conflicts and address the comments I made if you're okay with it!

Hey! Sorry, I was on vacation for the past time, so I did not have time to go properly through those. I will handle your comments tomorrow or the day after.

If you want to go over it yourself and ship earlier, feel free to let me know, it also works for me.

@thdxg

thdxg commented Aug 4, 2026

Copy link
Copy Markdown
Owner

@OpenSource03 No worries! No pressure on hurrying up. I'll leave it up to you!

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

Labels

area:config Ghostty config wrappers area:state AppState, models, persistence area:terminal Terminal surface, ghostty integration area:tests Test changes area:ui Views, Settings UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants