macos: setup colorspace in base terminal controller

Fixes #2519

This sets up the colorspace for terminal windows in the base controller.

This also modifies some of our logic so its easier for subclasses of
base controllers to specify custom logic when the configuration reloads,
since that's likely to be a common thing.
This commit is contained in:
Mitchell Hashimoto
2024-10-30 20:25:40 -04:00
parent 569d887de8
commit e64b231248
5 changed files with 111 additions and 105 deletions

View File

@ -496,11 +496,6 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance.
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.
let c = ConfigurationErrorsController.sharedInstance
c.errors = state.config.errors

View File

@ -33,11 +33,6 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onToggleFullscreen),
name: Ghostty.Notification.ghosttyToggleFullscreen,
object: nil)
center.addObserver(
self,
selector: #selector(ghosttyDidReloadConfig),
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
}
required init?(coder: NSCoder) {
@ -53,6 +48,8 @@ class QuickTerminalController: BaseTerminalController {
// MARK: NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
guard let window = self.window else { return }
// 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.
window.isRestorable = false
// Setup our configured appearance that we support.
syncAppearance()
// Setup our initial size based on our configured position
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
@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
toggleFullscreen(mode: .nonNative)
}
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
syncAppearance()
}
}
extension Notification.Name {

View File

@ -93,6 +93,11 @@ class BaseTerminalController: NSWindowController,
selector: #selector(didChangeScreenParametersNotification),
name: NSApplication.didChangeScreenParametersNotification,
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
// 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
/// 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 }
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
private func windowFrameDidChange() {
// 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)
}
// 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
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
@ -191,6 +246,10 @@ class BaseTerminalController: NSWindowController,
window.setFrame(newFrame, display: true)
}
@objc private func ghosttyDidReloadConfigNotification(notification: SwiftUI.Notification) {
ghosttyDidReloadConfig()
}
// MARK: Local Events
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()
// 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 }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
syncAppearance()
}
//MARK: - Methods
/// Update the accessory view of each tab according to the keyboard
/// shortcut that activates it (if any). This is called when the key window
/// changes, when a window is closed, and when tabs are reordered
@ -164,21 +166,6 @@ class TerminalController: BaseTerminalController {
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
guard window.hasStyledTabs else { return }
@ -208,6 +195,8 @@ class TerminalController: BaseTerminalController {
}
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window as? TerminalWindow else { return }
// Setting all three of these is required for restoration to work.
@ -220,19 +209,6 @@ class TerminalController: BaseTerminalController {
// If window decorations are disabled, remove our title
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
// an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree {

View File

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