mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user