Skip to content

terminal: copy/paste keys, a pane context menu, opt-in copy-on-select (#131) - #133

Merged
Lexus2016 merged 1 commit into
mainfrom
fix/terminal-clipboard-131
Nov 8, 2026
Merged

Lexus2016 merged 1 commit into
mainfrom
fix/terminal-clipboard-131

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

Closes part of #131. The first slice, in the order written in the issue.

Why

Selecting text in the live pane was unreliable and copying was a side effect of it. Every
mouseup that had a selection wrote it to the clipboard, so a drag you did not mean to copy
replaced whatever you had. And the one gesture that would normally give you Copy/Paste —
right-click — had nothing to open: the pane installs no context menu, and the native one is
not usable in the desktop build.

What changed

  • Keyboard: Ctrl+Shift+C / Ctrl+Shift+V on Linux and Windows, Cmd+C / Cmd+V on macOS,
    via attachCustomKeyEventHandler. The handler returns false for a chord it handled —
    without that xterm forwards it to the TUI, so Ctrl+Shift+C would copy and send ^C to
    the running agent. Plain Ctrl+C is untouched, so SIGINT still reaches it.
  • Context menu: Copy / Paste / Select all / Copy on select on right-click. Copy is
    disabled while nothing is selected. A right-button release is skipped before the copy
    branch, so the selection the user just made is what Copy acts on instead of being
    copied and dropped. Paste reuses the existing bracketed-paste path
    (pasteToTerminal), which never auto-submits; it gained an optional { silent } so a
    paste from the keyboard does not toast.
  • Copy-on-select is now opt-in, default off, toggled from that menu and remembered in
    localStorage (ccs:termCopyOnSelect). It used to be unconditional and the only way to
    copy.
  • Selection options: macOptionClickForcesSelection: true,
    rightClickSelectsWord: false, and a title on the pane saying that Shift+drag selects
    when the app reports mouse events.

New i18n keys (term.menu.copy, term.menu.paste, term.menu.selectAll,
term.menu.copyOnSelect, term.select.hint, term.paste.denied) are in all five
dictionaries.

How tested

test/terminal-clipboard.test.js runs the real code — the helper block and
_createTerminal itself are cut out of public/index.html and evaluated in a vm with a
fake xterm, a fake element and a fake clipboard. 12 checks: the chord map per platform,
Ctrl+C passthrough, copy-on-select default and persistence (including a browser that
throws on storage), the menu model, and the mouseup/contextmenu wiring.

test/composer-terminal.test.js keeps all of its assertions; two of them described the old
mouseup shape (no event argument, unconditional copy), so they are re-pointed at the new
one rather than dropped.

Locally: every test module that reads public/index.html passes (20 modules), including
i18n completeness (139 checks) and the new file. The three modules that need
better-sqlite3 (config-resolve, interrupt-idle-config, interrupted-recovery) pass
under Node 24, which is the ABI the module in this checkout was built for. All three inline
script blocks of index.html parse.

Not in this slice: the addons (web-links, unicode11, search, clipboard, webgl) and the
mobile proposals in the second comment — they change rendering or need vendored files.

…#131)

Selecting text in the live pane was unreliable and copying was a side effect of it:
every mouseup with a selection overwrote the clipboard, and a right-click — the obvious
way to reach Copy/Paste — had nothing to open. This is the first slice of #131, in the
order written there.

- Ctrl+Shift+C / Ctrl+Shift+V on Linux and Windows, Cmd+C / Cmd+V on macOS, through
  attachCustomKeyEventHandler. The handler returns false for a chord it handled: without
  that xterm forwards it to the TUI, so Ctrl+Shift+C would both copy and send ^C to the
  running agent. Plain Ctrl+C is untouched, so SIGINT still reaches it.
- The pane answers right-click itself: Copy / Paste / Select all / Copy on select.
  Copy is disabled while nothing is selected, and a right-button release no longer copies
  first, so the selection the user just made is what Copy acts on. Paste reuses the
  existing bracketed-paste path (pasteToTerminal), which never auto-submits; it gained an
  optional { silent } so a paste from the keyboard does not toast.
- Copy-on-select is now a setting, off by default, toggled from that menu and remembered
  in localStorage. It used to be unconditional and the only way to copy.
- macOptionClickForcesSelection: true, rightClickSelectsWord: false, and a title on the
  pane saying Shift+drag selects when the app reports mouse events.

Tests: test/terminal-clipboard.test.js runs the helpers and _createTerminal itself in a
vm with a fake xterm, element and clipboard — chords per platform, Ctrl+C passthrough,
copy-on-select default and persistence, the menu model, and the mouseup/contextmenu
wiring. 12 checks. test/composer-terminal.test.js keeps its assertions but two of them
described the old mouseup shape (no event argument, unconditional copy), so they are
re-pointed rather than dropped.

Verified: every test module that reads public/index.html passes (20 modules, including
i18n completeness at 139 checks with the 6 new keys in all 5 languages); the three that
need better-sqlite3 pass under Node 24, which is what it was built for; all three inline
script blocks of index.html parse.

Not in this slice: the addons (web-links, unicode11, search, clipboard, webgl) and the
mobile proposals in the second comment on the issue. They change rendering or need
vendored files, so they get their own PR.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T21:12:16.229338Z 1672e2a PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Lexus2016
Lexus2016 force-pushed the fix/terminal-clipboard-131 branch from 1672e2a to d0c7e9d Compare November 8, 2026 21:10

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1672e2af17

ℹ️ 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 thread public/index.html
// Dismissal, deferred by a task: the click that opened the menu is still being
// dispatched, and a listener registered now would see it and close immediately.
setTimeout(() => {
document.addEventListener('mousedown', _hideTermMenu, { once: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the context menu attached when pressing one of its items

When a user clicks any menu item, its mousedown bubbles to this document listener, which removes the entire menu before mouseup and click can reach the button. Since the actions are registered on click, Copy, Paste, Select all, and Copy on select cannot be activated normally with the mouse. Ignore presses inside the menu when dismissing it, and close it only after an item action or an outside press.

Useful? React with 👍 / 👎.

Comment thread public/index.html
Comment on lines +19728 to +19729
_pasteFromClipboard();
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent the native paste action for handled clipboard shortcuts

When pressing Cmd+V on macOS or Ctrl+Shift+V in Chromium, this handler reads the clipboard and sends a paste frame but does not call ev.preventDefault(). In the shipped public/vendor/xterm.js, returning false from the custom handler only exits _keyDown; it does not cancel the browser event. The browser can therefore also dispatch its native paste event, which xterm handles separately through onData, inserting the clipboard contents twice. Cancel the default keyboard action before performing the custom paste.

Useful? React with 👍 / 👎.

@Lexus2016
Lexus2016 merged commit e28cb11 into main Nov 8, 2026
3 checks passed
@Lexus2016
Lexus2016 deleted the fix/terminal-clipboard-131 branch November 8, 2026 21:15
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