[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

fix: spaces and uppercase characters in multiline input (#534)

authored by

Matt Stypa and committed by
GitHub
(May 12, 2026, 2:37 PM +0100) 3dcb31a7 fe2bcd27

+45 -13
+6
.changeset/weak-clocks-switch.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + "@clack/core": patch 4 + --- 5 + 6 + Fixed spaces and uppercase characters in multiline prompt
+6 -4
packages/core/src/prompts/multi-line.ts
··· 1 1 import type { Key } from 'node:readline'; 2 2 import { styleText } from 'node:util'; 3 3 import { findTextCursor } from '../utils/cursor.js'; 4 - import { type Action, settings } from '../utils/index.js'; 5 4 import Prompt, { type PromptOptions } from './prompt.js'; 5 + 6 + type CursorAction = 'up' | 'down' | 'left' | 'right'; 7 + const cursorActions = new Set<CursorAction>(['up', 'down', 'left', 'right']); 6 8 7 9 export interface MultiLineOptions extends PromptOptions<string, MultiLinePrompt> { 8 10 placeholder?: string; ··· 41 43 this.userInput.slice(0, this.cursor) + char + this.userInput.slice(this.cursor) 42 44 ); 43 45 } 44 - #handleCursor(key?: Action) { 46 + #handleCursor(key?: CursorAction) { 45 47 const text = this.value ?? ''; 46 48 switch (key) { 47 49 case 'up': ··· 89 91 this.#showSubmit = opts.showSubmit ?? false; 90 92 91 93 this.on('key', (char, key) => { 92 - if (key?.name && settings.actions.has(key.name as Action)) { 93 - this.#handleCursor(key.name as Action); 94 + if (key?.name && cursorActions.has(key.name as CursorAction)) { 95 + this.#handleCursor(key.name as CursorAction); 94 96 return; 95 97 } 96 98 if (char === '\t' && this.#showSubmit) {
+3 -3
packages/core/src/prompts/multi-select.ts
··· 60 60 0 61 61 ); 62 62 this.cursor = this.options[cursor].disabled ? findCursor<T>(cursor, 1, this.options) : cursor; 63 - this.on('key', (char) => { 64 - if (char === 'a') { 63 + this.on('key', (_char, key) => { 64 + if (key.name === 'a') { 65 65 this.toggleAll(); 66 66 } 67 - if (char === 'i') { 67 + if (key.name === 'i') { 68 68 this.toggleInvert(); 69 69 } 70 70 });
+1 -1
packages/core/src/prompts/prompt.ts
··· 226 226 } 227 227 228 228 // Call the key event handler and emit the key event 229 - this.emit('key', char?.toLowerCase(), key); 229 + this.emit('key', char, key); 230 230 231 231 if (key?.name === 'return' && this._shouldSubmit(char, key)) { 232 232 if (this.opts.validate) {
+3 -3
packages/core/src/prompts/select-key.ts
··· 19 19 }); 20 20 this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); 21 21 22 - this.on('key', (key, keyInfo) => { 22 + this.on('key', (key) => { 23 23 if (!key) { 24 24 return; 25 25 } 26 - const casedKey = caseSensitive && keyInfo.shift ? key.toUpperCase() : key; 26 + const casedKey = caseSensitive ? key : key.toLowerCase(); 27 27 if (!keys.includes(casedKey)) { 28 28 return; 29 29 } 30 30 31 31 const value = this.options.find(({ value: [initial] }) => { 32 - return caseSensitive ? initial === casedKey : initial?.toLowerCase() === key; 32 + return caseSensitive ? initial === casedKey : initial?.toLowerCase() === casedKey; 33 33 }); 34 34 if (value) { 35 35 this.value = value.value;
+24
packages/core/test/prompts/multi-line.test.ts
··· 175 175 expect(result).to.equal('xy'); 176 176 }); 177 177 178 + test('space inserts space', () => { 179 + const instance = new MultiLinePrompt({ 180 + input, 181 + output, 182 + render: () => 'foo', 183 + }); 184 + instance.prompt(); 185 + input.emit('keypress', 'x', { name: 'x' }); 186 + input.emit('keypress', ' ', { name: 'space' }); 187 + expect(instance.userInput).to.equal('x '); 188 + }); 189 + 190 + test('shift modifier inserts uppercase characters', () => { 191 + const instance = new MultiLinePrompt({ 192 + input, 193 + output, 194 + render: () => 'foo', 195 + }); 196 + instance.prompt(); 197 + input.emit('keypress', 'x', { name: 'x' }); 198 + input.emit('keypress', 'X', { name: 'x', shift: true }); 199 + expect(instance.userInput).to.equal('xX'); 200 + }); 201 + 178 202 test('backspace deletes previous char', async () => { 179 203 const instance = new MultiLinePrompt({ 180 204 input,
+2 -2
packages/prompts/test/select-key.test.ts
··· 114 114 output, 115 115 }); 116 116 117 - input.emit('keypress', 'a', { name: 'a', shift: true }); 117 + input.emit('keypress', 'A', { name: 'a', shift: true }); 118 118 119 119 const value = await result; 120 120 ··· 156 156 output, 157 157 }); 158 158 159 - input.emit('keypress', 'a', { name: 'a', shift: true }); 159 + input.emit('keypress', 'A', { name: 'a', shift: true }); 160 160 161 161 const value = await result; 162 162