From 97cf6789258d9a771723669a458014a5eb26e971 Mon Sep 17 00:00:00 2001 From: Kai Natori Date: Mon, 21 Sep 2026 16:39:01 +0800 Subject: [PATCH 1/2] fix: treat IME-committed ideographic space as space in multi-select Japanese, Chinese and other CJK IMEs can commit an ideographic space (U+3000) instead of U+0020 when the space key is pressed. Node readline reports it with no key name, so prompts never saw a space key and toggling multi-select options silently did nothing. Handle the unnamed ideographic space keypress in MultiSelectPrompt and GroupMultiSelectPrompt directly, so the raw character stays observable for consumers that want to detect U+3000 itself. --- .changeset/olive-donuts-switch.md | 5 +++++ packages/core/src/prompts/group-multiselect.ts | 9 +++++++++ packages/core/src/prompts/multi-select.ts | 8 +++++++- .../test/prompts/group-multiselect.test.ts | 18 ++++++++++++++++++ .../core/test/prompts/multi-select.test.ts | 15 +++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 .changeset/olive-donuts-switch.md diff --git a/.changeset/olive-donuts-switch.md b/.changeset/olive-donuts-switch.md new file mode 100644 index 00000000..b11b2923 --- /dev/null +++ b/.changeset/olive-donuts-switch.md @@ -0,0 +1,5 @@ +--- +"@clack/core": patch +--- + +Treat an ideographic space (U+3000) committed by IMEs (e.g. Japanese or Chinese input) as space when toggling options in multi-select prompts. diff --git a/packages/core/src/prompts/group-multiselect.ts b/packages/core/src/prompts/group-multiselect.ts index e5496728..75f2a890 100644 --- a/packages/core/src/prompts/group-multiselect.ts +++ b/packages/core/src/prompts/group-multiselect.ts @@ -67,6 +67,15 @@ export default class GroupMultiSelectPrompt extends Pr this.#selectableGroups ? 0 : 1 ); + this.on('key', (char, key) => { + // Some IMEs (e.g. Japanese or Chinese input) commit an ideographic space (U+3000) + // with no key name when the space key is pressed. Treat it as space so + // toggling options still works. + if (key.name === undefined && char === ' ') { + this.toggleValue(); + } + }); + this.on('cursor', (key) => { switch (key) { case 'left': diff --git a/packages/core/src/prompts/multi-select.ts b/packages/core/src/prompts/multi-select.ts index 88d76a8e..dd590518 100644 --- a/packages/core/src/prompts/multi-select.ts +++ b/packages/core/src/prompts/multi-select.ts @@ -60,13 +60,19 @@ export default class MultiSelectPrompt extends Prompt(cursor, 1, this.options) : cursor; - this.on('key', (_char, key) => { + this.on('key', (char, key) => { if (key.name === 'a') { this.toggleAll(); } if (key.name === 'i') { this.toggleInvert(); } + // Some IMEs (e.g. Japanese or Chinese input) commit an ideographic space (U+3000) + // with no key name when the space key is pressed. Treat it as space so + // toggling options still works. + if (key.name === undefined && char === ' ') { + this.toggleValue(); + } }); this.on('cursor', (key) => { diff --git a/packages/core/test/prompts/group-multiselect.test.ts b/packages/core/test/prompts/group-multiselect.test.ts index fa370f7f..fc71642e 100644 --- a/packages/core/test/prompts/group-multiselect.test.ts +++ b/packages/core/test/prompts/group-multiselect.test.ts @@ -30,6 +30,24 @@ describe('GroupMultiSelectPrompt', () => { expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); }); + test('toggles option with ideographic space (U+3000) committed by IME', () => { + const instance = new GroupMultiSelectPrompt({ + input, + output, + render: () => 'foo', + options: { + group: [{ value: 'foo' }, { value: 'bar' }], + }, + selectableGroups: false, + }); + instance.prompt(); + + input.emit('keypress', ' ', { sequence: ' ' }); + expect(instance.value).toEqual(['foo']); + input.emit('keypress', ' ', { sequence: ' ' }); + expect(instance.value).toEqual([]); + }); + test('does not throw if empty options are provided', () => { const instance = new GroupMultiSelectPrompt({ input, diff --git a/packages/core/test/prompts/multi-select.test.ts b/packages/core/test/prompts/multi-select.test.ts index e8af630d..56e50aad 100644 --- a/packages/core/test/prompts/multi-select.test.ts +++ b/packages/core/test/prompts/multi-select.test.ts @@ -143,6 +143,21 @@ describe('MultiSelectPrompt', () => { expect(instance.value).toEqual(['foo']); }); + test('toggles option with ideographic space (U+3000) committed by IME', () => { + const instance = new MultiSelectPrompt({ + input, + output, + render: () => 'foo', + options: [{ value: 'foo' }, { value: 'bar' }], + }); + instance.prompt(); + + input.emit('keypress', ' ', { sequence: ' ' }); + expect(instance.value).toEqual(['foo']); + input.emit('keypress', ' ', { sequence: ' ' }); + expect(instance.value).toEqual([]); + }); + test('disabled options are skipped', () => { const instance = new MultiSelectPrompt({ input, From e65846a547edc28be28f6c9d762c046a01bb08d3 Mon Sep 17 00:00:00 2001 From: kai-natori <129699611+kai-natori@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:23:28 +0800 Subject: [PATCH 2/2] style: apply review suggestions for ideographic space handling Co-authored-by: James Garbutt <43081j@users.noreply.github.com> --- packages/core/src/prompts/group-multiselect.ts | 4 +--- packages/core/test/prompts/group-multiselect.test.ts | 4 ++-- packages/core/test/prompts/multi-select.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/prompts/group-multiselect.ts b/packages/core/src/prompts/group-multiselect.ts index 75f2a890..f68e0fe5 100644 --- a/packages/core/src/prompts/group-multiselect.ts +++ b/packages/core/src/prompts/group-multiselect.ts @@ -68,9 +68,7 @@ export default class GroupMultiSelectPrompt extends Pr ); this.on('key', (char, key) => { - // Some IMEs (e.g. Japanese or Chinese input) commit an ideographic space (U+3000) - // with no key name when the space key is pressed. Treat it as space so - // toggling options still works. + // Treat ideographic spaces as if they were normal spaces. if (key.name === undefined && char === ' ') { this.toggleValue(); } diff --git a/packages/core/test/prompts/group-multiselect.test.ts b/packages/core/test/prompts/group-multiselect.test.ts index fc71642e..755bbf0c 100644 --- a/packages/core/test/prompts/group-multiselect.test.ts +++ b/packages/core/test/prompts/group-multiselect.test.ts @@ -42,9 +42,9 @@ describe('GroupMultiSelectPrompt', () => { }); instance.prompt(); - input.emit('keypress', ' ', { sequence: ' ' }); + input.emit('keypress', ' ', { sequence: '\u3000' }); expect(instance.value).toEqual(['foo']); - input.emit('keypress', ' ', { sequence: ' ' }); + input.emit('keypress', ' ', { sequence: '\u3000' }); expect(instance.value).toEqual([]); }); diff --git a/packages/core/test/prompts/multi-select.test.ts b/packages/core/test/prompts/multi-select.test.ts index 56e50aad..edef73e4 100644 --- a/packages/core/test/prompts/multi-select.test.ts +++ b/packages/core/test/prompts/multi-select.test.ts @@ -152,9 +152,9 @@ describe('MultiSelectPrompt', () => { }); instance.prompt(); - input.emit('keypress', ' ', { sequence: ' ' }); + input.emit('keypress', ' ', { sequence: '\u3000' }); expect(instance.value).toEqual(['foo']); - input.emit('keypress', ' ', { sequence: ' ' }); + input.emit('keypress', ' ', { sequence: '\u3000' }); expect(instance.value).toEqual([]); });