Skip to content

Mouse and clipboard#17

Open
nedtwigg wants to merge 35 commits intomainfrom
mouse-and-clipboard
Open

Mouse and clipboard#17
nedtwigg wants to merge 35 commits intomainfrom
mouse-and-clipboard

Conversation

@nedtwigg
Copy link
Copy Markdown
Member

This adds our new mouse and clipboard behavior. Still TODO is update the tutorial to demonstrate (and test) it all, particularly for TUI's which have opted-in to mouse handling.

nedtwigg and others added 30 commits April 17, 2026 15:08
New module `lib/src/lib/mouse-selection.ts` holds mouse-reporting,
bracketed-paste, override, selection, and hint-token state per
terminal. Mirrors the subscribeToSessionStateChanges / getSnapshot
pattern in terminal-registry.ts so it plugs into useSyncExternalStore.

Enforces two spec rules at the store level: override can't be
activated while mouse reporting is off, and mouse reporting going
to none auto-ends any active override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ET modes

New `mouse-mode-observer.ts` registers parser hooks on every xterm
terminal that fire on DECSET (CSI ? ... h) and DECRST (CSI ? ... l),
returning false so xterm still handles the sequence. A queueMicrotask
then syncs our store from `terminal.modes.mouseTrackingMode` and
`terminal.modes.bracketedPasteMode` once xterm's own handler has run.

terminal-registry wires the observer into setupTerminalEntry and
calls removeMouseSelectionState in destroyTerminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Used downstream for choosing Cmd vs Ctrl paste/copy keybindings.
Guards against undefined navigator so it's safe in node test envs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TerminalPaneHeader now subscribes to the mouse-selection store and
renders a CursorClickIcon between the title and the alarm bell
whenever the inside program has requested mouse reporting. When
an override is active, a Prohibit glyph is composited on top and
the tooltip/aria-label switch to the "restore" wording from spec
§1.2. Clicking toggles a temporary override on, or ends any
active override.

Four Storybook states: hidden, reporting on, temporary override,
permanent override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Portal banner anchored below the No-Mouse icon shows while the
override is temporary, with Make-permanent and Cancel buttons
that toggle the store. Kept in sync with scroll/resize like
TodoAlarmDialog. Per spec §2.4 the banner is mouse-only.

The existing TemporaryOverride story in MouseHeaderIcon.stories.tsx
now exercises the banner as well.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds beginDrag / updateDrag / endDrag / isDragging state helpers
to mouse-selection.ts and wires capture-phase mousedown/mousemove/
mouseup listeners in setupTerminalEntry.

Classifies each click per spec §6.1 state matrix: terminal owns
the drag if mouse reporting is off, an override is active, or the
mousedown originated in scrollback. Otherwise the event is left
alone to bubble to xterm and forward to the inside program.

This story intentionally does NOT stopPropagation — we observe
events and build selection state while xterm's default visible
selection still works. C.2 adds the overlay and takes over event
handling fully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New SelectionOverlay component renders the current selection as
absolute-positioned rectangles above the xterm grid: one per row
for linewise, one total for block. Uses the page's
--vscode-terminal-selectionBackground for the fill color so it
matches the theme.

Terminal overlay dimensions (cols, rows, viewportY, baseY, element
size) are exposed via getTerminalOverlayDims. A global renderTick
bumped by terminal.onRender / onResize lets the overlay
re-measure and reposition on scroll or resize without each pane
needing its own subscription plumbing.

computeRects is tested exhaustively for normalization, multi-row
linewise, block, and viewport clipping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setDragAlt flips the in-progress selection between linewise and
block shape when Alt is pressed or released without the mouse
moving. Window-level keydown/keyup listeners in setupTerminalEntry
call it while a drag is active.

SelectionOverlay renders a "Hold Alt for block selection" hint
above the drag-end cell while dragging. The hint is clamped
inside the overlay bounds and hidden once the drag ends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lipboard

New modules:
- rewrap.ts: Copy Rewrapped transform — strips box-drawing frame
  lines, strips leading/trailing border chars from remaining lines,
  joins wrapped paragraphs with spaces, preserves blank-line
  paragraph separators. Table-driven tests cover frames, tables,
  paragraph unwrap, edge cases.
- selection-text.ts: extract the cells covered by a Selection from
  an xterm buffer, handling linewise and block shapes (block is
  rectangular slab, not rewrapped).
- clipboard.ts: copyRaw / copyRewrapped / doPaste glue over
  navigator.clipboard, keyed by terminal id. doPaste wraps the
  text in bracketed-paste markers when the inside program has
  enabled bracketed paste.

getTerminalInstance exposed from terminal-registry for features
that need direct buffer access.

Copy/paste UI and keyboard shortcuts follow in D.1/D.3/F.1-F.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New SelectionPopup shown after mouse-up on an active selection
with Copy Raw and Copy Rewrapped buttons. Labels switch Cmd/Ctrl
based on IS_MAC. Dismissed by Esc, click-outside, or a completed
copy.

Pond's keydown handler gets an early branch that intercepts
Cmd/Ctrl+C (and +Shift) before the passthrough-mode short-circuit
when the selected pane has a finalized terminal selection. This
is the only place where Ctrl+C is prevented from reaching the
inside program — spec §4.2's narrow rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cmd+V / Cmd+Shift+V on macOS and Ctrl+V / Ctrl+Shift+V on other
platforms are intercepted in Pond's keydown handler and routed
to doPaste, which reads the clipboard and writes it to the PTY
wrapped in bracketed-paste markers when the inside program has
opted in (already implemented in D.2a).

Ctrl+V on macOS is intentionally NOT intercepted and still reaches
the inside program as the raw 0x16 control byte (spec §8.2.1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New smart-token.ts exposes detectTokenAt(line, col): finds the
whitespace-delimited token at the given position, tests it
against URL/path/error-location patterns, and strips unlikely-
trailing characters (periods, commas, unmatched brackets) per
spec §5.1. Matched pairs like the trailing `)` in wikipedia-style
URLs are preserved.

Table-driven tests cover each listed pattern, trailing-punctuation
rules, non-matches, and position sensitivity within a token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mousemove listener in terminal-registry reads the buffer line
under the drag cursor, runs detectTokenAt, and stores any matching
URL/path as the hintToken. SelectionOverlay now shows both the
"Hold Alt" hint and, when a token is detected, "Press e to select
the full URL/path".

extendSelectionToToken in mouse-selection respects drag direction:
the anchor stays, the end jumps to whichever token boundary is
farther from the anchor, so left-to-right and right-to-left drags
both feel natural.

Pond's keydown handler gets a mid-drag branch that handles e
(extend) and Esc (cancel), both consumed so they don't reach
the inside program.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a drag ends, setupTerminalEntry snapshots the selected text
as a baseline. Each subsequent terminal.onRender re-extracts the
current text over the same coordinates and cancels the selection
if anything differs. terminal.onResize also cancels unconditionally,
per spec §3.4.

While a drag is in progress the baseline is held at null so the
active selection isn't fighting the user's own updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the mouse router decides the terminal should handle a drag,
it now calls preventDefault + stopPropagation on mousedown,
mousemove, and mouseup. That prevents xterm's built-in text
selection from starting in parallel with ours — which would have
left two highlighted regions on top of each other.

Propagation is only stopped when the terminal owns the drag;
program-bound events (mouse reporting on, no override, live
region) still flow through to xterm untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the new spec to the AGENTS.md index with the usual one-line
summary and the list of files it covers, following the existing
entries' format. Stories C.5 (auto-scroll during drag), F.3
(right-click / Edit menu paste), and the Copy Rewrapped heuristic
refinement stay deferred to follow-ups per spec §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…handler blocks in Pond, remove unused BOX_RUN_GLOBAL, flatten token hint logic
…ike block) and add try/catch for clipboard API errors
Replace the per-row translucent-fill divs with a single SVG path that
traces the perimeter of the whole selection. For multi-row linewise
selections the path walks the right edges down, the bottom of the last
row, then the left edges up in reverse — so width transitions between
rows produce proper outside corners instead of stacked-rectangle
double-lines.

Border color prefers --vscode-focusBorder (typically fully opaque),
falls back to --vscode-terminal-foreground, then selectionBackground,
then a cornflower default. This renders cleanly across themes where
the translucent fill previously looked bad.

New rectsToPath helper has coverage for empty, single-rect, two-row Z,
three-row with full-width middle, and block shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both computeCell (mouse hit-testing) and computeRects (overlay)
were deriving cell size from element.width / cols and element.height
/ rows. xterm actually renders into `.xterm-screen` inside the
element with a few pixels of padding around it, so each cell was
(padding / rows) px taller than reality. The per-cell error is
negligible but it accumulated linearly — the selection border
drifted out of alignment as row count grew.

getTerminalOverlayDims now queries `.xterm-screen`, exposing the
measured cellWidth / cellHeight plus gridLeft / gridTop offsets.
computeCell uses the same helper so mouse and highlight can't drift
apart. SelectionOverlay translates its SVG path by (gridLeft, gridTop)
and positions the mid-drag hint with the same offsets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes to the Alt hint and copy popup:

1. Never flip sides when the preferred side would clip off-screen.
   The old fallback put the hint INSIDE the selection for drag-up
   near the viewport top, and bounced in and out as the mouse
   crossed the fit-check threshold. Now we clamp to the same-side
   viewport edge instead.

2. Add one cellHeight of gap on the drag-down side so the line
   adjacent to the selection stays visible. The drag-up side
   already felt like two lines of clearance because the hint's
   own height extends it away from the selection; shifting
   drag-down by one row matches that visual weight.

3. The hint label reads "Hold Opt for block selection" on macOS
   and "Hold Alt for block selection" elsewhere, driven by
   IS_MAC from lib/platform. Spec §3.3 updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nedtwigg and others added 5 commits April 20, 2026 15:20
Popup outer container: drop px-1.5 py-0.5 and gap-1; add
items-stretch + overflow-hidden so button hover backgrounds paint
edge-to-edge and get clipped by the container's rounded corners.
Buttons: drop border-border and inner p-0; use px-1.5 py-0.5 so
the popup height equals the Alt hint height and hover fills the
button's full hit area.

New stories file lib/src/stories/TextSelection.stories.tsx mounts
a real TerminalPane with SCENARIO_LS_OUTPUT and drives selection
state directly via setSelection. Six variants: linewise outline,
block outline, Alt hint for drag-up and drag-down, copy popup for
drag-up and drag-down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drag-up used to compute `top` by subtracting a per-element height
estimate, so the popup (~32px est) sat 12px closer to the selection
than the hint (~44px est). Now both elements anchor their BOTTOM
edge via CSS `bottom`, so heights cancel out and the popup lands
exactly where the hint was. Symmetric with drag-down, which
already top-anchors via a height-free formula.

Also shift the drag-up anchor up by one full cell, so the row
adjacent to the selection stays visible on both sides — matching
the +2-row offset on the drag-down top-anchored path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The separate "Copied!" confirmation card shifted position because
it had different padding and content than the popup it replaced.
Now the popup stays mounted and the pressed button signals the
press in situ: accent background + text color, inline check mark
where the shortcut label was, plus a 260ms back-eased scale pop.

Uses the existing flashCopy state machine so both the Cmd+C
keyboard path and the button click look identical. Respects
prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shortcut label is now kept in the DOM as an invisible span
when the button is flashed, so it reserves its width. The check
icon is overlaid via absolute positioning + flex centering inside
that reserved box. Button size no longer jumps between the
normal and the just-pressed state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 20, 2026

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 25c3d23
Status: ✅  Deploy successful!
Preview URL: https://89b467d4.mouseterm.pages.dev
Branch Preview URL: https://mouse-and-clipboard.mouseterm.pages.dev

View logs

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