Skip to content

fix: overlay scrollbar covers terminal text - #1057

Open
Milofax wants to merge 2 commits into
manaflow-ai:mainfrom
Milofax:fix/overlay-scrollbar-covers-text
Open

Milofax wants to merge 2 commits into
manaflow-ai:mainfrom
Milofax:fix/overlay-scrollbar-covers-text

Conversation

@Milofax

@Milofax Milofax commented Mar 7, 2026 •

Copy link
Copy Markdown

Summary

  • Narrow surfaceView and documentView frames by the overlay scrollbar width so the Metal drawable and view bounds match
  • Simplify synchronizeCoreSurface() to read surfaceView.frame.width directly, avoiding double-subtraction

Problem

The overlay scrollbar in GhosttySurfaceScrollView sits on top of terminal text instead of text wrapping to leave space for it. This makes text unreadable behind the scrollbar, especially in long-running sessions.

Root cause: synchronizeGeometryAndContent() sets surfaceView.frame to the full scrollView.bounds.size, then synchronizeCoreSurface() tells Ghostty to render at a narrower width (minus scrollbar). The Metal layer's drawableSize is narrower than the view's bounds, causing the rendered content to stretch — the scrollbar gutter space is lost.

Solution

  1. Subtract overlayScrollbarInsetWidth() from surfaceView.frame.size.width so the Metal drawable matches the view bounds
  2. Subtract the same inset from documentView.frame.size.width for consistent scroll view content
  3. Simplify synchronizeCoreSurface() to use surfaceView.frame.width directly

Test plan

  • Open a terminal pane, run a command with long wrapped lines — scrollbar appears next to text, not on top
  • Text wraps at the correct column (before the scrollbar gutter)
  • Resize the window — no visual glitches during live resize
  • Test with split panes — both panes handle scrollbar correctly
  • Build succeeds locally (BUILD SUCCEEDED verified)

🤖 Generated with Claude Code


Summary by cubic

Fixes overlay scrollbar covering terminal text by sizing the terminal surface to exclude the scrollbar gutter. Text now wraps before the scrollbar, and the Metal drawable matches the view bounds.

  • Bug Fixes
    • Subtract overlayScrollbarInsetWidth() from surfaceView and documentView, clamped to >= 0 and using the same adjusted width for both.
    • Use surfaceView.frame.width in synchronizeCoreSurface() to avoid double-subtraction/stretching.

Written for commit 5969be6. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed layout issues caused by the overlay scrollbar gutter so content no longer clips at the right edge.
    • Ensured viewport and content widths are calculated consistently, preventing misalignment when the overlay scrollbar is present.
    • Improved surface synchronization so rendered content stays correctly positioned and sized during scrolling and resizing.

@vercel

vercel Bot commented Mar 7, 2026

Copy link
Copy Markdown

@Milofax is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 7, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 866d0fed-e675-4998-8b03-a68f9a215876

📥 Commits

Reviewing files that changed from the base of the PR and between b0b33b4 and 5969be6.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

📝 Walkthrough

Walkthrough

Adjusted layout calculations in GhosttyTerminalView to subtract the overlay scrollbar inset when computing target surface and document widths; synchronizeCoreSurface now derives the core surface width from surfaceView.frame.width instead of scrollView content width.

Changes

Cohort / File(s) Summary
Overlay Scrollbar Gutter Sizing
Sources/GhosttyTerminalView.swift
Subtract overlayScrollbarInsetWidth() from target surface and document widths; update synchronizeCoreSurface to use surfaceView.frame.width as the source of truth for surface width.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 I nudged the gutter, snug and slight,
Frames now fit tidy, left to right.
No sliver missed, no pixel stray,
Scrolling smooth as springtime play. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main fix: preventing the overlay scrollbar from covering terminal text, which is the primary change in this changeset.
Description check ✅ Passed The PR description is comprehensive and includes all required template sections: Summary, Problem, Solution, and a detailed Test plan. It explains the root cause, implementation details, and expected outcomes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 5266-5271: Clamp the scrollbar-adjusted width into a single
non-negative value and reuse it for both surfaceView.frame.size.width and
documentView.frame.size.width to avoid negative sizes during live resize or
split collapse; compute let adjustedWidth = max(0, targetSize.width -
overlayScrollbarInsetWidth()) (or similar), set surfaceView.frame.size.width =
adjustedWidth and documentView.frame.size.width = scrollView.bounds.width -
overlayScrollbarInsetWidth() replaced with the same clamped adjusted value (or
compute another clamped value for the scrollView case) so the same non-negative
width is stored and subsequent layout/resize logic (including the code path
referenced by the early-exit at the later resize check) sees consistent,
non-negative geometry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bfbbad5-514c-4627-8df2-f8463900c26c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb5e19 and 4ba009c.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
@greptile-apps

greptile-apps Bot commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the overlay scrollbar covering terminal text in GhosttySurfaceScrollView by narrowing surfaceView.frame.size.width and documentView.frame.size.width by overlayScrollbarInsetWidth(), so the Metal drawable size matches the view bounds. However, the simplification of synchronizeCoreSurface() introduces a regression for legacy (non-overlay) scrollbars.

Key changes:

  • synchronizeGeometryAndContent(): subtracts overlayScrollbarInsetWidth() from both surfaceView.frame.size.width and documentView.frame.size.width, correctly resolving the Metal drawable/view-bounds mismatch for overlay scrollers
  • synchronizeCoreSurface(): changed from scrollView.contentSize.width - overlayScrollbarInsetWidth() to surfaceView.frame.width

Issue found:
When a user's system is configured to always show legacy scrollbars (System Preferences → Appearance → "Always"), AppKit overrides the scroll view's scrollerStyle to .legacy at runtime. In this case, overlayScrollbarInsetWidth() returns 0, so surfaceView.frame.width = scrollView.bounds.width. However, for legacy scrollers, the actual visible content area is scrollView.contentSize.width, which is narrower by the scrollbar's physical width. The new code tells libghostty a wider value, causing text to wrap at the wrong column count.

Confidence Score: 3/5

  • Safe for users with overlay scrollbars (macOS default) but introduces a terminal column-count regression for users whose system is configured to always show legacy scrollbars.
  • The overlay scrollbar fix is correct and well-reasoned. The regression path (legacy scrollers) requires a specific system preference to trigger and won't affect most users, but it is a real behavioral difference — libghostty is told an incorrect content width — that should be addressed before merging.
  • Sources/GhosttyTerminalView.swift — specifically the synchronizeCoreSurface() function and its interaction with overlayScrollbarInsetWidth() under .legacy scroller style.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[synchronizeGeometryAndContent] --> B[Set scrollView.frame = bounds]
    B --> C[Compute targetSize = scrollView.bounds.size]
    C --> D[scrollbarInset = overlayScrollbarInsetWidth]
    D --> E{scrollerStyle == .overlay\nAND hasVerticalScroller?}
    E -- No / legacy --> F[return 0]
    E -- Yes --> G{AppKit already reserved\nalreadyReserved > 0.5?}
    G -- Yes --> F
    G -- No --> H[return measured/fallback scroller width]
    F --> I[surfaceView.frame.width = bounds.width]
    H --> J[surfaceView.frame.width = bounds.width - scrollbarInset]
    I --> K[synchronizeCoreSurface\nwidth = surfaceView.frame.width\n= bounds.width ⚠️ too wide for legacy]
    J --> L[synchronizeCoreSurface\nwidth = surfaceView.frame.width\n= bounds.width - inset ✅ correct]
    K --> M[pushTargetSurfaceSize\nwrong column count for legacy scrollers]
    L --> N[pushTargetSurfaceSize\ncorrect column count for overlay scrollers]
Loading

Comments Outside Diff (1)

  1. Sources/GhosttyTerminalView.swift, line 6366-6373 (link)

    Regression for legacy (non-overlay) scrollers

    This simplification introduces a regression when the scrollbar style is .legacy (e.g., when a user's system is set to "Always Show Scroll Indicators"). In that case, overlayScrollbarInsetWidth() returns 0 because of the guard on line 6377, so surfaceView.frame.width becomes scrollView.bounds.width.

    For legacy scrollbars, AppKit reserves physical space in the view bounds but the actual visible content area is scrollView.contentSize.width — which is narrower than bounds.width. The old code correctly used:

    let width = max(0, scrollView.contentSize.width - overlayScrollbarInsetWidth())
    // = scrollView.contentSize.width (correct for legacy)

    But the new code now uses surfaceView.frame.width = scrollView.bounds.width, which is too wide for legacy scrollers. This causes libghostty to compute an incorrect column count, and text can wrap or overflow behind the scrollbar.

    To reproduce, switch your system scroll-indicator preference to "Always" (System Preferences → Appearance), which forces AppKit to use legacy scrollbars.

    Suggested fix:

    private func synchronizeCoreSurface() {
        let inset = overlayScrollbarInsetWidth()
        let width: CGFloat
        if inset > 0 {
            width = surfaceView.frame.width   // already narrowed by inset in synchronizeGeometryAndContent
        } else {
            width = max(0, scrollView.contentSize.width)  // legacy or no scroller: use content area
        }
        let height = surfaceView.frame.height
        guard width > 0, height > 0 else { return }
        surfaceView.pushTargetSurfaceSize(CGSize(width: width, height: height))
    }

Last reviewed commit: 4ba009c

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:5268">
P2: Clamp the computed content width to zero before assigning frame sizes; the new subtraction can produce negative widths during narrow/transition layouts.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift Outdated
The overlay scrollbar sat on top of terminal text because surfaceView
was sized to the full scrollView bounds while the Metal drawable was
told to render at a narrower width. This mismatch caused the rendered
content to stretch, losing the scrollbar gutter space.

Subtract overlayScrollbarInsetWidth() from both surfaceView and
documentView frames so the Metal drawable and view bounds match,
and simplify synchronizeCoreSurface() to read surfaceView.frame.width
directly instead of double-subtracting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Milofax
Milofax force-pushed the fix/overlay-scrollbar-covers-text branch from 4ba009c to b0b33b4 Compare March 7, 2026 22:38
Address review feedback: during narrow live resize or split collapse,
subtracting the scrollbar inset could produce negative widths. Clamp
to zero and reuse the same adjusted value for both surfaceView and
documentView frames.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@teamleaderleo teamleaderleo added S3: minor Wrong behavior with a workaround area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants