Skip to content

Commit

Permalink
fix(cli/unstable): promptMultipleSelect() add isTerminal() check (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
timreichen authored Dec 17, 2024
1 parent 026a953 commit 7921baa
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 15 deletions.
40 changes: 25 additions & 15 deletions cli/unstable_prompt_multiple_select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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
Expand All @@ -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<number>();

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;
Expand All @@ -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);
}
37 changes: 37 additions & 0 deletions cli/unstable_prompt_multiple_select_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
});

0 comments on commit 7921baa

Please sign in to comment.