feat(selection): Add triple-click and selection improvements#115
feat(selection): Add triple-click and selection improvements#1150xBigBoss wants to merge 9 commits intocoder:mainfrom
Conversation
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
Additional changes for discussionWe 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:
Would these be useful upstream, or are they too VS Code-specific? Happy to submit a separate PR if there's interest. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ad518e433
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) { | ||
| // Clear same-cell selection from click-without-drag | ||
| this.selectionStart = null; | ||
| this.selectionEnd = null; | ||
| return; |
There was a problem hiding this comment.
Mark dirty rows when clearing same-cell selection
Clearing a click-without-drag selection here bypasses clearSelection(), so no dirty rows are recorded. Because hasSelection() now returns true for same-cell selections, a render can paint the highlight between mousedown and mouseup; after you null out the selection and return, the renderer has no reason to redraw that row, so the highlight can stick until some unrelated redraw (cursor blink/move, new text). Consider calling clearSelection() or at least markCurrentSelectionDirty() before nulling out the selection so the row is repainted.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
@0xBigBoss I noticed this as well while trying out your changes
Summary
This PR adds comprehensive selection improvements to ghostty-web:
event.detailfor reliable double/triple-click detection instead of custom timing logic/,.,-,_,~for selecting file paths (matching native Ghostty)Test plan
/usr/local/bin