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" + } }