From 2470ea4404b184b0d31e4738358488a5ad4e85f0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 26 Aug 2026 11:59:50 +0200 Subject: [PATCH] fix(tui): let drag select text over the editor The editor consumed left mouse presses to position the cursor, which made the alternate screen route the whole gesture to it and never start its screen-level text selection. Handle the synthesized click event instead so press/drag/release stay available for selection and copy. --- packages/tui/src/components/editor.ts | 6 ++++- packages/tui/test/mouse-components.test.ts | 26 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 682cf34c5..ddba3f6eb 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -633,7 +633,11 @@ export class Editor implements Component, Focusable { return result ? { ...result, focus: true } : undefined; } - if (event.type !== "press" || event.button !== "left") return undefined; + // Leave press/drag/release unhandled so the renderer's screen-level text + // selection can run over the editor rows (drag to select, release to copy). + // The renderer synthesizes a click when press and release land on the same + // cell without movement, which is the gesture that positions the cursor. + if (event.type !== "click" || event.button !== "left") return undefined; if (event.y <= 0 || event.y > this.renderedVisibleLineCount) return { handled: true, focus: true }; const visualLines = this.buildVisualLineMap(this.lastWidth); diff --git a/packages/tui/test/mouse-components.test.ts b/packages/tui/test/mouse-components.test.ts index de93da3f6..bf7e4e7c8 100644 --- a/packages/tui/test/mouse-components.test.ts +++ b/packages/tui/test/mouse-components.test.ts @@ -150,4 +150,30 @@ describe("mouse-aware components", () => { assert.strictEqual(tui.getFocusedComponent(), editor); tui.stop(); }); + + it("selects and copies editor text on drag instead of moving the cursor", async () => { + const terminal = new VirtualTerminal(20, 6); + const copied: string[] = []; + const tui = new TuiAltScreen(terminal, undefined, undefined, { + copySelection: async (text) => { + copied.push(text); + return true; + }, + }); + const editor = new Editor(tui, editorTheme); + editor.setText("hello world"); + tui.addChild(editor); + tui.start(); + await terminal.waitForRender(); + const cursorBefore = editor.getCursor(); + + terminal.sendInput("\x1b[<0;1;2M"); + terminal.sendInput("\x1b[<32;5;2M"); + terminal.sendInput("\x1b[<0;5;2m"); + await terminal.waitForRender(); + + assert.deepStrictEqual(copied, ["hello"]); + assert.deepStrictEqual(editor.getCursor(), cursorBefore); + tui.stop(); + }); });