Skip to content

Commit 963f4d0

Browse files
committed
feat: support shift+enter in terminal
It acts the same alt+enter, but is more familiar to users.
1 parent 28789d7 commit 963f4d0

File tree

2 files changed

+37
-0
lines changed

2 files changed

+37
-0
lines changed

site/src/pages/TerminalPage/TerminalPage.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "jest-canvas-mock";
22
import { waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
34
import { API } from "api/api";
45
import WS from "jest-websocket-mock";
56
import { http, HttpResponse } from "msw";
@@ -148,4 +149,21 @@ describe("TerminalPage", () => {
148149
ws.send(text);
149150
await expectTerminalText(container, text);
150151
});
152+
153+
it("supports shift+enter", async () => {
154+
const ws = new WS(
155+
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
156+
);
157+
158+
const { container } = await renderTerminal();
159+
// Ideally we could use ws.connected but that seems to pause React updates.
160+
// For now, wait for the initial resize message instead.
161+
await ws.nextMessage;
162+
163+
const msg = ws.nextMessage;
164+
const terminal = container.getElementsByClassName("xterm");
165+
await userEvent.type(terminal[0], "{Shift>}{Enter}{/Shift}");
166+
const req = JSON.parse(new TextDecoder().decode((await msg) as Uint8Array));
167+
expect(req.data).toBe("\x1b\r");
168+
});
151169
});

site/src/pages/TerminalPage/TerminalPage.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,22 @@ const TerminalPage: FC = () => {
148148
}),
149149
);
150150

151+
// Make shift+enter send ^[^M (escaped carriage return). Applications
152+
// typically take this to mean to insert a literal newline. There is no way
153+
// to remove this handler, so we must attach it once and rely on a ref to
154+
// send it to the current socket.
155+
terminal.attachCustomKeyEventHandler((ev) => {
156+
if (ev.shiftKey && ev.key === "Enter") {
157+
if (ev.type === "keydown") {
158+
websocketRef.current?.send(
159+
new TextEncoder().encode(JSON.stringify({ data: "\x1b\r" })),
160+
);
161+
}
162+
return false;
163+
}
164+
return true;
165+
});
166+
151167
terminal.open(terminalWrapperRef.current);
152168

153169
// We have to fit twice here. It's unknown why, but the first fit will
@@ -190,6 +206,7 @@ const TerminalPage: FC = () => {
190206
}, [navigate, reconnectionToken, searchParams]);
191207

192208
// Hook up the terminal through a web socket.
209+
const websocketRef = useRef<Websocket>();
193210
useEffect(() => {
194211
if (!terminal) {
195212
return;
@@ -270,6 +287,7 @@ const TerminalPage: FC = () => {
270287
.withBackoff(new ExponentialBackoff(1000, 6))
271288
.build();
272289
websocket.binaryType = "arraybuffer";
290+
websocketRef.current = websocket;
273291
websocket.addEventListener(WebsocketEvent.open, () => {
274292
// Now that we are connected, allow user input.
275293
terminal.options = {
@@ -333,6 +351,7 @@ const TerminalPage: FC = () => {
333351
d.dispose();
334352
}
335353
websocket?.close(1000);
354+
websocketRef.current = undefined;
336355
};
337356
}, [
338357
command,

0 commit comments

Comments
 (0)