@@ -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,26 @@ 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 ( "Failed to copy text, but no error message was provided" ) ;
160
+ }
161
+ } )
162
+ }
163
+ } ;
164
+
165
+ // There is no way to remove this handler, so we must attach it once and
166
+ // rely on a ref to send it to the current socket.
154
167
const escapedCarriageReturn = "\x1b\r" ;
155
168
terminal . attachCustomKeyEventHandler ( ( ev ) => {
169
+ // Make shift+enter send ^[^M (escaped carriage return). Applications
170
+ // typically take this to mean to insert a literal newline.
156
171
if ( ev . shiftKey && ev . key === "Enter" ) {
157
172
if ( ev . type === "keydown" ) {
158
173
websocketRef . current ?. send (
@@ -163,9 +178,36 @@ const TerminalPage: FC = () => {
163
178
}
164
179
return false ;
165
180
}
181
+ // Make ctrl+shift+c (command+shift+c on macOS) copy the selected text.
182
+ // By default this usually launches the browser dev tools, but users
183
+ // expect this keybinding to copy when in the context of the web terminal.
184
+ if ( ( navigator . platform . match ( "Mac" ) ?ev . metaKey :ev . ctrlKey ) && ev . shiftKey && ev . key === "C" ) {
185
+ ev . preventDefault ( )
186
+ if ( ev . type === "keydown" ) {
187
+ copySelection ( ) ;
188
+ }
189
+ return false ;
190
+ }
166
191
return true ;
167
192
} ) ;
168
193
194
+ // Copy using the clipboard API on selection. This selected text will go
195
+ // into the clipboard, not the primary selection, as the browser does not
196
+ // give us an API to set the primary selection (only relevant to systems
197
+ // that have this distinction, like X11).
198
+ //
199
+ // We could bind the middle mouse button to paste from the clipboard to
200
+ // compensate, but then we would break pasting selections from external
201
+ // applications into the web terminal. Not sure which tradeoff is worse; it
202
+ // probably varies between users.
203
+ //
204
+ // In other words, this copied text can be pasted with a keybinding
205
+ // (typically ctrl+v, ctrl+shift+v, or shift+insert), but *not* with the
206
+ // middle mouse button.
207
+ terminal . onSelectionChange ( ( ) => {
208
+ copySelection ( ) ;
209
+ } ) ;
210
+
169
211
terminal . open ( terminalWrapperRef . current ) ;
170
212
171
213
// We have to fit twice here. It's unknown why, but the first fit will