Skip to content

Show search matches as scrollbar tick marks, colored like the text highlights#176

Merged
thdxg merged 2 commits into
thdxg:mainfrom
jcuhnoio:feat/search-scrollbar-ticks
Jul 20, 2026
Merged

Show search matches as scrollbar tick marks, colored like the text highlights#176
thdxg merged 2 commits into
thdxg:mainfrom
jcuhnoio:feat/search-scrollbar-ticks

Conversation

@jcuhnoio

@jcuhnoio jcuhnoio commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Cmd+F now marks every match's position in the scrollbar track (Xcode-style ticks), so you can see where matches live in the scrollback at a glance. The selected match's tick is drawn last and in the selected-highlight color, so it stands out from candidate matches.

Design

libghostty never reports match positions — its search API only emits counts (SEARCH_TOTAL / SEARCH_SELECTED). So tick rows are recovered Macterm-side:

  • SearchTicks (pure, unit-tested): reads the scrollback via the existing ghostty_surface_read_text path and scans it with the same ASCII-case-insensitive literal match ghostty's search uses (std.ascii.indexOfIgnoreCase in sliding_window.zig), re-wrapping logical lines at the surface's column count to map matches to visual rows. The match list stays 1:1 with ghostty's, so the SEARCH_SELECTED index (counted from the newest match) picks the selected tick; if the counts ever disagree, the selected highlight is dropped rather than drawn on the wrong tick.
  • SearchTickOverlay: an NSView pinned to the scroller track in tile(), above the overlay scroller so ticks stay visible while it fades out, with hit-testing disabled so scrollbar click/drag is untouched.
  • Wiring: SurfaceScrollView re-scans on SEARCH_TOTAL updates (coalesced 100ms — totals stream while ghostty walks history), recolors the selected tick on SEARCH_SELECTED, clears on search end. The scan runs against the needle actually sent to the core, not the live (possibly newer) field text.

Tick colors mirror the renderer's search highlight backgrounds (search-background / search-selected-background, defaults golden yellow #FFE082 / soft peach #F2A57E), so ticks read as the same colors as the highlighted text. Those keys are TerminalColor unions with no cval, so ghostty_config_get can't return them — a new pure SearchHighlightColors helper scans the user's config text (same pattern and config-file-include limitation as ShellIntegrationFeatures), honoring hex overrides and falling back to the defaults for named-X11 / cell-* values a tick can't reproduce.

Known approximations (noted in code comments)

  • Columns are counted as UTF-8 code points, so double-width cells can drift a tick by one row — invisible at scrollbar scale.
  • A match spanning a hard line break isn't found (ghostty itself is ambiguous there).

Testing

  • Unit tests for SearchTicks (wrap arithmetic, case-insensitivity, non-overlap, selected-from-end mapping, multibyte counting) and SearchHighlightColors (defaults, hex/quoted parsing, last-wins, fallback semantics).
  • mise run format, lint, and the full suite pass. Overlay drawing is AppKit view code, intentionally not unit-tested per repo conventions.

Summary by CodeRabbit

  • New Features

    • Added scrollbar markers showing the locations of terminal search matches.
    • Highlighted the currently selected match with a distinct marker.
    • Search markers update as queries, selections, and scrollback content change.
    • Marker colors now follow configured search highlight colors, with sensible defaults.
  • Bug Fixes

    • Improved search matching across wrapped lines, mixed case, and multibyte text.

jcuhnoio added 2 commits July 20, 2026 13:07
…llow)

libghostty's search only reports match counts (SEARCH_TOTAL/SELECTED),
never positions, so tick rows are recovered Macterm-side: on each total
update (coalesced 100ms) the scrollback text is re-read via
ghostty_surface_read_text and scanned with the same ASCII-case-
insensitive literal match ghostty uses, re-wrapping logical lines at the
surface width to map matches onto visual rows. The match list stays 1:1
with ghostty's so the SEARCH_SELECTED index (counted from the newest
match) picks the orange tick; a count mismatch just drops the orange
highlight rather than mislabeling a tick.

Ticks draw in an overlay pinned to the scroller track in tile(), above
the scroller so they survive overlay-scroller fade, with hit-testing
disabled so the scroller keeps full click/drag behavior. Colors come
from MactermTheme: ANSI yellow for matches; orange has no ANSI slot, so
the selected tick is the one fixed system color.
The ticks previously used ANSI yellow + systemOrange, which didn't match
what the renderer actually paints. Ghostty highlights matches with
search-background / search-selected-background (defaults: golden yellow
#FFE082, soft peach #F2A57E). Those keys are TerminalColor unions with
no cval, so ghostty_config_get can't return them — the user's config
text is scanned directly (same pattern and config-file-include
limitation as ShellIntegrationFeatures), falling back to ghostty's
documented defaults. Ticks grow 2x4pt to 4x2pt-inset for visibility.
@github-actions github-actions Bot added area:ui Views, Settings UI area:terminal Terminal surface, ghostty integration area:state AppState, models, persistence area:config Ghostty config wrappers area:tests Test changes labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds themed search-match ticks to the terminal scrollbar. Search queries trigger debounced scrollback scans, matches are mapped to wrapped visual rows, selected results are tracked, and a non-interactive overlay renders normalized tick positions.

Changes

Search scrollbar ticks

Layer / File(s) Summary
Highlight color resolution
Macterm/Config/SearchHighlightColors.swift, Macterm/Ghostty/Theme.swift, MactermTests/Config/SearchHighlightColorsTests.swift
Search highlight colors are parsed from Ghostty configuration text, converted to NSColor, and tested for defaults, quoting, precedence, comments, and invalid values.
Match row and selection mapping
Macterm/Model/SearchTicks.swift, MactermTests/Model/SearchTicksTests.swift
Scrollback matches are found case-insensitively, mapped across wrapped rows using Unicode code-point widths, and resolved against end-based selection indices.
Scrollbar tick rendering
Macterm/Views/Terminal/SearchTickOverlay.swift
The overlay deduplicates normalized match positions, renders selected ticks above other ticks, and remains non-interactive.
Search lifecycle and overlay updates
Macterm/Model/SplitNode.swift, Macterm/Views/Terminal/SurfaceScrollView.swift, Macterm/Views/TerminalPane.swift
Search state flows from TerminalPane through SurfaceScrollView, which debounces scans, reads scrollback, updates selection, and keeps the overlay aligned with the scrollbar.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TerminalPane
  participant SurfaceScrollView
  participant ghostty_surface_read_text
  participant SearchTicks
  participant SearchTickOverlay
  TerminalPane->>SurfaceScrollView: noteSearchNeedle(query)
  TerminalPane->>SurfaceScrollView: refreshSearchTicks()
  SurfaceScrollView->>ghostty_surface_read_text: read scrollback text
  SurfaceScrollView->>SearchTicks: compute match rows and selected row
  SurfaceScrollView->>SearchTickOverlay: push match rows and total rows
Loading

Suggested reviewers: thdxg

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: search matches rendered as scrollbar tick marks with highlight-matched colors.
Description check ✅ Passed The description covers what, how, and verification, with only minor template differences like an explicit Why section and issue link.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@jcuhnoio

Copy link
Copy Markdown
Collaborator Author

Here's an example of what it looks like:
image

@github-actions

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@469f50be7 this branch Δ
focused CPU % 0.80 1.10 +38%
Memory (RSS MB) 108.3 107.2 -1%
CPU ms/s (powermetrics) 7.4 9.7 +31%
Wakeups/s (powermetrics) 165.2 130.8 -21%
unfocused CPU % 0.70 1.10 +57%
Memory (RSS MB) 101.7 103.7 +2%
CPU ms/s (powermetrics) 6.8 10.2 +52%
Wakeups/s (powermetrics) 156.5 177.7 +14%
minimized CPU % 0.20 0.30 +50%
Memory (RSS MB) 103.0 104.3 +1%
CPU ms/s (powermetrics) 2.0 2.3 +15%
Wakeups/s (powermetrics) 73.3 62.6 -14%
workload-focused CPU % 2.10 2.00 -5%
Memory (RSS MB) 156.9 157.1 +0%
CPU ms/s (powermetrics) 20.8 20.2 -3%
Wakeups/s (powermetrics) 262.3 253.1 -4%
workload-unfocused CPU % 3.00 2.60 -13%
Memory (RSS MB) 157.4 157.5 +0%
CPU ms/s (powermetrics) 29.7 25.3 -15%
Wakeups/s (powermetrics) 257.0 259.8 +1%
workload-minimized CPU % 0.30 0.30 +0%
Memory (RSS MB) 157.4 157.5 +0%
CPU ms/s (powermetrics) 3.3 3.1 -5%
Wakeups/s (powermetrics) 142.6 138.6 -3%

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). Flagged changes add the benchmark:regression / benchmark:improvement label.

@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.

🧹 Nitpick comments (1)
Macterm/Views/Terminal/SurfaceScrollView.swift (1)

243-261: 🚀 Performance & Scalability | 🔵 Trivial

Full scrollback rescan runs synchronously on the main thread up to ~10×/sec during active search.

scanSearchTicks reads the entire scrollback (readText(scrollback: true)) and runs SearchTicks.matchRows on it every time the 100ms debounce fires, driven by every SEARCH_TOTAL update while ghostty streams match counts. For typical scrollback sizes this is likely negligible, but with a large scrollback-limit configured, this repeated synchronous main-thread work could cause visible stutter while the user types/edits a search query.

Worth verifying against a large-scrollback benchmark, and if it's real, considering either a longer/adaptive debounce or offloading the read+scan to a background queue (contingent on ghostty_surface_read_text being safe to call off the main actor).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Macterm/Views/Terminal/SurfaceScrollView.swift` around lines 243 - 261,
Update scanSearchTicks and its 100ms debounce path to avoid repeatedly
performing the full scrollback read and SearchTicks.matchRows synchronously on
the main thread during active search. Verify the behavior with a large
scrollback benchmark, then use a longer or adaptive debounce or safely offload
the read-and-scan work from the main actor, confirming ghostty_surface_read_text
is thread-safe before doing so; preserve existing tick clearing and result
updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Macterm/Views/Terminal/SurfaceScrollView.swift`:
- Around line 243-261: Update scanSearchTicks and its 100ms debounce path to
avoid repeatedly performing the full scrollback read and SearchTicks.matchRows
synchronously on the main thread during active search. Verify the behavior with
a large scrollback benchmark, then use a longer or adaptive debounce or safely
offload the read-and-scan work from the main actor, confirming
ghostty_surface_read_text is thread-safe before doing so; preserve existing tick
clearing and result updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 784f963d-dd7a-43ff-b7af-9829840944a4

📥 Commits

Reviewing files that changed from the base of the PR and between 469f50b and a560ebb.

📒 Files selected for processing (9)
  • Macterm/Config/SearchHighlightColors.swift
  • Macterm/Ghostty/Theme.swift
  • Macterm/Model/SearchTicks.swift
  • Macterm/Model/SplitNode.swift
  • Macterm/Views/Terminal/SearchTickOverlay.swift
  • Macterm/Views/Terminal/SurfaceScrollView.swift
  • Macterm/Views/TerminalPane.swift
  • MactermTests/Config/SearchHighlightColorsTests.swift
  • MactermTests/Model/SearchTicksTests.swift

@thdxg
thdxg merged commit 3c38ec7 into thdxg:main Jul 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:config Ghostty config wrappers area:state AppState, models, persistence area:terminal Terminal surface, ghostty integration area:tests Test changes area:ui Views, Settings UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants