Skip to content

Add a native Show Toolbar option that hides the whole title bar - #234

Merged
thdxg merged 10 commits into
mainfrom
claude/titlebar-hide-feasibility-baa8cd
Aug 7, 2026
Merged

Add a native Show Toolbar option that hides the whole title bar#234
thdxg merged 10 commits into
mainfrom
claude/titlebar-hide-feasibility-baa8cd

Conversation

@thdxg

@thdxg thdxg commented Aug 7, 2026

Copy link
Copy Markdown
Owner

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 the NavigationSplitView. Hiding the window toolbar makes AppKit collapse the whole titlebar of a .fullSizeContentView window, traffic lights included. The visibility parameter (rather than an if-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 8pt safeAreaPadding (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.
  • Drag band (WindowAppearance.syncTitleBarHidden): even fully collapsed, the titlebar container tracks its old rect and AppKit keeps a titlebar-height drag band (the area above contentLayoutRect) that moves the window from any hit view answering mouseDownCanMoveWindow. Ghostty's hidden style fixes this by overriding contentLayoutRect on its NSWindow subclass; SwiftUI owns our window class, so the public equivalents are: hide the NSTitlebarContainerView (as Ghostty also does), answer mouseDownCanMoveWindow = false from GhosttyTerminalNSView (mirrors Ghostty's NonDraggableHostingView), and set isMovable = false while hidden. Programmatic moves — including AX-driven tiling window managers — are unaffected.
  • Click-blocking pocket: the macOS 26+ scroll-edge-effect NSScrollPocket is 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.
  • Scroller inset: NSScrollView's automatic content insets pushed the overlay scroller's track down by the titlebar overlap, so it started below the window top. SurfaceScrollView opts 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, and mise run test all pass
  • Built and ran the change in the app (mise run run) and confirmed the behavior
  • Added or updated tests for new model / persistence / palette / hotkey logic — plain Preferences bool with no logic; matches existing toggles, which carry no per-setting tests

Verified 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

  • The original issue asked for Terminal.app-style auto-minimize (expand when tabs > 1). That variant was spiked and works, but the shipped behavior is an unconditional hide by explicit choice — the sidebar is already the tab UI, and chrome popping in on tab spawn read as a bug in practice.
  • With the toolbar hidden the window cannot be moved by dragging at all (same end state as Ghostty's macos-titlebar-style = hidden); close/minimize stay available via the Window menu and hotkeys.
  • Native fullscreen (toolbar hidden) is handled: AppKit hosts the fullscreen titlebar in a separate NSToolbarFullScreenWindow parented to the main window, and titlebarContainer follows that hop (Ghostty's TerminalWindow pattern). toolbar.isVisible = false removes 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 plain alphaValue = 0 didn't survive).
  • A White bar visible at the top when in fullscreen #24-style fix for the toolbar-VISIBLE fullscreen bar (paint the overlay's titlebar with the window background) was tried on this branch and reverted: it makes the detail column seamless but trades the gray seam for a color seam at the sidebar column, whose surface shade the flat paint can't match — the same reason the 2026-05 attempt on White bar visible at the top when in fullscreen #24 wasn't landed. The stock gray fullscreen toolbar stays for now.

🤖 Generated with Claude Code

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.
@thdxg thdxg added the enhancement New feature or request label Aug 7, 2026
@github-actions github-actions Bot added the area:ui Views, Settings UI label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@e7abb22b9 this branch Δ
focused CPU % 1.00 1.20 +20%
Memory (RSS MB) 109.7 109.2 -0%
CPU ms/s (powermetrics) 9.5 11.6 +23%
Wakeups/s (powermetrics) 153.9 180.6 +17%
workload-focused CPU % 2.90 2.90 +0%
Memory (RSS MB) 167.1 166.8 -0%
CPU ms/s (powermetrics) 26.2 28.6 +9%
Wakeups/s (powermetrics) 212.3 266.3 +25% 🔺
workload-unfocused CPU % 2.70 2.30 -15%
Memory (RSS MB) 157.8 170.0 +8%
CPU ms/s (powermetrics) 26.3 22.0 -17%
Wakeups/s (powermetrics) 194.5 286.1 +47% 🔺

⚠️ Labeled benchmark:regression

This PR is labeled benchmark:regression because ≥2 metrics regressed by ≥25% vs main@e7abb22b9 (beyond each metric's noise floor), at least one under workload:

  • workload-focused — Wakeups/s (powermetrics): 212.3 → 266.3 (+25%)
  • workload-unfocused — Wakeups/s (powermetrics): 194.5 → 286.1 (+47%)

Reported value is the median of 3×10s windows per state (splitting the window and taking the median keeps one co-scheduled spike from skewing a state); CPU % is the process CPU-time delta over a window. Runs land on different shared runners, so treat small deltas as noise — 🔺/🔻 marks changes ≥25% that also clear the metric's absolute noise floor (CPU % ≥0.5, Memory (RSS MB) ≥25, CPU ms/s ≥5, Wakeups/s ≥50); CPU deltas off a noise-dominated baseline aren't flagged (CPU % baseline ≥1.5, CPU ms/s baseline ≥15). The benchmark:regression / benchmark:improvement label needs corroboration — ≥2 flagged metrics in the same direction, at least one under workload — so a lone noisy cell shows its arrow here without tagging the PR.

thdxg added 2 commits August 8, 2026 00:42
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.
@github-actions github-actions Bot added the area:terminal Terminal surface, ghostty integration label Aug 7, 2026
thdxg added 2 commits August 8, 2026 01:00
… 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.
@thdxg thdxg changed the title Add a native Hide Title Bar option Add a native Show Toolbar option that hides the whole title bar Aug 7, 2026
thdxg added 2 commits August 8, 2026 01:10
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.
@github-actions github-actions Bot added the benchmark:regression CI benchmark: significant resource regression vs main label Aug 7, 2026
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.
@github-actions github-actions Bot removed the benchmark:regression CI benchmark: significant resource regression vs main label Aug 7, 2026
thdxg added 2 commits August 8, 2026 01:43
… 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.
@thdxg
thdxg enabled auto-merge (squash) August 7, 2026 16:47
@thdxg
thdxg merged commit afaacff into main Aug 7, 2026
8 checks passed
@thdxg
thdxg deleted the claude/titlebar-hide-feasibility-baa8cd branch August 7, 2026 16:49
@github-actions github-actions Bot added the benchmark:regression CI benchmark: significant resource regression vs main label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:terminal Terminal surface, ghostty integration area:ui Views, Settings UI benchmark:regression CI benchmark: significant resource regression vs main enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to have a minimized title bar

1 participant