From d06db09a53958e485131527db25c1293f29b9367 Mon Sep 17 00:00:00 2001 From: Georgi <46296586+Exrun94@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:30:49 +0300 Subject: [PATCH] fix(tui): decode CSI-u Ctrl+letter inside bracketed paste, fixes #3599 (#3623) Co-authored-by: Georgi Chochev --- packages/tui/src/components/editor.ts | 14 +++++++++++++- packages/tui/test/editor.test.ts | 10 ++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index a0337d60..990469e5 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1088,8 +1088,20 @@ export class Editor implements Component, Focusable { this.pushUndoSnapshot(); + // Some terminals (e.g. tmux popups with extended-keys-format=csi-u) re-encode + // control bytes inside bracketed paste as CSI-u Ctrl+ sequences + // (ESC [ ; 5 u). Decode those back to their literal byte so the + // per-char filter below preserves newlines instead of stripping ESC and + // leaking the printable tail (e.g. "[106;5u") into the editor. + const decodedText = pastedText.replace(/\x1b\[(\d+);5u/g, (match, code) => { + const cp = Number(code); + if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96); + if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64); + return match; + }); + // Clean the pasted text: normalize line endings, expand tabs - const cleanText = this.normalizeText(pastedText); + const cleanText = this.normalizeText(decodedText); // Filter out non-printable characters except newlines let filteredText = cleanText diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index c3805129..99dd1cc7 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -1677,6 +1677,16 @@ describe("Editor component", () => { assert.strictEqual(editor.isShowingAutocomplete(), false); }); + it("decodes CSI-u Ctrl+letter sequences inside bracketed paste (tmux popup)", () => { + const editor = new Editor(createTestTUI(), defaultEditorTheme); + + // tmux popups with extended-keys-format=csi-u re-encode \n in pastes as + // \x1b[106;5u (Ctrl+J). Without decoding, the per-char filter strips ESC + // and leaks "[106;5u" between lines. See issue #3599. + editor.handleInput("\x1b[200~line1\x1b[106;5uline2\x1b[106;5uline3\x1b[201~"); + assert.strictEqual(editor.getText(), "line1\nline2\nline3"); + }); + it("undoes multi-line paste atomically", () => { const editor = new Editor(createTestTUI(), defaultEditorTheme);