From 963f4d05ab2bd504141179eb9dd99b72d04257a1 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 23 Jul 2025 15:39:26 -0800 Subject: [PATCH 1/3] feat: support shift+enter in terminal It acts the same alt+enter, but is more familiar to users. --- .../pages/TerminalPage/TerminalPage.test.tsx | 18 ++++++++++++++++++ site/src/pages/TerminalPage/TerminalPage.tsx | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) 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..6b9bfb4a695d0 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -148,6 +148,22 @@ 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. + terminal.attachCustomKeyEventHandler((ev) => { + if (ev.shiftKey && ev.key === "Enter") { + if (ev.type === "keydown") { + websocketRef.current?.send( + new TextEncoder().encode(JSON.stringify({ data: "\x1b\r" })), + ); + } + 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 +206,7 @@ const TerminalPage: FC = () => { }, [navigate, reconnectionToken, searchParams]); // Hook up the terminal through a web socket. + const websocketRef = useRef(); useEffect(() => { if (!terminal) { return; @@ -270,6 +287,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 +351,7 @@ const TerminalPage: FC = () => { d.dispose(); } websocket?.close(1000); + websocketRef.current = undefined; }; }, [ command, From 77d94ae103802d024dc4f6992888709ec93c3d95 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 24 Jul 2025 11:41:05 -0800 Subject: [PATCH 2/3] Name the escaped carriage return --- site/src/pages/TerminalPage/TerminalPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 6b9bfb4a695d0..14fe872be1e05 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -152,11 +152,12 @@ const TerminalPage: FC = () => { // 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: "\x1b\r" })), + new TextEncoder().encode(JSON.stringify({ data: escapedCarriageReturn })), ); } return false; From 33ac7dc91f61181bc2e528c739ed5ed5ce9323ca Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 29 Jul 2025 09:17:43 -0800 Subject: [PATCH 3/3] fmt more like fml --- site/src/pages/TerminalPage/TerminalPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 14fe872be1e05..bde7517ef5d19 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -157,7 +157,9 @@ const TerminalPage: FC = () => { if (ev.shiftKey && ev.key === "Enter") { if (ev.type === "keydown") { websocketRef.current?.send( - new TextEncoder().encode(JSON.stringify({ data: escapedCarriageReturn })), + new TextEncoder().encode( + JSON.stringify({ data: escapedCarriageReturn }), + ), ); } return false; 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