From c07fd5d570f15154df695759fc56074f56f08ed1 Mon Sep 17 00:00:00 2001 From: Craig Cheney Date: Fri, 18 Sep 2026 15:06:48 -0400 Subject: [PATCH] Fix pasted terminal input --- js/terminal.js | 13 ++++--- tests/terminal.test.js | 77 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/js/terminal.js b/js/terminal.js index f8014ba1..40a3146a 100644 --- a/js/terminal.js +++ b/js/terminal.js @@ -205,12 +205,17 @@ function runRootTerminal(term) { term.tabOptions = []; term.tabBase = ""; - // Optimize: just write the character instead of redrawing entire line - if (e.length === 1 && e.charCodeAt(0) >= 32) { + // xterm sends pasted text as one payload, so normalize it before insert. + const input = e + .replace(/[\r\n\t]/g, " ") + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); + + if (input.length > 0) { const pos = term.pos(); const restOfLine = term.currentLine.slice(pos); - term.currentLine = term.currentLine.slice(0, pos) + e + restOfLine; - term.write(e); + term.currentLine = + term.currentLine.slice(0, pos) + input + restOfLine; + term.write(input); if (restOfLine.length > 0) { term.write(restOfLine); term.write("\x1b[D".repeat(restOfLine.length)); diff --git a/tests/terminal.test.js b/tests/terminal.test.js index 1a046e51..d08eaa9b 100644 --- a/tests/terminal.test.js +++ b/tests/terminal.test.js @@ -95,6 +95,83 @@ describe("runRootTerminal", () => { expect(term.scrollToBottom).toHaveBeenCalled(); }); + it("inserts multi-character pasted input", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm(); + + runRootTerminal(term); + term._onData("help"); + + expect(term.currentLine).toBe("help"); + expect(term.write).toHaveBeenCalledWith("help"); + }); + + it("inserts pasted input at the cursor before trailing text", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm({ + currentLine: "heo", + pos: vi.fn(() => 2), + }); + + runRootTerminal(term); + term._onData("ll"); + + expect(term.currentLine).toBe("hello"); + expect(term.write).toHaveBeenNthCalledWith(1, "ll"); + expect(term.write).toHaveBeenNthCalledWith(2, "o"); + expect(term.write).toHaveBeenNthCalledWith(3, "\x1b[D"); + }); + + it("converts pasted newlines to spaces without executing the command", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm(); + + runRootTerminal(term); + term._onData("whois\nroot"); + + expect(term.currentLine).toBe("whois root"); + expect(term.write).toHaveBeenCalledWith("whois root"); + expect(term.executeCommandLine).not.toHaveBeenCalled(); + }); + + it("ignores pasted control-only input", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm(); + + runRootTerminal(term); + term._onData("\u0000\u001f\u007f"); + + expect(term.currentLine).toBe(""); + expect(term.write).not.toHaveBeenCalled(); + expect(term.executeCommandLine).not.toHaveBeenCalled(); + }); + + it("ignores pasted input while locked", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm(); + + runRootTerminal(term); + term.locked = true; + term._onData("help"); + + expect(term.currentLine).toBe(""); + expect(term.write).not.toHaveBeenCalled(); + expect(term.scrollToBottom).not.toHaveBeenCalled(); + }); + + it("ignores pasted input while busy", () => { + const { runRootTerminal } = loadTerminalScript(); + const term = createTerm(); + + runRootTerminal(term); + term.busy = true; + term._onData("help"); + + expect(term.currentLine).toBe(""); + expect(term.write).not.toHaveBeenCalled(); + expect(term.scrollToBottom).not.toHaveBeenCalled(); + }); + it("debounces resize handling with requestAnimationFrame", () => { const rafCallbacks = []; const requestAnimationFrame = vi.fn((callback) => {