Match Ghostty split dimming and divider colors

This commit is contained in:
Lawrence Chen 2026-01-22 18:01:44 -08:00
parent 62136dbdd3
commit 4440e0ae10
2 changed files with 184 additions and 0 deletions

View file

@ -7,6 +7,9 @@ struct GhosttyConfig {
var theme: String?
var workingDirectory: String?
var scrollbackLimit: Int = 10000
var unfocusedSplitOpacity: Double = 0.7
var unfocusedSplitFill: NSColor?
var splitDividerColor: NSColor?
// Colors (from theme or config)
var backgroundColor: NSColor = NSColor(hex: "#272822")!
@ -19,6 +22,24 @@ struct GhosttyConfig {
// Palette colors (0-15)
var palette: [Int: NSColor] = [:]
var unfocusedSplitOverlayOpacity: Double {
let clamped = min(1.0, max(0.15, unfocusedSplitOpacity))
return min(1.0, max(0.0, 1.0 - clamped))
}
var unfocusedSplitOverlayFill: NSColor {
unfocusedSplitFill ?? backgroundColor
}
var resolvedSplitDividerColor: NSColor {
if let splitDividerColor {
return splitDividerColor
}
let isLightBackground = backgroundColor.isLightColor
return backgroundColor.darken(by: isLightBackground ? 0.08 : 0.4)
}
static func load() -> GhosttyConfig {
var config = GhosttyConfig()
@ -96,6 +117,18 @@ struct GhosttyConfig {
let color = NSColor(hex: String(paletteParts[1])) {
palette[index] = color
}
case "unfocused-split-opacity":
if let opacity = Double(value) {
unfocusedSplitOpacity = opacity
}
case "unfocused-split-fill":
if let color = NSColor(hex: value) {
unfocusedSplitFill = color
}
case "split-divider-color":
if let color = NSColor(hex: value) {
splitDividerColor = color
}
default:
break
}
@ -140,4 +173,33 @@ extension NSColor {
self.init(red: r, green: g, blue: b, alpha: 1.0)
}
var isLightColor: Bool {
luminance > 0.5
}
var luminance: Double {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
guard let rgb = usingColorSpace(.sRGB) else { return 0 }
rgb.getRed(&r, green: &g, blue: &b, alpha: &a)
return (0.299 * r) + (0.587 * g) + (0.114 * b)
}
func darken(by amount: CGFloat) -> NSColor {
var h: CGFloat = 0
var s: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
getHue(&h, saturation: &s, brightness: &b, alpha: &a)
return NSColor(
hue: h,
saturation: s,
brightness: min(b * (1 - amount), 1),
alpha: a
)
}
}

View file

@ -0,0 +1,122 @@
import SwiftUI
struct TerminalSplitTreeView: View {
@ObservedObject var tab: Tab
let isTabActive: Bool
@State private var config = GhosttyConfig.load()
var body: some View {
let appearance = SplitAppearance(
dividerColor: Color(nsColor: config.resolvedSplitDividerColor),
unfocusedOverlayColor: Color(nsColor: config.unfocusedSplitOverlayFill),
unfocusedOverlayOpacity: config.unfocusedSplitOverlayOpacity
)
Group {
if let node = tab.splitTree.zoomed ?? tab.splitTree.root {
TerminalSplitSubtreeView(
node: node,
isRoot: node == tab.splitTree.root,
isSplit: tab.splitTree.isSplit,
isTabActive: isTabActive,
focusedSurfaceId: tab.focusedSurfaceId,
appearance: appearance,
onFocus: { tab.focusSurface($0) },
onResize: { tab.updateSplitRatio(node: $0, ratio: $1) },
onEqualize: { tab.equalizeSplits() }
)
.id(node.structuralIdentity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(GeometryReader { proxy in
Color.clear
.onAppear { tab.updateSplitViewSize(proxy.size) }
.onChange(of: proxy.size) { tab.updateSplitViewSize($0) }
})
}
}
fileprivate struct TerminalSplitSubtreeView: View {
let node: SplitTree<TerminalSurface>.Node
let isRoot: Bool
let isSplit: Bool
let isTabActive: Bool
let focusedSurfaceId: UUID?
let appearance: SplitAppearance
let onFocus: (UUID) -> Void
let onResize: (SplitTree<TerminalSurface>.Node, Double) -> Void
let onEqualize: () -> Void
var body: some View {
switch node {
case .leaf(let surface):
let isFocused = isTabActive && focusedSurfaceId == surface.id
ZStack {
GhosttyTerminalView(
terminalSurface: surface,
isActive: isFocused,
onFocus: { _ in onFocus(surface.id) }
)
.background(Color(nsColor: .windowBackgroundColor))
if isSplit && !isFocused && appearance.unfocusedOverlayOpacity > 0 {
Rectangle()
.fill(appearance.unfocusedOverlayColor)
.opacity(appearance.unfocusedOverlayOpacity)
.allowsHitTesting(false)
}
}
case .split(let split):
let splitViewDirection: SplitViewDirection = switch split.direction {
case .horizontal: .horizontal
case .vertical: .vertical
}
SplitView(
splitViewDirection,
.init(get: {
CGFloat(split.ratio)
}, set: {
onResize(node, Double($0))
}),
dividerColor: appearance.dividerColor,
resizeIncrements: .init(width: 1, height: 1),
left: {
TerminalSplitSubtreeView(
node: split.left,
isRoot: false,
isSplit: isSplit,
isTabActive: isTabActive,
focusedSurfaceId: focusedSurfaceId,
appearance: appearance,
onFocus: onFocus,
onResize: onResize,
onEqualize: onEqualize
)
},
right: {
TerminalSplitSubtreeView(
node: split.right,
isRoot: false,
isSplit: isSplit,
isTabActive: isTabActive,
focusedSurfaceId: focusedSurfaceId,
appearance: appearance,
onFocus: onFocus,
onResize: onResize,
onEqualize: onEqualize
)
},
onEqualize: {
onEqualize()
}
)
}
}
}
private struct SplitAppearance {
let dividerColor: Color
let unfocusedOverlayColor: Color
let unfocusedOverlayOpacity: Double
}