Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli/unstable): hide cursor while showing the selection with promptSelect() #6221

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions cli/unstable_prompt_select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
const INDICATOR = "❯";
const PADDING = " ".repeat(INDICATOR.length);

const CLR_ALL = "\x1b[J"; // Clear all lines after cursor

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const CLR_ALL = encoder.encode("\x1b[J"); // Clear all lines after cursor
const HIDE_CURSOR = encoder.encode("\x1b[?25l");
const SHOW_CURSOR = encoder.encode("\x1b[?25h");

/**
* Shows the given message and waits for the user's input. Returns the user's selected value as string.
*
Expand All @@ -41,21 +43,23 @@
const length = values.length;
let selectedIndex = 0;

Deno.stdout.writeSync(encoder.encode(`${message}\r\n`));
Deno.stdin.setRaw(true);

Deno.stdout.writeSync(HIDE_CURSOR);
const buffer = new Uint8Array(4);
loop:
while (true) {
Deno.stdout.writeSync(encoder.encode(`${message}\r\n`));
for (const [index, value] of values.entries()) {
const start = index === selectedIndex ? INDICATOR : PADDING;
Deno.stdout.writeSync(encoder.encode(`${start} ${value}\r\n`));
}
const n = Deno.stdin.readSync(buffer);
if (n === null || n === 0) break;
const input = decoder.decode(buffer.slice(0, n));

switch (input) {
case ETX:
Deno.stdout.writeSync(SHOW_CURSOR);

Check warning on line 62 in cli/unstable_prompt_select.ts

View check run for this annotation

Codecov / codecov/patch

cli/unstable_prompt_select.ts#L62

Added line #L62 was not covered by tests
return Deno.exit(0);
case ARROW_UP:
selectedIndex = (selectedIndex - 1 + length) % length;
Expand All @@ -66,13 +70,13 @@
case CR:
break loop;
}
Deno.stdout.writeSync(encoder.encode(`\x1b[${length}A`)); // move cursor after message
Deno.stdout.writeSync(encoder.encode(CLR_ALL));
Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`));
}
if (clear) {
Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`)); // move cursor before message
Deno.stdout.writeSync(encoder.encode(CLR_ALL));
Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`));
Deno.stdout.writeSync(CLR_ALL);
}
Deno.stdout.writeSync(SHOW_CURSOR);
Deno.stdin.setRaw(false);
return values[selectedIndex] ?? null;
}
80 changes: 46 additions & 34 deletions cli/unstable_prompt_select_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ Deno.test("promptSelect() handles enter", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -54,38 +55,40 @@ Deno.test("promptSelect() handles enter", () => {
]);

assertEquals(browser, "safari");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSelect() handles arrow down", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
"❯ chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
" chrome\r\n",
"❯ firefox\r\n",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -116,38 +119,40 @@ Deno.test("promptSelect() handles arrow down", () => {
]);

assertEquals(browser, "firefox");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSelect() handles arrow up", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
"❯ chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -178,33 +183,35 @@ Deno.test("promptSelect() handles arrow up", () => {
]);

assertEquals(browser, "safari");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSelect() handles up index overflow", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
" chrome\r\n",
"❯ firefox\r\n",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -234,43 +241,45 @@ Deno.test("promptSelect() handles up index overflow", () => {
]);

assertEquals(browser, "firefox");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSelect() handles down index overflow", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
"❯ chrome\r\n",
" firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
" safari\r\n",
" chrome\r\n",
"❯ firefox\r\n",
"\x1b[3A",
"\x1b[J",
"\x1b[4A",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -302,30 +311,32 @@ Deno.test("promptSelect() handles down index overflow", () => {
]);

assertEquals(browser, "safari");
assertEquals(expectedOutput, actualOutput);
restore();
});

Deno.test("promptSelect() handles clear option", () => {
stub(Deno.stdin, "setRaw");

const expectedOutput = [
"\x1b[?25l",
"Please select a browser:\r\n",
"❯ safari\r\n",
" chrome\r\n",
" firefox\r\n",
"\x1b[4A",
"\x1b[J",
"\x1b[?25h",
];

let writeIndex = 0;
const actualOutput: string[] = [];

stub(
Deno.stdout,
"writeSync",
(data: Uint8Array) => {
const output = decoder.decode(data);
assertEquals(output, expectedOutput[writeIndex]);
writeIndex++;
actualOutput.push(output);
return data.length;
},
);
Expand Down Expand Up @@ -354,5 +365,6 @@ Deno.test("promptSelect() handles clear option", () => {
], { clear: true });

assertEquals(browser, "safari");
assertEquals(expectedOutput, actualOutput);
restore();
});
Loading