Merge pull request #2552 from ghostty-org/push-ryrokukruoxr

macos: setup colorspace in base terminal controller
This commit is contained in:
Mitchell Hashimoto
2024-10-30 20:48:30 -04:00
committed by GitHub
5 changed files with 111 additions and 105 deletions

View File

@ -496,11 +496,6 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance. // AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance() } DispatchQueue.main.async { self.syncAppearance() }
// Update all of our windows
terminalManager.windows.forEach { window in
window.controller.configDidReload()
}
// If we have configuration errors, we need to show them. // If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance let c = ConfigurationErrorsController.sharedInstance
c.errors = state.config.errors c.errors = state.config.errors

View File

@ -33,11 +33,6 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onToggleFullscreen), selector: #selector(onToggleFullscreen),
name: Ghostty.Notification.ghosttyToggleFullscreen, name: Ghostty.Notification.ghosttyToggleFullscreen,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(ghosttyDidReloadConfig),
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -53,6 +48,8 @@ class QuickTerminalController: BaseTerminalController {
// MARK: NSWindowController // MARK: NSWindowController
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad()
guard let window = self.window else { return } guard let window = self.window else { return }
// The controller is the window delegate so we can detect events such as // The controller is the window delegate so we can detect events such as
@ -63,9 +60,6 @@ class QuickTerminalController: BaseTerminalController {
// make this restorable, but it isn't currently implemented. // make this restorable, but it isn't currently implemented.
window.isRestorable = false window.isRestorable = false
// Setup our configured appearance that we support.
syncAppearance()
// Setup our initial size based on our configured position // Setup our initial size based on our configured position
position.setLoaded(window) position.setLoaded(window)
@ -297,35 +291,6 @@ class QuickTerminalController: BaseTerminalController {
}) })
} }
private func syncAppearance() {
guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some
// APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) {
window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that
// matches Terminal.app much more closer. This lets users transition from
// Terminal.app more easily.
window.backgroundColor = .white.withAlphaComponent(0.001)
ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque())
} else {
window.isOpaque = true
window.backgroundColor = .windowBackgroundColor
}
}
// MARK: First Responder // MARK: First Responder
@IBAction override func closeWindow(_ sender: Any) { @IBAction override func closeWindow(_ sender: Any) {
@ -357,10 +322,6 @@ class QuickTerminalController: BaseTerminalController {
// We ignore the requested mode and always use non-native for the quick terminal // We ignore the requested mode and always use non-native for the quick terminal
toggleFullscreen(mode: .nonNative) toggleFullscreen(mode: .nonNative)
} }
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
syncAppearance()
}
} }
extension Notification.Name { extension Notification.Name {

View File

@ -93,6 +93,11 @@ class BaseTerminalController: NSWindowController,
selector: #selector(didChangeScreenParametersNotification), selector: #selector(didChangeScreenParametersNotification),
name: NSApplication.didChangeScreenParametersNotification, name: NSApplication.didChangeScreenParametersNotification,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(ghosttyDidReloadConfigNotification),
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
// Listen for local events that we need to know of outside of // Listen for local events that we need to know of outside of
// single surface handlers. // single surface handlers.
@ -123,7 +128,7 @@ class BaseTerminalController: NSWindowController,
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
/// what surface is focused. This must be called whenever a surface OR window changes focus. /// what surface is focused. This must be called whenever a surface OR window changes focus.
func syncFocusToSurfaceTree() { private func syncFocusToSurfaceTree() {
guard let tree = self.surfaceTree else { return } guard let tree = self.surfaceTree else { return }
for leaf in tree { for leaf in tree {
@ -136,6 +141,48 @@ class BaseTerminalController: NSWindowController,
} }
} }
// Call this whenever we want to setup our appearance parameters based on
// configuration changes.
private func syncAppearance() {
guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some
// APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) {
window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that
// matches Terminal.app much more closer. This lets users transition from
// Terminal.app more easily.
window.backgroundColor = .white.withAlphaComponent(0.001)
ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque())
} else {
window.isOpaque = true
window.backgroundColor = .windowBackgroundColor
}
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.config.windowColorspace) {
case .displayP3:
window.colorSpace = .displayP3
case .srgb:
window.colorSpace = .sRGB
}
}
// Call this whenever the frame changes // Call this whenever the frame changes
private func windowFrameDidChange() { private func windowFrameDidChange() {
// We need to update our saved frame information in case of monitor // We need to update our saved frame information in case of monitor
@ -145,6 +192,14 @@ class BaseTerminalController: NSWindowController,
savedFrame = .init(window: window.frame, screen: screen.visibleFrame) savedFrame = .init(window: window.frame, screen: screen.visibleFrame)
} }
// MARK: Overridable Callbacks
/// Called whenever Ghostty reloads the configuration. Callers should call super.
open func ghosttyDidReloadConfig() {
// Whenever the config changes we setup our appearance.
syncAppearance()
}
// MARK: Notifications // MARK: Notifications
@objc private func didChangeScreenParametersNotification(_ notification: Notification) { @objc private func didChangeScreenParametersNotification(_ notification: Notification) {
@ -191,6 +246,10 @@ class BaseTerminalController: NSWindowController,
window.setFrame(newFrame, display: true) window.setFrame(newFrame, display: true)
} }
@objc private func ghosttyDidReloadConfigNotification(notification: SwiftUI.Notification) {
ghosttyDidReloadConfig()
}
// MARK: Local Events // MARK: Local Events
private func localEventHandler(_ event: NSEvent) -> NSEvent? { private func localEventHandler(_ event: NSEvent) -> NSEvent? {
@ -381,7 +440,16 @@ class BaseTerminalController: NSWindowController,
} }
} }
//MARK: - NSWindowDelegate // MARK: NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
// Setup our configured appearance that we support.
syncAppearance()
}
// MARK: NSWindowDelegate
// This is called when performClose is called on a window (NOT when close() // This is called when performClose is called on a window (NOT when close()
// is called directly). performClose is called primarily when UI elements such // is called directly). performClose is called primarily when UI elements such

View File

@ -78,14 +78,16 @@ class TerminalController: BaseTerminalController {
} }
} }
//MARK: - Methods override func ghosttyDidReloadConfig() {
super.ghosttyDidReloadConfig()
func configDidReload() {
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse window.focusFollowsMouse = ghostty.config.focusFollowsMouse
syncAppearance() syncAppearance()
} }
//MARK: - Methods
/// Update the accessory view of each tab according to the keyboard /// Update the accessory view of each tab according to the keyboard
/// shortcut that activates it (if any). This is called when the key window /// shortcut that activates it (if any). This is called when the key window
/// changes, when a window is closed, and when tabs are reordered /// changes, when a window is closed, and when tabs are reordered
@ -164,21 +166,6 @@ class TerminalController: BaseTerminalController {
window.titlebarFont = nil window.titlebarFont = nil
} }
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) {
window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that
// matches Terminal.app much more closer. This lets users transition from
// Terminal.app more easily.
window.backgroundColor = .white.withAlphaComponent(0.001)
ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque())
} else {
window.isOpaque = true
window.backgroundColor = .windowBackgroundColor
}
window.hasShadow = ghostty.config.macosWindowShadow window.hasShadow = ghostty.config.macosWindowShadow
guard window.hasStyledTabs else { return } guard window.hasStyledTabs else { return }
@ -208,31 +195,20 @@ class TerminalController: BaseTerminalController {
} }
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad()
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
// Setting all three of these is required for restoration to work. // Setting all three of these is required for restoration to work.
window.isRestorable = restorable window.isRestorable = restorable
if (restorable) { if (restorable) {
window.restorationClass = TerminalWindowRestoration.self window.restorationClass = TerminalWindowRestoration.self
window.identifier = .init(String(describing: TerminalWindowRestoration.self)) window.identifier = .init(String(describing: TerminalWindowRestoration.self))
} }
// If window decorations are disabled, remove our title // If window decorations are disabled, remove our title
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.config.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
fallthrough
default:
window.colorSpace = .sRGB
}
// If we have only a single surface (no splits) and that surface requested // If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now. // an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree { if case let .leaf(leaf) = surfaceTree {
@ -245,21 +221,21 @@ class TerminalController: BaseTerminalController {
frame.size.height -= leaf.surface.frame.size.height frame.size.height -= leaf.surface.frame.size.height
frame.size.width += min(initialSize.width, screen.frame.width) frame.size.width += min(initialSize.width, screen.frame.width)
frame.size.height += min(initialSize.height, screen.frame.height) frame.size.height += min(initialSize.height, screen.frame.height)
// We have no tabs and we are not a split, so set the initial size of the window. // We have no tabs and we are not a split, so set the initial size of the window.
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
} }
} }
// Center the window to start, we'll move the window frame automatically // Center the window to start, we'll move the window frame automatically
// when cascading. // when cascading.
window.center() window.center()
// Make sure our theme is set on the window so styling is correct. // Make sure our theme is set on the window so styling is correct.
if let windowTheme = ghostty.config.windowTheme { if let windowTheme = ghostty.config.windowTheme {
window.windowTheme = .init(rawValue: windowTheme) window.windowTheme = .init(rawValue: windowTheme)
} }
// Handle titlebar tabs config option. Something about what we do while setting up the // Handle titlebar tabs config option. Something about what we do while setting up the
// titlebar tabs interferes with the window restore process unless window.tabbingMode // titlebar tabs interferes with the window restore process unless window.tabbingMode
// is set to .preferred, so we set it, and switch back to automatic as soon as we can. // is set to .preferred, so we set it, and switch back to automatic as soon as we can.
@ -272,50 +248,50 @@ class TerminalController: BaseTerminalController {
} else if (ghostty.config.macosTitlebarStyle == "transparent") { } else if (ghostty.config.macosTitlebarStyle == "transparent") {
window.transparentTabs = true window.transparentTabs = true
} }
if window.hasStyledTabs { if window.hasStyledTabs {
// Set the background color of the window // Set the background color of the window
let backgroundColor = NSColor(ghostty.config.backgroundColor) let backgroundColor = NSColor(ghostty.config.backgroundColor)
window.backgroundColor = backgroundColor window.backgroundColor = backgroundColor
// This makes sure our titlebar renders correctly when there is a transparent background // This makes sure our titlebar renders correctly when there is a transparent background
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
} }
// Initialize our content view to the SwiftUI root // Initialize our content view to the SwiftUI root
window.contentView = NSHostingView(rootView: TerminalView( window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty, ghostty: self.ghostty,
viewModel: self, viewModel: self,
delegate: self delegate: self
)) ))
// If our titlebar style is "hidden" we adjust the style appropriately // If our titlebar style is "hidden" we adjust the style appropriately
if (ghostty.config.macosTitlebarStyle == "hidden") { if (ghostty.config.macosTitlebarStyle == "hidden") {
window.styleMask = [ window.styleMask = [
// We need `titled` in the mask to get the normal window frame // We need `titled` in the mask to get the normal window frame
.titled, .titled,
// Full size content view so we can extend // Full size content view so we can extend
// content in to the hidden titlebar's area // content in to the hidden titlebar's area
.fullSizeContentView, .fullSizeContentView,
.resizable, .resizable,
.closable, .closable,
.miniaturizable, .miniaturizable,
] ]
// Hide the title // Hide the title
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true window.titlebarAppearsTransparent = true
// Hide the traffic lights (window control buttons) // Hide the traffic lights (window control buttons)
window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
window.tabbingMode = .disallowed window.tabbingMode = .disallowed
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are // 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 // some operations that appear to bring back the titlebar visibility so this ensures
// it is gone forever. // it is gone forever.
@ -324,7 +300,7 @@ class TerminalController: BaseTerminalController {
titleBarContainer.isHidden = true titleBarContainer.isHidden = true
} }
} }
// In various situations, macOS automatically tabs new windows. Ghostty handles // In various situations, macOS automatically tabs new windows. Ghostty handles
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes // its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it. // it.
@ -344,9 +320,9 @@ class TerminalController: BaseTerminalController {
window.tabGroup?.removeWindow(window) window.tabGroup?.removeWindow(window)
} }
} }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse window.focusFollowsMouse = ghostty.config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window. // Apply any additional appearance-related properties to the new window.
syncAppearance() syncAppearance()
} }

View File

@ -128,13 +128,14 @@ extension Ghostty {
return v return v
} }
var windowColorspace: String { var windowColorspace: WindowColorspace {
guard let config = self.config else { return "" } guard let config = self.config else { return .srgb }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace" let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .srgb }
guard let ptr = v else { return "" } guard let ptr = v else { return .srgb }
return String(cString: ptr) let str = String(cString: ptr)
return WindowColorspace(rawValue: str) ?? .srgb
} }
var windowSaveState: String { var windowSaveState: String {
@ -474,4 +475,9 @@ extension Ghostty.Config {
} }
} }
} }
enum WindowColorspace : String {
case srgb
case displayP3 = "display-p3"
}
} }