Skip to content

Commit aa81c09

Browse files
timreichenkt3k
andauthored
feat(cli/unstable): add promptMultipleSelect() (denoland#6223)
Co-authored-by: Yoshiya Hinosawa <[email protected]>
1 parent 6828931 commit aa81c09

File tree

4 files changed

+647
-0
lines changed

4 files changed

+647
-0
lines changed

_tools/check_docs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const ENTRY_POINTS = [
3838
"../cbor/mod.ts",
3939
"../cli/mod.ts",
4040
"../cli/unstable_spinner.ts",
41+
"../cli/unstable_prompt_multiple_select.ts",
4142
"../crypto/mod.ts",
4243
"../collections/mod.ts",
4344
"../csv/mod.ts",

cli/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"./parse-args": "./parse_args.ts",
77
"./prompt-secret": "./prompt_secret.ts",
88
"./unstable-prompt-select": "./unstable_prompt_select.ts",
9+
"./unstable-prompt-multiple-select": "./unstable_prompt_multiple_select.ts",
910
"./unstable-spinner": "./unstable_spinner.ts",
1011
"./unicode-width": "./unicode_width.ts"
1112
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
/** Options for {@linkcode promptMultipleSelect}. */
4+
export interface PromptMultipleSelectOptions {
5+
/** Clear the lines after the user's input. */
6+
clear?: boolean;
7+
}
8+
9+
const ETX = "\x03";
10+
const ARROW_UP = "\u001B[A";
11+
const ARROW_DOWN = "\u001B[B";
12+
const CR = "\r";
13+
const INDICATOR = "❯";
14+
const PADDING = " ".repeat(INDICATOR.length);
15+
16+
const CHECKED = "◉";
17+
const UNCHECKED = "◯";
18+
19+
const encoder = new TextEncoder();
20+
const decoder = new TextDecoder();
21+
22+
const CLEAR_ALL = encoder.encode("\x1b[J"); // Clear all lines after cursor
23+
const HIDE_CURSOR = encoder.encode("\x1b[?25l");
24+
const SHOW_CURSOR = encoder.encode("\x1b[?25h");
25+
26+
/**
27+
* Shows the given message and waits for the user's input. Returns the user's selected value as string.
28+
*
29+
* @param message The prompt message to show to the user.
30+
* @param values The values for the prompt.
31+
* @param options The options for the prompt.
32+
* @returns The selected values as an array of strings.
33+
*
34+
* @example Usage
35+
* ```ts ignore
36+
* import { promptMultipleSelect } from "@std/cli/unstable-prompt-multiple-select";
37+
*
38+
* const browsers = promptMultipleSelect("Please select browsers:", ["safari", "chrome", "firefox"], { clear: true });
39+
* ```
40+
*/
41+
export function promptMultipleSelect(
42+
message: string,
43+
values: string[],
44+
options: PromptMultipleSelectOptions = {},
45+
): string[] {
46+
const { clear } = options;
47+
const length = values.length;
48+
let selectedIndex = 0;
49+
const selectedIndexes = new Set<number>();
50+
51+
Deno.stdin.setRaw(true);
52+
Deno.stdout.writeSync(HIDE_CURSOR);
53+
const buffer = new Uint8Array(4);
54+
loop:
55+
while (true) {
56+
Deno.stdout.writeSync(encoder.encode(`${message}\r\n`));
57+
for (const [index, value] of values.entries()) {
58+
const selected = index === selectedIndex;
59+
const start = selected ? INDICATOR : PADDING;
60+
const checked = selectedIndexes.has(index);
61+
const state = checked ? CHECKED : UNCHECKED;
62+
Deno.stdout.writeSync(encoder.encode(`${start} ${state} ${value}\r\n`));
63+
}
64+
const n = Deno.stdin.readSync(buffer);
65+
if (n === null || n === 0) break;
66+
const input = decoder.decode(buffer.slice(0, n));
67+
68+
switch (input) {
69+
case ETX:
70+
Deno.stdout.writeSync(SHOW_CURSOR);
71+
return Deno.exit(0);
72+
case ARROW_UP:
73+
selectedIndex = (selectedIndex - 1 + length) % length;
74+
break;
75+
case ARROW_DOWN:
76+
selectedIndex = (selectedIndex + 1) % length;
77+
break;
78+
case CR:
79+
break loop;
80+
case " ":
81+
if (selectedIndexes.has(selectedIndex)) {
82+
selectedIndexes.delete(selectedIndex);
83+
} else {
84+
selectedIndexes.add(selectedIndex);
85+
}
86+
break;
87+
}
88+
Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`));
89+
}
90+
if (clear) {
91+
Deno.stdout.writeSync(encoder.encode(`\x1b[${length + 1}A`));
92+
Deno.stdout.writeSync(CLEAR_ALL);
93+
}
94+
Deno.stdout.writeSync(SHOW_CURSOR);
95+
Deno.stdin.setRaw(false);
96+
return [...selectedIndexes].map((it) => values[it] as string);
97+
}

0 commit comments

Comments
 (0)