diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index e997868f7..e5f3bc298 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1,6 +1,7 @@ import Foundation import Cocoa import SwiftUI +import Combine import GhosttyKit /// A classic, tabbed terminal experience. @@ -23,6 +24,9 @@ class TerminalController: BaseTerminalController { /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + /// The notification cancellable for focused surface property changes. + private var surfaceAppearanceCancellables: Set = [] + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil @@ -225,7 +229,7 @@ class TerminalController: BaseTerminalController { // The titlebar is always updated. We don't need to worry about opacity // because we handle it here. - let backgroundColor = OSColor(surfaceConfig.backgroundColor) + let backgroundColor = OSColor(focusedSurface?.backgroundColor ?? surfaceConfig.backgroundColor) window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) if (window.isOpaque) { @@ -536,10 +540,31 @@ class TerminalController: BaseTerminalController { override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) + // We always cancel our event listener + surfaceAppearanceCancellables.removeAll() + // When our focus changes, we update our window appearance based on the // currently focused surface. guard let focusedSurface else { return } syncAppearance(focusedSurface.derivedConfig) + + // We also want to get notified of certain changes to update our appearance. + focusedSurface.$derivedConfig + .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } + .store(in: &surfaceAppearanceCancellables) + focusedSurface.$backgroundColor + .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } + .store(in: &surfaceAppearanceCancellables) + } + + private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) { + guard let surface else { return } + DispatchQueue.main.async { [weak self, weak surface] in + guard let surface else { return } + guard let self else { return } + guard self.focusedSurface == surface else { return } + self.syncAppearance(surface.derivedConfig) + } } //MARK: - Notifications diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index d9e58b28c..dfdb0bff5 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -1,3 +1,4 @@ +import SwiftUI import GhosttyKit extension Ghostty { @@ -5,6 +6,33 @@ extension Ghostty { } extension Ghostty.Action { + struct ColorChange { + let kind: Kind + let color: Color + + enum Kind { + case foreground + case background + case cursor + case palette(index: UInt8) + } + + init(c: ghostty_action_color_change_s) { + switch (c.kind) { + case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND: + self.kind = .foreground + case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND: + self.kind = .background + case GHOSTTY_ACTION_COLOR_KIND_CURSOR: + self.kind = .cursor + default: + self.kind = .palette(index: UInt8(c.kind.rawValue)) + } + + self.color = Color(red: Double(c.r) / 255, green: Double(c.g) / 255, blue: Double(c.b) / 255) + } + } + struct MoveTab { let amount: Int diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2ec62a426..b9bebe542 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -515,7 +515,8 @@ extension Ghostty { configChange(app, target: target, v: action.action.config_change) case GHOSTTY_ACTION_COLOR_CHANGE: - fallthrough + colorChange(app, target: target, change: action.action.color_change) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -1188,6 +1189,32 @@ extension Ghostty { } } + private static func colorChange( + _ app: ghostty_app_t, + target: ghostty_target_s, + change: ghostty_action_color_change_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("color change does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + NotificationCenter.default.post( + name: .ghosttyColorDidChange, + object: surfaceView, + userInfo: [ + SwiftUI.Notification.Name.GhosttyColorChangeKey: Action.ColorChange(c: change) + ] + ) + + default: + assertionFailure() + } + } + + // MARK: User Notifications /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 074ce4743..a4d1914e0 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -210,6 +210,10 @@ extension Notification.Name { static let ghosttyConfigDidChange = Notification.Name("com.mitchellh.ghostty.configDidChange") static let GhosttyConfigChangeKey = ghosttyConfigDidChange.rawValue + /// Color change. Object is the surface changing. + static let ghosttyColorDidChange = Notification.Name("com.mitchellh.ghostty.ghosttyColorDidChange") + static let GhosttyColorChangeKey = ghosttyColorDidChange.rawValue + /// Goto tab. Has tab index in the userinfo. static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab") static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 8f281df54..7e861a229 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -51,6 +51,10 @@ extension Ghostty { /// The configuration derived from the Ghostty config so we don't need to rely on references. @Published private(set) var derivedConfig: DerivedConfig + /// The background color within the color palette of the surface. This is only set if it is + /// dynamically updated. Otherwise, the background color is the default background color. + @Published private(set) var backgroundColor: Color? = nil + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -152,6 +156,11 @@ extension Ghostty { selector: #selector(ghosttyConfigDidChange(_:)), name: .ghosttyConfigDidChange, object: self) + center.addObserver( + self, + selector: #selector(ghosttyColorDidChange(_:)), + name: .ghosttyColorDidChange, + object: self) center.addObserver( self, selector: #selector(windowDidChangeScreen), @@ -358,6 +367,21 @@ extension Ghostty { self.derivedConfig = DerivedConfig(config) } + @objc private func ghosttyColorDidChange(_ notification: SwiftUI.Notification) { + guard let change = notification.userInfo?[ + SwiftUI.Notification.Name.GhosttyColorChangeKey + ] as? Ghostty.Action.ColorChange else { return } + + switch (change.kind) { + case .background: + self.backgroundColor = change.color + + default: + // We don't do anything for the other colors yet. + break + } + } + @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { guard let window = self.window else { return } guard let object = notification.object as? NSWindow, window == object else { return }