From 004b400c6d2254f9b5ad637cdc71c5d860640687 Mon Sep 17 00:00:00 2001 From: kuldeepsinh19 Date: Wed, 11 Nov 2026 09:46:01 +0530 Subject: [PATCH 1/3] fix(core): keep control characters out of multi-line input The key handler inserted every keypress that carries a character. Named keys are reported with their raw sequence, so backspace at the start of the input inserted DEL and ctrl+key combinations inserted their control character. Terminals do not draw them, but they were returned in the value. --- packages/core/src/prompts/multi-line.ts | 8 +++- packages/core/test/prompts/multi-line.test.ts | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/core/src/prompts/multi-line.ts b/packages/core/src/prompts/multi-line.ts index f6b6139c..dc3d8f5d 100644 --- a/packages/core/src/prompts/multi-line.ts +++ b/packages/core/src/prompts/multi-line.ts @@ -6,6 +6,12 @@ import Prompt, { type PromptOptions } from './prompt.js'; type CursorAction = 'up' | 'down' | 'left' | 'right'; const cursorActions = new Set(['up', 'down', 'left', 'right']); +// readline reports keys like backspace or ctrl+a as a control character (`\x7f`, `\x01`) +function isControlCharacter(char: string): boolean { + const code = char.charCodeAt(0); + return code === 0x7f || (code < 0x20 && char !== '\t' && char !== '\n'); +} + export interface MultiLineOptions extends PromptOptions { placeholder?: string; defaultValue?: string; @@ -130,7 +136,7 @@ export default class MultiLinePrompt extends Prompt { ); return; } - if (char) { + if (char && !isControlCharacter(char)) { if (this.#showSubmit && this.focused === 'submit') { this.focused = 'editor'; } diff --git a/packages/core/test/prompts/multi-line.test.ts b/packages/core/test/prompts/multi-line.test.ts index c04e6183..7cea5229 100644 --- a/packages/core/test/prompts/multi-line.test.ts +++ b/packages/core/test/prompts/multi-line.test.ts @@ -309,6 +309,43 @@ describe('MultiLinePrompt', () => { await resultPromise; }); + test('backspace at start does not insert its control character', async () => { + const instance = new MultiLinePrompt({ + input, + output, + render: () => 'foo', + }); + const resultPromise = instance.prompt(); + // a terminal sends DEL (or BS) as the character of the backspace key + input.emit('keypress', '\x7f', { name: 'backspace', sequence: '\x7f' }); + expect(instance.userInput).to.equal(''); + input.emit('keypress', '\b', { name: 'backspace', sequence: '\b' }); + expect(instance.userInput).to.equal(''); + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'return' }); + input.emit('keypress', '', { name: 'return' }); + const result = await resultPromise; + expect(result).to.equal('x'); + }); + + test('ctrl key combinations do not insert control characters', async () => { + const instance = new MultiLinePrompt({ + input, + output, + render: () => 'foo', + }); + const resultPromise = instance.prompt(); + input.emit('keypress', 'x', { name: 'x' }); + // ctrl+a and ctrl+u arrive as the control characters SOH and NAK + input.emit('keypress', '\x01', { name: 'a', sequence: '\x01', ctrl: true }); + input.emit('keypress', '\x15', { name: 'u', sequence: '\x15', ctrl: true }); + expect(instance.userInput).to.equal('x'); + input.emit('keypress', '', { name: 'return' }); + input.emit('keypress', '', { name: 'return' }); + const result = await resultPromise; + expect(result).to.equal('x'); + }); + test('left moves left until start', async () => { const instance = new MultiLinePrompt({ input, From 6af62d210dc9d5986d6d41ab7ffdeff3edcab592 Mon Sep 17 00:00:00 2001 From: kuldeepsinh19 Date: Wed, 11 Nov 2026 09:46:14 +0530 Subject: [PATCH 2/3] test(core): cover tab and newline insertion in multi-line input Tab and newline are control characters as well, but they are whitespace a multi-line value can contain, so they have to keep being inserted. --- packages/core/test/prompts/multi-line.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/core/test/prompts/multi-line.test.ts b/packages/core/test/prompts/multi-line.test.ts index 7cea5229..cf220438 100644 --- a/packages/core/test/prompts/multi-line.test.ts +++ b/packages/core/test/prompts/multi-line.test.ts @@ -346,6 +346,22 @@ describe('MultiLinePrompt', () => { expect(result).to.equal('x'); }); + test('tab and newline characters are still inserted', async () => { + const instance = new MultiLinePrompt({ + input, + output, + render: () => 'foo', + }); + const resultPromise = instance.prompt(); + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '\t', { name: 'tab', sequence: '\t' }); + input.emit('keypress', '\n', { name: 'enter', sequence: '\n' }); + input.emit('keypress', 'y', { name: 'y' }); + expect(instance.userInput).to.equal('x\t\ny'); + input.emit('keypress', '', { name: 'escape' }); + await resultPromise; + }); + test('left moves left until start', async () => { const instance = new MultiLinePrompt({ input, From 80ee1d0a2ecee7feb2d46ad1db9baff6cfe1c3da Mon Sep 17 00:00:00 2001 From: kuldeepsinh19 Date: Wed, 11 Nov 2026 09:46:14 +0530 Subject: [PATCH 3/3] chore: add changeset --- .changeset/calm-keys-listen.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-keys-listen.md diff --git a/.changeset/calm-keys-listen.md b/.changeset/calm-keys-listen.md new file mode 100644 index 00000000..11b6a6a7 --- /dev/null +++ b/.changeset/calm-keys-listen.md @@ -0,0 +1,5 @@ +--- +"@clack/core": patch +--- + +Stop the multi-line prompt from inserting control characters into its value, such as the DEL sent by backspace at the start of the input or the characters sent by ctrl+key combinations.