Skip to content

Conversation

@0xBigBoss
Copy link

Summary

This PR adds comprehensive selection improvements to ghostty-web:

  • Triple-click line selection: Select entire lines with triple-click, matching native terminal behavior
  • Browser-native click detection: Use event.detail for reliable double/triple-click detection instead of custom timing logic
  • Path-friendly word selection: Expand word characters to include /, ., -, _, ~ for selecting file paths (matching native Ghostty)
  • Smart line selection: Triple-click selects actual text content, not full terminal width
  • Scrollback support: Line selection works correctly in scrollback buffer
  • Single-cell handling: Properly handle edge cases with single-cell and single-character selections
  • Semi-transparent overlay: Use VS Code-style semi-transparent selection overlay instead of inverted colors

Test plan

  • All existing tests pass (328 tests)
  • Manual: Double-click selects words including paths like /usr/local/bin
  • Manual: Triple-click selects the line content (not trailing whitespace)
  • Manual: Triple-click in scrollback buffer works correctly
  • Manual: Selection overlay is semi-transparent blue (not inverted)
  • Manual: Single-cell clicks don't leave stray highlights

Add triple-click support to SelectionManager for selecting entire lines,
matching standard terminal behavior. Detection uses click timing within
500ms threshold to distinguish from double-click word selection.
The previous implementation tried to track triple-clicks via the dblclick
event, but browsers only fire dblclick on the second click (with detail=2),
not the third. Triple-clicks never triggered line selection.

Fix: Use the click event with event.detail:
- detail === 2: double-click -> select word
- detail >= 3: triple-click -> select line

This is the standard browser API for detecting multi-clicks.
Double-click now selects entire paths (e.g., ~/foo/bar.ts) instead of
breaking at slashes. Added characters to word boundary:
- / (path separator)
- . (file extensions)
- ~ (home directory)
- : (line numbers like file.ts:42)
- @ (emails/usernames)
- + (common in URLs/paths)

This matches native Ghostty terminal behavior.
Match native Ghostty behavior: triple-click now selects only the actual
line content, excluding trailing whitespace/empty cells. Previously it
selected the entire terminal width which showed as a full-width highlight.
Triple-click was using viewport-relative getLine() to compute line
length, which reads the wrong line when the viewport is scrolled
into scrollback. Now uses the same scrollback-aware pattern as
getSelection() - checking absoluteRow against scrollbackLength to
choose between getScrollbackLine() and getLine().
When a line has only one character at column 0, the previous code set
selectionEnd.col = 0, making start == end. hasSelection() then returned
false, preventing the selection from being rendered or copied.

Fix: use endCol + 1 to ensure start != end, and skip selection entirely
for empty lines (endCol = -1). The extra column is harmless because
getSelection() trims trailing empty cells.
…highlight

The previous fix used endCol + 1 to avoid hasSelection() returning false
for single-char lines, but this caused the renderer to highlight an extra
trailing cell.

Fix: modify hasSelection() to distinguish between drag selections (where
same start/end means no drag happened) and programmatic selections (where
same start/end is valid for single-char content like triple-click).

- During drag (isSelecting=true): same-cell = no selection (unchanged)
- Programmatic (isSelecting=false): same-cell = valid selection (new)

This allows single-char line selections without highlighting extra cells.
A click without drag was leaving a one-cell selection that persisted
after mouseup, which differs from native terminal behavior.

Fix: in mouseup handler, check if start equals end (no drag happened)
and clear the selection coordinates. This ensures clicks don't create
selections while preserving programmatic selections (triple-click on
single-char line, select() API).

Simplified hasSelection() since same-cell from click-without-drag is
now cleared in mouseup - any remaining same-cell selection is valid.
Changed selection rendering from solid color replacement to a
semi-transparent overlay (40% opacity) that preserves original
text colors. This matches VS Code's editor selection behavior
and improves readability.

- Selection background now overlays on top of cell background
- Text keeps original colors unless selectionForeground is defined
- Added TODO for configurable opacity via theme.selectionOpacity
@0xBigBoss
Copy link
Author

Additional changes for discussion

We have a few more changes in our fork that enable VS Code webview integration. These are potentially VS Code-specific, so I wanted to gauge interest before submitting a separate PR.

Compare: main...0xBigBoss:ghostty-web:feat/integration-hooks

Changes included:

  1. onLinkClick callback - Allows the host to handle link clicks (e.g., open in external browser from VS Code webview)

  2. Three-way return semantics for customKeyEventHandler

    • Current: true = terminal handles, falsy = default processing
    • Proposed: true = terminal handles, false = bubble to host, undefined = default processing
    • Use case: VS Code needs certain key combos (Cmd+P, Cmd+Shift+P) to bubble up to the host instead of being processed by the terminal
  3. Let Cmd/Meta combos bubble to host - On macOS, Meta+key combos return early so VS Code can handle them (Quick Open, Command Palette, etc.)

Would these be useful upstream, or are they too VS Code-specific? Happy to submit a separate PR if there's interest.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant