Add a native Show Toolbar option that hides the whole title bar - #234
Conversation
Hiding the window toolbar via SwiftUI collapses the whole titlebar — traffic lights included — but leaves its safe-area inset reserved and the title text in place, so the option needs three levers together: the toolbar visibility flip, a conditional top safe-area reclaim on both NavigationSplitView columns, and titleVisibility on the window. All native API; toggling live is lossless.
Window-state benchmark
|
With Hide Title Bar on, the collapsed titlebar container still tracked its old rect: an invisible strip over the terminal's top rows dragged the window and swallowed clicks. Hide the NSTitlebarContainerView itself while the option is on — the same thing Ghostty's hidden titlebar style does — so events fall through to the content. Re-asserted from every WindowAppearance.sync (AppKit rebuilds titlebar subviews on becomeMain/fullscreen) and from WindowStyler for live setting flips.
Ghostty's hidden style removes the band by overriding contentLayoutRect on its NSWindow subclass; SwiftUI owns our window class, so this uses the two public equivalents instead. GhosttyTerminalNSView answers mouseDownCanMoveWindow with false (same idea as Ghostty's NonDraggableHostingView) so a click on the terminal's top rows is input, not a window drag, and the window turns isMovable off while the option is on so sidebar dead space stops dragging too — programmatic moves, including AX-driven window managers, are unaffected.
… bar is hidden Three leftovers of the hidden titlebar band, all found by use. The macOS 26+ scroll-edge pocket (NSScrollPocket) gets hosted in the titlebar area — moved there outright on macOS 27, ghostty#13390 — where it sat over the terminal's top rows and blocked text selection; hide every pocket while the option is on (one can exist per scroll view). NSScrollView's automatic content insets pushed the overlay scroller's track down by the titlebar overlap, so it started below the window top; SurfaceScrollView now opts out (a no-op while the chrome is visible, since the pane sits below the titlebar and the insets are zero). And the sidebar's first row sat flush against the window edge; an 8pt safe-area pad — inside the ignoresSafeArea, so the sidebar surface still reaches the edge — gives it breathing room.
The preference reads better beside the tab-switcher settings it supersedes than as a Window item, and phrased positively (on by default) like the sidebar's Show toggles. The stored preference stays hideTitleBar. The tab switcher pickers gray out while the toolbar is hidden, since they configure a control that isn't on screen.
In native fullscreen AppKit hosts the titlebar in a separate overlay window parented to ours, so the container lookup found nothing there and a hidden toolbar left an empty bar pinned to the top of the fullscreen space. Follow the parent hop the way Ghostty's TerminalWindow does — matching on parent picks the right overlay when several fullscreen windows exist — so the hide (and the windowed-mode titlebar styling) applies to whichever window actually holds the titlebar. The enter/exit-fullscreen delegate hooks already re-sync.
…eveal Two mechanisms behind the bar that survived hiding the titlebar container. SwiftUI's toolbar hide keeps the NSToolbar object on the window, and native fullscreen creates a 52pt NSToolbarFullScreenWindow overlay for any window owning one — toolbar.isVisible = false removes that slot. What remains is a bare 32pt titlebar the system slides down on top-edge hover, with a system-painted background that ignores titlebarAppearsTransparent and whose subviews AppKit rebuilds on each reveal (undoing view-level hides) — so the overlay window's own alphaValue carries the hide instead; the menu bar is a separate system window and still slides in for menu access. Both are symmetric with the setting, and the enter/exit-fullscreen syncs re-assert them.
alphaValue = 0 on the NSToolbarFullScreenWindow didn't hold: the top-edge hover reveal animates the overlay's alpha back in, and the bar returned mid-animation as a translucent strip. Hiding the overlay's root view instead lets the slide animate whatever alpha it likes over a window that renders nothing — the root survives the reveal's subview churn, unlike the container-level hides. Kept alongside the alpha set; both are symmetric with the setting.
… gray bar) In native fullscreen the titlebar lives in the NSToolbarFullScreenWindow overlay — a separate window nothing of ours shows through — so the clear layer that lets the sidebar glass through in windowed mode just exposed the overlay's native gray material there, which ignores titlebarAppearsTransparent. That gray band over the detail column is the visual issue #24 was closed over. Now that titlebarContainer() resolves through the overlay, syncTitlebar paints the titlebar layer with the window background in fullscreen (and hides NSTitlebarBackgroundView, whose subviews would repaint the gray on top), while windowed mode keeps the clear layer and its glass-sidebar rationale. Verified by pixel sampling: the pinned fullscreen toolbar band now matches the terminal background exactly at entry, no toggle dance needed.
What
A Settings → Appearance → Toolbar → Show toolbar toggle (on by default). Turning it off removes the window's entire top chrome — toolbar (sidebar toggle, tab switcher, update button), title text, traffic lights, and the drag band — so the sidebar and terminal surface extend to the window's top edge. The tab-switcher settings gray out while hidden.
Why
Requested in #226 (an unobtrusive, kitty/Terminal.app-minimal look). Feasibility was the question; it turned out to be achievable with entirely native API, no private-view mutation beyond the hide/show of two titlebar subviews this codebase already reaches (and Ghostty's hidden style performs the same hides).
Closes #226
How
Six levers, each one earned against a real symptom — the first three make the chrome disappear, the last three un-break the strip it leaves behind:
.toolbar(hidden ? .hidden : .visible, for: .windowToolbar)on theNavigationSplitView. Hiding the window toolbar makes AppKit collapse the whole titlebar of a.fullSizeContentViewwindow, traffic lights included. The visibility parameter (rather than anif-wrapped modifier) keeps the split view's identity across toggles, so terminal surfaces are never rebuilt..ignoresSafeArea(.container, edges: .top)on both columns — hiding the toolbar does not release its reserved safe-area inset. The sidebar keeps an 8ptsafeAreaPadding(innermost, so its surface still reaches the edge) for breathing room.window.titleVisibility = .hidden(public AppKit): the title text belongs to the titlebar, not the toolbar.WindowAppearance.syncTitleBarHidden): even fully collapsed, the titlebar container tracks its old rect and AppKit keeps a titlebar-height drag band (the area abovecontentLayoutRect) that moves the window from any hit view answeringmouseDownCanMoveWindow. Ghostty's hidden style fixes this by overridingcontentLayoutRecton its NSWindow subclass; SwiftUI owns our window class, so the public equivalents are: hide theNSTitlebarContainerView(as Ghostty also does), answermouseDownCanMoveWindow = falsefromGhosttyTerminalNSView(mirrors Ghostty'sNonDraggableHostingView), and setisMovable = falsewhile hidden. Programmatic moves — including AX-driven tiling window managers — are unaffected.NSScrollPocketis hosted in the titlebar area (moved there outright on macOS 27, ghostty#13390) and sat over the terminal's top rows, blocking text selection. Hidden while the option is on — every pocket, since one can exist per scroll view.NSScrollView's automatic content insets pushed the overlay scroller's track down by the titlebar overlap, so it started below the window top.SurfaceScrollViewopts out (automaticallyAdjustsContentInsets = false) — a no-op while the chrome is visible, since the pane then sits below the titlebar and the insets are zero.All of it re-asserts from every
WindowAppearance.sync(AppKit rebuilds titlebar subviews on becomeMain/fullscreen transitions) and applies live in both directions — no restart, window frame unchanged, surfaces untouched.Verified
mise run format,mise run lint, andmise run testall passmise run run) and confirmed the behaviorPreferencesbool with no logic; matches existing toggles, which carry no per-setting testsVerified in a hermetic instance and by hand on a real setup (macOS 27, AeroSpace): chrome gone with content at the top edge, no drag from the top strip, text selection works in the top rows, scroller starts at the window top, stock chrome returns losslessly when toggled back, and the Settings pane disables the tab-switcher pickers while hidden.
Notes for reviewers
macos-titlebar-style = hidden); close/minimize stay available via the Window menu and hotkeys.NSToolbarFullScreenWindowparented to the main window, andtitlebarContainerfollows that hop (Ghostty'sTerminalWindowpattern).toolbar.isVisible = falseremoves the overlay's 52pt slot, and hiding the overlay's root view blanks the top-edge hover reveal (its slide animates the overlay's alpha back, so a plainalphaValue = 0didn't survive).🤖 Generated with Claude Code