Files
ghostty/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift
Mitchell Hashimoto c8243ffd99 macOS: prevent focus loss in hidden titlebar + non-native fullscreen
When using hidden titlebar with non-native fullscreen, the window would
lose focus after entering the first command. This occurred because:

1. Shell commands update the window title
2. Title changes trigger reapplyHiddenStyle() 
3. reapplyHiddenStyle() re-adds .titled to the style mask
4. Style mask changes during fullscreen confuse AppKit, causing focus loss

Fixed by adding a guard to skip titlebar restyling while fullscreen is
active, using terminalController.fullscreenStyle.isFullscreen for
proper detection of both native and non-native fullscreen modes.

https://ampcode.com/threads/T-c4ef59cc-1232-4fa5-8f09-c65724ee84d3
2025-09-03 10:01:28 -07:00

104 lines
3.5 KiB
Swift

import AppKit
class HiddenTitlebarTerminalWindow: TerminalWindow {
override func awakeFromNib() {
super.awakeFromNib()
// Setup our initial style
reapplyHiddenStyle()
// Notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(fullscreenDidExit(_:)),
name: .fullscreenDidExit,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private static let hiddenStyleMask: NSWindow.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,
]
/// Apply the hidden titlebar style.
private func reapplyHiddenStyle() {
// If our window is fullscreen then we don't reapply the hidden style because
// it can result in messing up non-native fullscreen. See:
// https://github.com/ghostty-org/ghostty/issues/8415
if terminalController?.fullscreenStyle?.isFullscreen ?? false {
return
}
// Apply our style mask while preserving the .fullScreen option
if styleMask.contains(.fullScreen) {
styleMask = Self.hiddenStyleMask.union([.fullScreen])
} else {
styleMask = Self.hiddenStyleMask
}
// Hide the title
titleVisibility = .hidden
titlebarAppearsTransparent = true
// Hide the traffic lights (window control buttons)
standardWindowButton(.closeButton)?.isHidden = true
standardWindowButton(.miniaturizeButton)?.isHidden = true
standardWindowButton(.zoomButton)?.isHidden = true
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
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.
if let themeFrame = contentView?.superview,
let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") {
titleBarContainer.isHidden = true
}
}
// MARK: NSWindow
override var title: String {
didSet {
// Updating the title text as above automatically reveals the
// native title view in macOS 15.0 and above. Since we're using
// a custom view instead, we need to re-hide it.
reapplyHiddenStyle()
}
}
// We override this so that with the hidden titlebar style the titlebar
// area is not draggable.
override var contentLayoutRect: CGRect {
var rect = super.contentLayoutRect
rect.origin.y = 0
rect.size.height = self.frame.height
return rect
}
// MARK: Notifications
@objc private func fullscreenDidExit(_ notification: Notification) {
// Make sure they're talking about our window
guard let fullscreen = notification.object as? FullscreenBase else { return }
guard fullscreen.window == self else { return }
// On exit we need to reapply the style because macOS breaks it usually.
// This is safe to call repeatedly so if its not broken its still safe.
reapplyHiddenStyle()
}
}