diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -354,14 +354,14 @@ return MacOSWindowButtons(rawValue: str) ?? defaultValue } - var macosTitlebarStyle: String { - let defaultValue = "transparent" + var macosTitlebarStyle: MacOSTitlebarStyle { + let defaultValue = MacOSTitlebarStyle.transparent guard let config = self.config else { return defaultValue } var v: UnsafePointer? let key = "macos-titlebar-style" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } - return String(cString: ptr) + return MacOSTitlebarStyle(rawValue: String(cString: ptr)) ?? defaultValue } var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon { @@ -905,5 +905,10 @@ static let bell = NotifyOnCommandFinishAction(rawValue: 1 << 0) static let notify = NotifyOnCommandFinishAction(rawValue: 1 << 1) + } + + enum MacOSTitlebarStyle: String { + static let `default` = MacOSTitlebarStyle.transparent + case native, transparent, tabs, hidden } } diff --git a/macos/Tests/Terminal/IntrinsicSizeTimingTests.swift b/macos/Tests/Terminal/IntrinsicSizeTimingTests.swift --- a/macos/Tests/Terminal/IntrinsicSizeTimingTests.swift +++ b/macos/Tests/Terminal/IntrinsicSizeTimingTests.swift @@ -12,7 +12,7 @@ private struct OptionalIdealSizeView: View { let idealWidth: CGFloat? let idealHeight: CGFloat? - let titlebarStyle: String + let titlebarStyle: Ghostty.Config.MacOSTitlebarStyle var body: some View { VStack(spacing: 0) { @@ -20,7 +20,7 @@ .frame(idealWidth: idealWidth, idealHeight: idealHeight) } // Matches TerminalView line 108: hidden style extends into titlebar - .ignoresSafeArea(.container, edges: titlebarStyle == "hidden" ? .top : []) + .ignoresSafeArea(.container, edges: titlebarStyle == .hidden ? .top : []) } } @@ -28,18 +28,18 @@ private let minReasonableHeight: CGFloat = 50 /// All macos-titlebar-style values that map to different window nibs. -private let allTitlebarStyles = ["native", "hidden", "transparent", "tabs"] +private let allTitlebarStyles: [Ghostty.Config.MacOSTitlebarStyle] = [.native, .hidden, .transparent, .tabs] /// Window style masks that roughly correspond to each titlebar style. /// In real Ghostty these come from different nib files; in tests we /// approximate with NSWindow style masks. -private func styleMask(for titlebarStyle: String) -> NSWindow.StyleMask { +private func styleMask(for titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) -> NSWindow.StyleMask { switch titlebarStyle { - case "hidden": + case .hidden: return [.titled, .resizable, .fullSizeContentView] - case "transparent", "tabs": + case .transparent, .tabs: return [.titled, .resizable, .fullSizeContentView] - default: + case .native: return [.titled, .resizable] } } @@ -67,7 +67,7 @@ /// intrinsicContentSize returns a tiny value. @Test(.bug("https://github.com/ghostty-org/ghostty/issues/11256", "intrinsicContentSize too small before @FocusedValue propagates"), arguments: allTitlebarStyles) - func intrinsicSizeTooSmallWithNilIdealSize(titlebarStyle: String) async throws { + func intrinsicSizeTooSmallWithNilIdealSize(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedSize = NSSize(width: 600, height: 400) // nil ideal sizes = @FocusedValue hasn't propagated lastFocusedSurface @@ -106,7 +106,7 @@ /// too-small window when intrinsicContentSize is based on nil ideal sizes. @Test(.bug("https://github.com/ghostty-org/ghostty/issues/11256", "apply() sets wrong window size due to racy intrinsicContentSize"), arguments: allTitlebarStyles) - func applyProducesWrongSizeWithNilIdealSize(titlebarStyle: String) async throws { + func applyProducesWrongSizeWithNilIdealSize(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let container = await TerminalViewContainer { OptionalIdealSizeView(idealWidth: nil, idealHeight: nil, titlebarStyle: titlebarStyle) } @@ -150,7 +150,7 @@ /// to propagate, so intrinsicContentSize is still tiny when apply() runs. @Test(.bug("https://github.com/ghostty-org/ghostty/issues/11256", "40ms async delay reads intrinsicContentSize before @FocusedValue propagates"), arguments: allTitlebarStyles) - func asyncAfterDelayProducesWrongSizeWithNilIdealSize(titlebarStyle: String) async throws { + func asyncAfterDelayProducesWrongSizeWithNilIdealSize(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let container = await TerminalViewContainer { OptionalIdealSizeView(idealWidth: nil, idealHeight: nil, titlebarStyle: titlebarStyle) } @@ -195,7 +195,7 @@ /// fallback value, not just adjust timing. @Test(.bug("https://github.com/ghostty-org/ghostty/issues/11256", "Synchronous apply also fails without fallback"), arguments: allTitlebarStyles) - func synchronousApplyAlsoFailsWithNilIdealSize(titlebarStyle: String) async throws { + func synchronousApplyAlsoFailsWithNilIdealSize(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let container = await TerminalViewContainer { OptionalIdealSizeView(idealWidth: nil, idealHeight: nil, titlebarStyle: titlebarStyle) } @@ -233,7 +233,7 @@ /// should be correct for every titlebar style. This is the "happy path" that /// works today when the 40ms delay is sufficient. @Test(arguments: allTitlebarStyles) - func intrinsicSizeCorrectWhenIdealSizesAvailable(titlebarStyle: String) async throws { + func intrinsicSizeCorrectWhenIdealSizesAvailable(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedSize = NSSize(width: 600, height: 400) let container = await TerminalViewContainer { @@ -274,7 +274,7 @@ /// Verifies that apply() sets a correctly sized window when ideal sizes /// are available, for each titlebar style. @Test(arguments: allTitlebarStyles) - func applyProducesCorrectSizeWhenIdealSizesAvailable(titlebarStyle: String) async throws { + func applyProducesCorrectSizeWhenIdealSizesAvailable(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedSize = NSSize(width: 600, height: 400) let container = await TerminalViewContainer { @@ -319,7 +319,7 @@ /// This should always pass — it validates the delay works when @FocusedValue /// has already propagated. @Test(arguments: allTitlebarStyles) - func asyncAfterDelayProducesCorrectSizeWhenIdealSizesAvailable(titlebarStyle: String) async throws { + func asyncAfterDelayProducesCorrectSizeWhenIdealSizesAvailable(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedSize = NSSize(width: 600, height: 400) let container = await TerminalViewContainer { @@ -364,7 +364,7 @@ /// (never .contentIntrinsicSize). The window uses its initial frame. /// This should work for all titlebar styles regardless of the bug. @Test(arguments: allTitlebarStyles) - func framePathWorksWithoutWindowSize(titlebarStyle: String) async throws { + func framePathWorksWithoutWindowSize(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedFrame = NSRect(x: 100, y: 100, width: 800, height: 600) let container = await TerminalViewContainer { @@ -399,7 +399,7 @@ /// Verifies isChanged correctly detects mismatch for contentIntrinsicSize /// across titlebar styles when ideal sizes are available. @Test(arguments: allTitlebarStyles) - func isChangedDetectsMismatch(titlebarStyle: String) async throws { + func isChangedDetectsMismatch(titlebarStyle: Ghostty.Config.MacOSTitlebarStyle) async throws { let expectedSize = NSSize(width: 600, height: 400) let container = await TerminalViewContainer { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -19,10 +19,10 @@ } let nib = switch config.macosTitlebarStyle { - case "native": "Terminal" - case "hidden": "TerminalHiddenTitlebar" - case "transparent": "TerminalTransparentTitlebar" - case "tabs": + case .native: "Terminal" + case .hidden: "TerminalHiddenTitlebar" + case .transparent: "TerminalTransparentTitlebar" + case .tabs: #if compiler(>=6.2) if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" @@ -32,7 +32,6 @@ #else "TerminalTabsTitlebarVentura" #endif - default: defaultValue } return nib @@ -1537,7 +1536,7 @@ struct DerivedConfig { let backgroundColor: Color let macosWindowButtons: Ghostty.MacOSWindowButtons - let macosTitlebarStyle: String + let macosTitlebarStyle: Ghostty.Config.MacOSTitlebarStyle let maximize: Bool let windowPositionX: Int16? let windowPositionY: Int16? @@ -1545,7 +1544,7 @@ init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.macosWindowButtons = .visible - self.macosTitlebarStyle = "system" + self.macosTitlebarStyle = .default self.maximize = false self.windowPositionX = nil self.windowPositionY = nil diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -105,7 +105,7 @@ idealHeight: lastFocusedSurface?.value?.initialSize?.height) } // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style - .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) + .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == .hidden ? .top : []) if let surfaceView = lastFocusedSurface?.value { TerminalCommandPaletteView( diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -588,7 +588,7 @@ let backgroundColor: NSColor let backgroundOpacity: Double let macosWindowButtons: Ghostty.MacOSWindowButtons - let macosTitlebarStyle: String + let macosTitlebarStyle: Ghostty.Config.MacOSTitlebarStyle let windowCornerRadius: CGFloat init() { @@ -597,7 +597,7 @@ self.backgroundOpacity = 1 self.macosWindowButtons = .visible self.backgroundBlur = .disabled - self.macosTitlebarStyle = "transparent" + self.macosTitlebarStyle = .default self.windowCornerRadius = 16 } @@ -613,7 +613,7 @@ // Native, transparent, and hidden styles use 16pt radius // Tabs style uses 20pt radius switch config.macosTitlebarStyle { - case "tabs": + case .tabs: self.windowCornerRadius = 20 default: self.windowCornerRadius = 16 diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -92,8 +92,8 @@ // For glass background styles, use a transparent titlebar to let the glass effect show through // Only apply this for transparent and tabs titlebar styles let isGlassStyle = derivedConfig.backgroundBlur.isGlassStyle - let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == "transparent" || - derivedConfig.macosTitlebarStyle == "tabs" + let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == .transparent || + derivedConfig.macosTitlebarStyle == .tabs titlebarView.layer?.backgroundColor = (isGlassStyle && isTransparentTitlebar) ? NSColor.clear.cgColor