From 826dbb341a24e5788033bf299e989d1366e64a2b Mon Sep 17 00:00:00 2001 From: showms Date: Thu, 4 Jun 2026 22:12:24 +0800 Subject: [PATCH] fix(ui): preserve Windows input after welcome screen --- src/ui/welcome-screen.ts | 49 ++++++++++++----------------- test/ui/welcome-screen.test.ts | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 test/ui/welcome-screen.test.ts diff --git a/src/ui/welcome-screen.ts b/src/ui/welcome-screen.ts index 5ed26b6a1..4d4c7e799 100644 --- a/src/ui/welcome-screen.ts +++ b/src/ui/welcome-screen.ts @@ -77,41 +77,30 @@ function canAnimate(): boolean { /** * Wait for Enter key press */ -function waitForEnter(): Promise { - return new Promise((resolve) => { - const { stdin } = process; - - // Handle non-TTY gracefully - if (!stdin.isTTY) { - resolve(); - return; - } - - const wasRaw = stdin.isRaw; - stdin.setRawMode(true); - stdin.resume(); - - const onData = (data: Buffer): void => { - const char = data.toString(); - - // Enter key or Ctrl+C - if (char === '\r' || char === '\n' || char === '\u0003') { - stdin.removeListener('data', onData); - stdin.setRawMode(wasRaw); - stdin.pause(); +async function waitForEnter(): Promise { + if (!process.stdin.isTTY) { + return; + } - // Handle Ctrl+C - if (char === '\u0003') { - process.stdout.write('\n'); - process.exit(0); - } + // Keep all interactive input on Inquirer's keypress lifecycle. Mixing a raw + // `data` listener between Inquirer prompts breaks arrow/space keys on Windows. + const { createPrompt, isEnterKey, useKeypress } = await import('@inquirer/core'); + const prompt = createPrompt>((_config, done) => { + useKeypress((key) => { + if (key.ctrl && key.name === 'c') { + process.stdout.write('\n'); + process.exit(0); + } - resolve(); + if (isEnterKey(key)) { + done(undefined); } - }; + }); - stdin.on('data', onData); + return ''; }); + + await prompt({}); } /** diff --git a/test/ui/welcome-screen.test.ts b/test/ui/welcome-screen.test.ts new file mode 100644 index 000000000..4d6b65b49 --- /dev/null +++ b/test/ui/welcome-screen.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { useKeypressMock } = vi.hoisted(() => ({ + useKeypressMock: vi.fn(), +})); + +vi.mock('@inquirer/core', () => ({ + createPrompt: vi.fn((view) => async (config: Record) => { + let keypressHandler: ((key: { name: string; ctrl: boolean }) => void) | undefined; + useKeypressMock.mockImplementation((handler) => { + keypressHandler = handler; + }); + + return new Promise((resolve) => { + view(config, resolve); + keypressHandler?.({ name: 'return', ctrl: false }); + }); + }), + isEnterKey: vi.fn((key) => key.name === 'return'), + useKeypress: useKeypressMock, +})); + +describe('welcome screen', () => { + const originalNoColor = process.env.NO_COLOR; + const originalStdinIsTTY = process.stdin.isTTY; + const originalStdoutIsTTY = process.stdout.isTTY; + const originalColumns = process.stdout.columns; + + beforeEach(() => { + delete process.env.NO_COLOR; + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true }); + Object.defineProperty(process.stdout, 'columns', { value: 100, configurable: true }); + vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + useKeypressMock.mockClear(); + }); + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR; + } else { + process.env.NO_COLOR = originalNoColor; + } + Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true }); + Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true }); + Object.defineProperty(process.stdout, 'columns', { value: originalColumns, configurable: true }); + vi.restoreAllMocks(); + }); + + it('uses an Inquirer prompt to wait for Enter', async () => { + const { showWelcomeScreen } = await import('../../src/ui/welcome-screen.js'); + + await showWelcomeScreen(); + + expect(useKeypressMock).toHaveBeenCalledOnce(); + }); +});