diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx index 4591190ad9904..7530a45914a85 100644 --- a/site/src/pages/TerminalPage/TerminalPage.test.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx @@ -1,5 +1,6 @@ import "jest-canvas-mock"; import { waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { API } from "api/api"; import WS from "jest-websocket-mock"; import { http, HttpResponse } from "msw"; @@ -148,4 +149,21 @@ describe("TerminalPage", () => { ws.send(text); await expectTerminalText(container, text); }); + + it("supports shift+enter", async () => { + const ws = new WS( + `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, + ); + + const { container } = await renderTerminal(); + // Ideally we could use ws.connected but that seems to pause React updates. + // For now, wait for the initial resize message instead. + await ws.nextMessage; + + const msg = ws.nextMessage; + const terminal = container.getElementsByClassName("xterm"); + await userEvent.type(terminal[0], "{Shift>}{Enter}{/Shift}"); + const req = JSON.parse(new TextDecoder().decode((await msg) as Uint8Array)); + expect(req.data).toBe("\x1b\r"); + }); }); diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 5c13e89c30005..bde7517ef5d19 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -148,6 +148,25 @@ 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 escapedCarriageReturn = "\x1b\r"; + terminal.attachCustomKeyEventHandler((ev) => { + if (ev.shiftKey && ev.key === "Enter") { + if (ev.type === "keydown") { + websocketRef.current?.send( + new TextEncoder().encode( + JSON.stringify({ data: escapedCarriageReturn }), + ), + ); + } + return false; + } + return true; + }); + terminal.open(terminalWrapperRef.current); // We have to fit twice here. It's unknown why, but the first fit will @@ -190,6 +209,7 @@ const TerminalPage: FC = () => { }, [navigate, reconnectionToken, searchParams]); // Hook up the terminal through a web socket. + const websocketRef = useRef(); useEffect(() => { if (!terminal) { return; @@ -270,6 +290,7 @@ const TerminalPage: FC = () => { .withBackoff(new ExponentialBackoff(1000, 6)) .build(); websocket.binaryType = "arraybuffer"; + websocketRef.current = websocket; websocket.addEventListener(WebsocketEvent.open, () => { // Now that we are connected, allow user input. terminal.options = { @@ -333,6 +354,7 @@ const TerminalPage: FC = () => { d.dispose(); } websocket?.close(1000); + websocketRef.current = undefined; }; }, [ command, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy