Skip to content

Comments

feat(selection): Add triple-click and selection improvements#115

Merged
sreya merged 14 commits intocoder:mainfrom
0xBigBoss:feat/selection-improvements
Feb 24, 2026
Merged

feat(selection): Add triple-click and selection improvements#115
sreya merged 14 commits intocoder:mainfrom
0xBigBoss:feat/selection-improvements

Conversation

@0xBigBoss
Copy link
Contributor

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

@0xBigBoss
Copy link
Contributor 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.

@sreya
Copy link
Contributor

sreya commented Jan 13, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines 560 to 564
) {
// Clear same-cell selection from click-without-drag
this.selectionStart = null;
this.selectionEnd = null;
return;

Choose a reason for hiding this comment

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

P3 Badge 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 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

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

@0xBigBoss I noticed this as well while trying out your changes

sreya and others added 2 commits February 24, 2026 01:22
Block cursors now re-draw the character underneath using the
cursorAccent theme color, making text visible on the opaque cursor.
Uses ctx.clip() to prevent wide characters from bleeding outside
the cursor cell.

Co-authored-by: Nathan Cooper <ncoop57@users.noreply.github.com>
… selection, and click handling

- Add triple-click to select entire line (text content only, not full width)
- Use event.detail for reliable double/triple-click detection
- Expand word characters to include path separators like native Ghostty
- Use scrollback-aware line retrieval in triple-click
- Handle single-cell and single-character selections properly
- Clear same-cell selection on mouseup via clearSelection() for proper dirty-row tracking
- Add comprehensive tests for new selection features
@sreya sreya force-pushed the feat/selection-improvements branch from 6ad518e to 268dd2b Compare February 24, 2026 01:22
sreya added 10 commits February 24, 2026 01:42
- Clear selection when user types by calling clearSelection() before
  firing the data event in the input callback (terminal.ts)
- Add tests for scrollback line content accuracy
- Add test for selection clearing on input
- Add test for triple-click scrollback line verification
- selectLines(): use viewportRowToAbsolute() instead of incorrect
  viewportY + row formula
- getWordAtCell(): convert viewport row to absolute row and use
  getScrollbackLine() when in scrollback region
On mousedown, only set selectionStart without selectionEnd. This prevents
the renderer from highlighting the clicked cell before any drag occurs.

On mouseup, check for null selectionEnd (no drag) instead of comparing
start==end coordinates. This also enables single-character selection by
dragging to the same cell, which previously looked like click-without-drag.
…hold

Replace dragMovedToNewCell flag with a 5px pixel-distance threshold
for selection drag detection. This prevents jitter from creating
accidental selections while allowing precise single-char selection
via intentional drag.

- Track mousedown pixel position (mouseDownX/mouseDownY)
- Only begin selection once mouse moves ≥5px from mousedown
- Use squared distance comparison (no sqrt needed)
- Clear selection on mouseup if threshold was never met
@sreya
Copy link
Contributor

sreya commented Feb 24, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3fb59aa94

ℹ️ 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".

Comment on lines 572 to 573
if (!this.selectionEnd || !this.dragThresholdMet) {
this.clearSelection();

Choose a reason for hiding this comment

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

P2 Badge Preserve drag selections when pointer leaves canvas early

This mouseup guard clears the selection whenever dragThresholdMet is false, but that flag is only set in the canvas mousemove handler; if the user drags quickly outside the canvas, the document-level mousemove path can update selectionEnd while dragThresholdMet stays false, so the completed drag is discarded on mouseup. In practice this breaks edge/outside drags (including auto-scroll selection) for fast pointer movement.

Useful? React with 👍 / 👎.

@sreya
Copy link
Contributor

sreya commented Feb 24, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf1a07faba

ℹ️ 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".

lib/renderer.ts Outdated
// Set CSS size (what user sees)
this.canvas.style.width = `${cssWidth}px`;
this.canvas.style.height = `${cssHeight}px`;
this.canvas.style.cursor = 'text';

Choose a reason for hiding this comment

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

P2 Badge Do not force text cursor on the canvas element

Setting the canvas cursor directly to text overrides the inherited cursor from the terminal container, so the link-hover cursor updates in Terminal.handleMouseMove (this.element.style.cursor = link ? 'pointer' : 'text') are no longer visible over the canvas. In practice this regresses hyperlink affordance: links still work, but users no longer get the pointer feedback when hovering them.

Useful? React with 👍 / 👎.

const absoluteRow = this.viewportRowToAbsolute(cell.row);
this.selectionStart = { col: cell.col, absoluteRow };
this.selectionEnd = { col: cell.col, absoluteRow };
this.selectionEnd = null; // Don't highlight until drag

Choose a reason for hiding this comment

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

P2 Badge Keep an initial selection end for edge-drag autoscroll

Initializing selectionEnd to null on mousedown breaks a real drag path: if the pointer leaves the canvas before an in-canvas move updates selectionEnd, document mousemove can enter auto-scroll mode and skip end updates while scrolling, and the auto-scroll extension path only runs when selectionEnd is already set. Mouseup then treats the action as a click and clears it, so fast edge drags (especially from top/bottom rows) can lose the selection entirely.

Useful? React with 👍 / 👎.

… for edge drag support

- Set canvas cursor to 'text' at creation, but update to 'pointer' on link
  hover (both canvas and container) so hyperlink affordance works
- Removed redundant cursor set from renderer.resize()
- Restored selectionEnd = start on mousedown so auto-scroll/edge drag works
- hasSelection() returns false while isSelecting && !dragThresholdMet to
  prevent flash-highlight on click
@sreya
Copy link
Contributor

sreya commented Feb 24, 2026

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. 🚀

ℹ️ 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".

@sreya sreya merged commit 6a1a50d into coder:main Feb 24, 2026
5 checks passed
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.

2 participants