feat: add copy on ctrl/command+shift+c and selection to web terminal (#20129)

Closes https://github.com/coder/coder/issues/20044
This commit is contained in:
Asher
2025-10-17 14:18:50 -08:00
committed by GitHub
parent 141ef23c81
commit cbaa97cb78
+44 -4
View File
@@ -14,6 +14,7 @@ import {
} from "api/queries/workspaces";
import { useProxy } from "contexts/ProxyContext";
import { ThemeOverride } from "contexts/ThemeProvider";
import { useClipboard } from "hooks/useClipboard";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { type FC, useCallback, useEffect, useRef, useState } from "react";
import { useQuery } from "react-query";
@@ -80,6 +81,8 @@ const TerminalPage: FC = () => {
const config = useQuery(deploymentConfig());
const renderer = config.data?.config.web_terminal_renderer;
const { copyToClipboard } = useClipboard();
// Periodically report workspace usage.
useQuery(
workspaceUsage({
@@ -147,12 +150,21 @@ const TerminalPage: FC = () => {
}),
);
// Make shift+enter send ^[^M (escaped carriage return). Applications
// typically take this to mean to insert a literal newline. There is no way
// to remove this handler, so we must attach it once and rely on a ref to
// send it to the current socket.
const isMac = navigator.platform.match("Mac");
const copySelection = () => {
const selection = terminal.getSelection();
if (selection) {
copyToClipboard(selection);
}
};
// There is no way to remove this handler, so we must attach it once and
// rely on a ref to send it to the current socket.
const escapedCarriageReturn = "\x1b\r";
terminal.attachCustomKeyEventHandler((ev) => {
// Make shift+enter send ^[^M (escaped carriage return). Applications
// typically take this to mean to insert a literal newline.
if (ev.shiftKey && ev.key === "Enter") {
if (ev.type === "keydown") {
websocketRef.current?.send(
@@ -163,9 +175,36 @@ const TerminalPage: FC = () => {
}
return false;
}
// Make ctrl+shift+c (command+shift+c on macOS) copy the selected text.
// By default this usually launches the browser dev tools, but users
// expect this keybinding to copy when in the context of the web terminal.
if ((isMac ? ev.metaKey : ev.ctrlKey) && ev.shiftKey && ev.key === "C") {
ev.preventDefault();
if (ev.type === "keydown") {
copySelection();
}
return false;
}
return true;
});
// Copy using the clipboard API on selection. This selected text will go
// into the clipboard, not the primary selection, as the browser does not
// give us an API to set the primary selection (only relevant to systems
// that have this distinction, like X11).
//
// We could bind the middle mouse button to paste from the clipboard to
// compensate, but then we would break pasting selections from external
// applications into the web terminal. Not sure which tradeoff is worse; it
// probably varies between users.
//
// In other words, this copied text can be pasted with a keybinding
// (typically ctrl+v, ctrl+shift+v, or shift+insert), but *not* with the
// middle mouse button.
terminal.onSelectionChange(() => {
copySelection();
});
terminal.open(terminalWrapperRef.current);
// We have to fit twice here. It's unknown why, but the first fit will
@@ -189,6 +228,7 @@ const TerminalPage: FC = () => {
renderer,
theme.palette.background.default,
currentTerminalFont,
copyToClipboard,
]);
// Updates the reconnection token into the URL if necessary.