From e64b231248f68b2fd1e19d538d243b886d5284ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Oct 2024 20:25:40 -0400 Subject: [PATCH] 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. --- macos/Sources/App/macOS/AppDelegate.swift | 5 -- .../QuickTerminalController.swift | 43 +--------- .../Terminal/BaseTerminalController.swift | 72 ++++++++++++++++- .../Terminal/TerminalController.swift | 80 +++++++------------ macos/Sources/Ghostty/Ghostty.Config.swift | 16 ++-- 5 files changed, 111 insertions(+), 105 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 8179e1950..2d8314a7f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -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 diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c382a62a0..82052eabb 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -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 { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 8acda4ed1..71a239710 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -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 diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f31740105..f0216d520 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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,31 +195,20 @@ 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. window.isRestorable = restorable if (restorable) { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } - + // 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 { @@ -245,21 +221,21 @@ class TerminalController: BaseTerminalController { frame.size.height -= leaf.surface.frame.size.height frame.size.width += min(initialSize.width, screen.frame.width) 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. window.setFrame(frame, display: true) } } - + // Center the window to start, we'll move the window frame automatically // when cascading. window.center() - + // Make sure our theme is set on the window so styling is correct. if let windowTheme = ghostty.config.windowTheme { window.windowTheme = .init(rawValue: windowTheme) } - + // 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 // 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") { window.transparentTabs = true } - + if window.hasStyledTabs { // Set the background color of the window let backgroundColor = NSColor(ghostty.config.backgroundColor) window.backgroundColor = backgroundColor - + // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) } - + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self )) - + // If our titlebar style is "hidden" we adjust the style appropriately if (ghostty.config.macosTitlebarStyle == "hidden") { 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, + .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. @@ -324,7 +300,7 @@ class TerminalController: BaseTerminalController { titleBarContainer.isHidden = true } } - + // 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 // it. @@ -344,9 +320,9 @@ class TerminalController: BaseTerminalController { window.tabGroup?.removeWindow(window) } } - + window.focusFollowsMouse = ghostty.config.focusFollowsMouse - + // Apply any additional appearance-related properties to the new window. syncAppearance() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b8c7d2594..f8375adf1 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -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? = 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" + } }