mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
feat(terminal): Auto Copy, put a finished selection on the clipboard
App Settings > Terminal & Input > Selection & clipboard > Auto Copy
Selection (`autoCopySelection`, per-device, default OFF). With it on,
highlighting text in the terminal copies it: mouse drag, double-click
word, triple-click line, and the phone long-press selection. Ctrl+C is
untouched and still copies on demand.
Three things decide the shape of it:
* It fires at the END of a gesture, never in onSelectionChange. That
callback runs for every cell a drag crosses, so copying there would be
one clipboard write per mouse move. It only arms a pending flag; a
document-level mouseup listener flushes, and the touch path calls the
flush itself because it preventDefaults its touchend and no mouseup
ever arrives there.
* The flush is synchronous inside the handler, because both clipboard
paths need user activation: Firefox gates navigator.clipboard
.writeText on it, and execCommand('copy'), the fallback the plain-HTTP
LAN install lands on, has to run in the gesture's own task. A timer or
a wait for onSelectionChange loses it, invisibly in Chrome.
* It deliberately does NOT do what copyTerminalSelection() does. That
one clears the selection (so a second Ctrl+C is an interrupt) and
focuses the terminal. Clearing would make text vanish under the cursor
that just highlighted it, and focusing opens the on-screen keyboard
over it on a phone. Focus is instead restored to whatever held it,
which only matters for the execCommand fallback.
Guards are pure in decideAutoCopy() (constants.js): off, blank or
whitespace-only text, and a 1M-char cap, since a drag off the top of the
viewport autoscrolls and one gesture can sweep the whole 50k-line
scrollback. Past the cap the copy is refused rather than truncated, with
a toast pointing at Ctrl+C.
Feedback is silent on success except once per page load, so a feature
that works by doing nothing visible can still be told from a dead
toggle; failures and refusals toast, throttled to 10s.
Per-device on both counts the settings rule requires: in `displayKeys`
and absent from the .strict() SettingsUpdateSchema, because clipboard
access differs by device and by origin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -562,6 +562,55 @@ function resolveTerminalFontFamily(custom) {
|
||||
return `${families.join(', ')}, ${TERMINAL_FONT_DEFAULT_STACK}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auto Copy (copy-on-select). Pure decision, so every guard below is testable
|
||||
// without a terminal, a clipboard, or a browser.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Upper bound on an AUTO-copied selection.
|
||||
*
|
||||
* A drag that runs off the top of the viewport autoscrolls, so one gesture can
|
||||
* sweep the entire 50k-line scrollback (millions of characters), and writing
|
||||
* that to the clipboard on every mouseup is a real hazard on a phone. Past the
|
||||
* cap the copy is REFUSED rather than truncated (half a selection on the
|
||||
* clipboard is worse than none) and the user is told to press Ctrl+C, which
|
||||
* still copies the whole thing through the explicit path.
|
||||
*/
|
||||
const AUTO_COPY_MAX_CHARS = 1_000_000;
|
||||
|
||||
/**
|
||||
* What an auto-copy attempt should do at the end of a selection gesture.
|
||||
*
|
||||
* `pending` is set by xterm's onSelectionChange and cleared on every flush;
|
||||
* `lastCopied` is the text this surface auto-copied last. Either one alone is
|
||||
* wrong, which is why both are here:
|
||||
*
|
||||
* - onSelectionChange does not reliably fire BEFORE the mouseup that ends the
|
||||
* drag (xterm fires it from its own document-level mouseup handler, and
|
||||
* listener order between the two is registration order, not something this
|
||||
* code controls). Gating on `pending` alone would silently drop the first
|
||||
* copy of a drag-selection.
|
||||
* - Gating on `text !== lastCopied` alone drops a deliberate re-selection of
|
||||
* the same text after the user copied something else in between, and it
|
||||
* would let any unrelated mouseup on the page re-copy a stale selection.
|
||||
*
|
||||
* So: a genuine selection change (`pending`) always copies, and otherwise only
|
||||
* text that differs from the last auto-copy does.
|
||||
*
|
||||
* @param {{enabled?: boolean, text?: string, lastCopied?: string, pending?: boolean}} params
|
||||
* @returns {'copy'|'skip'|'too-large'}
|
||||
*/
|
||||
function decideAutoCopy({ enabled, text, lastCopied, pending } = {}) {
|
||||
if (!enabled) return 'skip';
|
||||
// Whitespace-only is what a drag across blank cells produces; putting a wall
|
||||
// of spaces on the clipboard is never what the gesture meant.
|
||||
if (typeof text !== 'string' || !text.trim()) return 'skip';
|
||||
if (!pending && text === lastCopied) return 'skip';
|
||||
if (text.length > AUTO_COPY_MAX_CHARS) return 'too-large';
|
||||
return 'copy';
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.WEBGL_FALLBACK = WEBGL_FALLBACK;
|
||||
window.evaluateWebGLLongTaskTrip = evaluateWebGLLongTaskTrip;
|
||||
@@ -595,6 +644,10 @@ if (typeof window !== 'undefined') {
|
||||
compare: compareSessionActivity,
|
||||
sort: sortSessionsByActivity,
|
||||
};
|
||||
window.CodemanAutoCopy = {
|
||||
decide: decideAutoCopy,
|
||||
MAX_CHARS: AUTO_COPY_MAX_CHARS,
|
||||
};
|
||||
window.CodemanTerminalFont = {
|
||||
DEFAULT_STACK: TERMINAL_FONT_DEFAULT_STACK,
|
||||
resolve: resolveTerminalFontFamily,
|
||||
|
||||
Reference in New Issue
Block a user