From 7921baa16605fe2a1dbb15ff1af98a65129faf91 Mon Sep 17 00:00:00 2001 From: Tim Reichen Date: Tue, 17 Dec 2024 04:33:46 +0100 Subject: [PATCH] fix(cli/unstable): `promptMultipleSelect()` add `isTerminal()` check (#6263) --- cli/unstable_prompt_multiple_select.ts | 40 +++++++++++++-------- cli/unstable_prompt_multiple_select_test.ts | 37 +++++++++++++++++++ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/cli/unstable_prompt_multiple_select.ts b/cli/unstable_prompt_multiple_select.ts index b202b9653294..eeb43f479bbc 100644 --- a/cli/unstable_prompt_multiple_select.ts +++ b/cli/unstable_prompt_multiple_select.ts @@ -16,6 +16,8 @@ const PADDING = " ".repeat(INDICATOR.length); const CHECKED = "◉"; const UNCHECKED = "◯"; +const input = Deno.stdin; +const output = Deno.stdout; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -29,7 +31,7 @@ const SHOW_CURSOR = encoder.encode("\x1b[?25h"); * @param message The prompt message to show to the user. * @param values The values for the prompt. * @param options The options for the prompt. - * @returns The selected values as an array of strings. + * @returns The selected values as an array of strings or `null` if stdin is not a TTY. * * @example Usage * ```ts ignore @@ -42,32 +44,37 @@ export function promptMultipleSelect( message: string, values: string[], options: PromptMultipleSelectOptions = {}, -): string[] { +): string[] | null { + if (!input.isTerminal()) return null; + const { clear } = options; + const length = values.length; let selectedIndex = 0; const selectedIndexes = new Set(); - Deno.stdin.setRaw(true); - Deno.stdout.writeSync(HIDE_CURSOR); + input.setRaw(true); + output.writeSync(HIDE_CURSOR); + const buffer = new Uint8Array(4); + loop: while (true) { - Deno.stdout.writeSync(encoder.encode(`${message}\r\n`)); + output.writeSync(encoder.encode(`${message}\r\n`)); for (const [index, value] of values.entries()) { const selected = index === selectedIndex; const start = selected ? INDICATOR : PADDING; const checked = selectedIndexes.has(index); const state = checked ? CHECKED : UNCHECKED; - Deno.stdout.writeSync(encoder.encode(`${start} ${state} ${value}\r\n`)); + output.writeSync(encoder.encode(`${start} ${state} ${value}\r\n`)); } - const n = Deno.stdin.readSync(buffer); + const n = input.readSync(buffer); if (n === null || n === 0) break; - const input = decoder.decode(buffer.slice(0, n)); + const string = decoder.decode(buffer.slice(0, n)); - switch (input) { + switch (string) { case ETX: - Deno.stdout.writeSync(SHOW_CURSOR); + output.writeSync(SHOW_CURSOR); return Deno.exit(0); case ARROW_UP: selectedIndex = (selectedIndex - 1 + length) % length; @@ -85,13 +92,16 @@ export function promptMultipleSelect( } break; } - Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`)); + output.writeSync(encoder.encode(`\x1b[${length + 1}A`)); } + if (clear) { - Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`)); - Deno.stdout.writeSync(CLEAR_ALL); + output.writeSync(encoder.encode(`\x1b[${length + 1}A`)); + output.writeSync(CLEAR_ALL); } - Deno.stdout.writeSync(SHOW_CURSOR); - Deno.stdin.setRaw(false); + + output.writeSync(SHOW_CURSOR); + input.setRaw(false); + return [...selectedIndexes].map((it) => values[it] as string); } diff --git a/cli/unstable_prompt_multiple_select_test.ts b/cli/unstable_prompt_multiple_select_test.ts index eb95b1252c90..ba0cc2e70b3c 100644 --- a/cli/unstable_prompt_multiple_select_test.ts +++ b/cli/unstable_prompt_multiple_select_test.ts @@ -9,6 +9,7 @@ const decoder = new TextDecoder(); Deno.test("promptMultipleSelect() handles enter", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -62,6 +63,7 @@ Deno.test("promptMultipleSelect() handles enter", () => { Deno.test("promptMultipleSelect() handles selection", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -121,6 +123,7 @@ Deno.test("promptMultipleSelect() handles selection", () => { Deno.test("promptMultipleSelect() handles multiple selection", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -204,6 +207,7 @@ Deno.test("promptMultipleSelect() handles multiple selection", () => { Deno.test("promptMultipleSelect() handles arrow down", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -275,6 +279,7 @@ Deno.test("promptMultipleSelect() handles arrow down", () => { Deno.test("promptMultipleSelect() handles arrow up", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -346,6 +351,7 @@ Deno.test("promptMultipleSelect() handles arrow up", () => { Deno.test("promptMultipleSelect() handles up index overflow", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -411,6 +417,7 @@ Deno.test("promptMultipleSelect() handles up index overflow", () => { Deno.test("promptMultipleSelect() handles down index overflow", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -489,6 +496,7 @@ Deno.test("promptMultipleSelect() handles down index overflow", () => { Deno.test("promptMultipleSelect() handles clear option", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "\x1b[?25l", @@ -549,6 +557,7 @@ Deno.test("promptMultipleSelect() handles clear option", () => { Deno.test("promptMultipleSelect() handles ETX", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); let called = false; stub( @@ -607,3 +616,31 @@ Deno.test("promptMultipleSelect() handles ETX", () => { assertEquals(expectedOutput, actualOutput); restore(); }); + +Deno.test("promptMultipleSelect() returns null if Deno.stdin.isTerminal() is false", () => { + stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => false); + + const expectedOutput: string[] = []; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + const browsers = promptMultipleSelect("Please select browsers:", [ + "safari", + "chrome", + "firefox", + ]); + assertEquals(browsers, null); + assertEquals(expectedOutput, actualOutput); + restore(); +});