@@ -12,6 +12,7 @@ import {
12
12
workspaceByOwnerAndName ,
13
13
workspaceUsage ,
14
14
} from "api/queries/workspaces" ;
15
+ import { displayError } from "components/GlobalSnackbar/utils" ;
15
16
import { useProxy } from "contexts/ProxyContext" ;
16
17
import { ThemeOverride } from "contexts/ThemeProvider" ;
17
18
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata" ;
@@ -147,12 +148,28 @@ const TerminalPage: FC = () => {
147
148
} ) ,
148
149
) ;
149
150
150
- // Make shift+enter send ^[^M (escaped carriage return). Applications
151
- // typically take this to mean to insert a literal newline. There is no way
152
- // to remove this handler, so we must attach it once and rely on a ref to
153
- // send it to the current socket.
151
+ const copySelection = ( ) => {
152
+ const selection = terminal . getSelection ( ) ;
153
+ if ( selection ) {
154
+ navigator . clipboard . writeText ( selection ) . catch ( ( err ) => {
155
+ console . error ( err ) ;
156
+ if ( err . message ) {
157
+ displayError ( `Failed to copy text:${ err . message } ` ) ;
158
+ } else {
159
+ displayError (
160
+ "Failed to copy text, but no error message was provided" ,
161
+ ) ;
162
+ }
163
+ } ) ;
164
+ }
165
+ } ;
166
+
167
+ // There is no way to remove this handler, so we must attach it once and
168
+ // rely on a ref to send it to the current socket.
154
169
const escapedCarriageReturn = "\x1b\r" ;
155
170
terminal . attachCustomKeyEventHandler ( ( ev ) => {
171
+ // Make shift+enter send ^[^M (escaped carriage return). Applications
172
+ // typically take this to mean to insert a literal newline.
156
173
if ( ev . shiftKey && ev . key === "Enter" ) {
157
174
if ( ev . type === "keydown" ) {
158
175
websocketRef . current ?. send (
@@ -163,9 +180,40 @@ const TerminalPage: FC = () => {
163
180
}
164
181
return false ;
165
182
}
183
+ // Make ctrl+shift+c (command+shift+c on macOS) copy the selected text.
184
+ // By default this usually launches the browser dev tools, but users
185
+ // expect this keybinding to copy when in the context of the web terminal.
186
+ if (
187
+ ( navigator . platform . match ( "Mac" ) ?ev . metaKey :ev . ctrlKey ) &&
188
+ ev . shiftKey &&
189
+ ev . key === "C"
190
+ ) {
191
+ ev . preventDefault ( ) ;
192
+ if ( ev . type === "keydown" ) {
193
+ copySelection ( ) ;
194
+ }
195
+ return false ;
196
+ }
166
197
return true ;
167
198
} ) ;
168
199
200
+ // Copy using the clipboard API on selection. This selected text will go
201
+ // into the clipboard, not the primary selection, as the browser does not
202
+ // give us an API to set the primary selection (only relevant to systems
203
+ // that have this distinction, like X11).
204
+ //
205
+ // We could bind the middle mouse button to paste from the clipboard to
206
+ // compensate, but then we would break pasting selections from external
207
+ // applications into the web terminal. Not sure which tradeoff is worse; it
208
+ // probably varies between users.
209
+ //
210
+ // In other words, this copied text can be pasted with a keybinding
211
+ // (typically ctrl+v, ctrl+shift+v, or shift+insert), but *not* with the
212
+ // middle mouse button.
213
+ terminal . onSelectionChange ( ( ) => {
214
+ copySelection ( ) ;
215
+ } ) ;
216
+
169
217
terminal . open ( terminalWrapperRef . current ) ;
170
218
171
219
// We have to fit twice here. It's unknown why, but the first fit will