diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ef4054c2e..4da607b93 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -253,20 +253,25 @@ class TerminalController: BaseTerminalController { // If it does, we match the focused surface. If it doesn't, we use the app // configuration. let backgroundColor: OSColor + let foregroundColor: OSColor if let surfaceTree { if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) { // Similar to above, an alpha component of "0" causes compositor issues, so // we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308 backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001) + foregroundColor = OSColor(focusedSurface.foregroundColor ?? surfaceConfig.foregroundColor) } else { // We don't have a focused surface or our surface doesn't border the // top. We choose to match the color of the top-left most surface. backgroundColor = OSColor(surfaceTree.topLeft().backgroundColor ?? derivedConfig.backgroundColor) + foregroundColor = OSColor(surfaceTree.topLeft().foregroundColor ?? derivedConfig.foregroundColor) } } else { backgroundColor = OSColor(self.derivedConfig.backgroundColor) + foregroundColor = OSColor(self.derivedConfig.foregroundColor) } window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) + window.titleForegroundColor = foregroundColor if (window.isOpaque) { // Bg color is only synced if we have no transparency. This is because @@ -278,6 +283,11 @@ class TerminalController: BaseTerminalController { // so we only call this if we are opaque. window.updateTabBar() } + + guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return } + for (w) in windows { + w.titleForegroundColor = window.titleForegroundColor + } } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { @@ -315,28 +325,28 @@ class TerminalController: BaseTerminalController { window.styleMask = [ // We need `titled` in the mask to get the normal window frame .titled, - + // Full size content view so we can extend // content in to the hidden titlebar's area .fullSizeContentView, - + .resizable, .closable, .miniaturizable, ] - + // Hide the title window.titleVisibility = .hidden window.titlebarAppearsTransparent = true - + // Hide the traffic lights (window control buttons) window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true - + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. window.tabbingMode = .disallowed - + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are // some operations that appear to bring back the titlebar visibility so this ensures // it is gone forever. @@ -345,7 +355,7 @@ class TerminalController: BaseTerminalController { titleBarContainer.isHidden = true } } - + override func windowDidLoad() { super.windowDidLoad() guard let window = window as? TerminalWindow else { return } @@ -429,6 +439,7 @@ class TerminalController: BaseTerminalController { // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + window.titleForegroundColor = NSColor(config.foregroundColor) } // Initialize our content view to the SwiftUI root @@ -487,6 +498,10 @@ class TerminalController: BaseTerminalController { super.windowDidBecomeKey(notification) self.relabelTabs() self.fixTabBar() + + if let focusedSurface { + self.syncAppearance(focusedSurface.derivedConfig) + } } override func windowDidMove(_ notification: Notification) { @@ -651,6 +666,9 @@ class TerminalController: BaseTerminalController { focusedSurface.$backgroundColor .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } .store(in: &surfaceAppearanceCancellables) + focusedSurface.$foregroundColor + .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } + .store(in: &surfaceAppearanceCancellables) } private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) { @@ -780,15 +798,18 @@ class TerminalController: BaseTerminalController { private struct DerivedConfig { let backgroundColor: Color + let foregroundColor: Color let macosTitlebarStyle: String init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) + self.foregroundColor = Color(NSColor.labelColor) self.macosTitlebarStyle = "system" } init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor + self.foregroundColor = config.foregroundColor self.macosTitlebarStyle = config.macosTitlebarStyle } } diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 87cb6ce4f..ebacc77ec 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -25,6 +25,16 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { } } + var titleColor: NSColor? { + get { + titleTextField.textColor + } + + set { + titleTextField.textColor = newValue + } + } + override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 0eb8daeeb..652deaf87 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -8,6 +8,17 @@ class TerminalWindow: NSWindow { guard let titlebarContainer else { return } titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = titlebarColor.cgColor + tab.attributedTitle = attributedTitle + } + } + + lazy var titleForegroundColor: NSColor = NSColor.labelColor { + didSet { + tab.attributedTitle = attributedTitle + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleColor = titleForegroundColor + } + self.updateKeyEquivalentLabel() } } @@ -28,13 +39,7 @@ class TerminalWindow: NSWindow { }, observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), - .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - let attributedString = NSAttributedString(string: " \(window.keyEquivalent) ", attributes: attributes) - - self?.keyEquivalentLabel.attributedStringValue = attributedString + self?.updateKeyEquivalentLabel() }, ] @@ -317,6 +322,17 @@ class TerminalWindow: NSWindow { } } + private func updateKeyEquivalentLabel() { + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 11), + .foregroundColor: self.titleForegroundColor.withAlphaComponent(isKeyWindow ? 1 : 0.4), + ] + let attributedString = NSAttributedString(string: " \(keyEquivalent) ", attributes: attributes) + + keyEquivalentLabel.attributedStringValue = attributedString + } + + // MARK: - Split Zoom Button @objc dynamic var surfaceIsZoomed: Bool = false @@ -403,13 +419,13 @@ class TerminalWindow: NSWindow { // Used to set the titlebar font. var titlebarFont: NSFont? { didSet { - let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + let font = titlebarFont ?? NSFont.titleBarFont(ofSize: 11) titlebarTextField?.font = font tab.attributedTitle = attributedTitle if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = font + toolbar.titleFont = font.withSize(NSFont.systemFontSize) } } } @@ -423,12 +439,11 @@ class TerminalWindow: NSWindow { // Return a styled representation of our title property. private var attributedTitle: NSAttributedString? { - guard let titlebarFont else { return nil } - let attributes: [NSAttributedString.Key: Any] = [ - .font: titlebarFont, - .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + .foregroundColor: titleForegroundColor, + .font: titlebarFont?.withSize(11) ?? NSFont.systemFont(ofSize: 11), ] + return NSAttributedString(string: title, attributes: attributes) } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 1b3263fc3..ce3bb0d23 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -350,6 +350,26 @@ extension Ghostty { return v; } + var foregroundColor: Color { + var color: ghostty_config_color_s = .init(); + let bg_key = "foreground" + if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.count))) { +#if os(macOS) + return Color(NSColor.labelColor) +#elseif os(iOS) + return Color(UIColor.label) +#else +#error("unsupported") +#endif + } + + return .init( + red: Double(color.r) / 255, + green: Double(color.g) / 255, + blue: Double(color.b) / 255 + ) + } + var unfocusedSplitOpacity: Double { guard let config = self.config else { return 1 } var opacity: Double = 0.85 diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c933eb9bf..f8c3d542d 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -55,6 +55,10 @@ extension Ghostty { /// dynamically updated. Otherwise, the background color is the default background color. @Published private(set) var backgroundColor: Color? = nil + /// The foreground 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 foregroundColor: 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 @@ -443,6 +447,11 @@ extension Ghostty { DispatchQueue.main.async { [weak self] in self?.backgroundColor = change.color } + self.backgroundColor = change.color + case .foreground: + DispatchQueue.main.async { [weak self] in + self?.foregroundColor = change.color + } default: // We don't do anything for the other colors yet. @@ -1209,6 +1218,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let foregroundColor: Color let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1216,6 +1226,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.foregroundColor = Color(NSColor.labelColor) self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1224,6 +1235,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.foregroundColor = config.foregroundColor self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config)