1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170 |
x1
x1
x1
x1
x1
x1
x1
x1
x1
x1
x1
x1
x15
x14
x15
x15
x15
x15
x15
x15
x15
x15
x142
x142
x15
x15
x15
x15
x15
x15
x15
x15
x57
x57
x1
x57
x2
x2
x57
x13
x13
x57
x13
x57
x13
x13
x13
x13
x13
x13
x57
x6
x6
x57
x9
x9
x57
x43
x15
x15
x15 |
I
|
// Copyright 2018-2026 the Deno authors. MIT license.
import { handlePromptSelect } from "./_prompt_select.ts";
/** Options for {@linkcode promptMultipleSelect}. */
export interface PromptMultipleSelectOptions {
/** Clear the lines after the user's input. */
clear?: boolean;
/** The number of lines to be visible at once */
visibleLines?: number;
/** The string to indicate the selected item */
indicator?: string;
/**
* If true, the visible lines will be calculated based on the remaining
* height from the current cursor position instead of using the full
* screen height. This is useful when you have content above the prompt
* that should remain visible.
*/
fitToRemainingHeight?: boolean;
}
/**
* Value for {@linkcode promptMultipleSelect}.
* If an object, it must have a title and a value, else it can just be a string.
*
* @typeParam V The value of the underlying Entry, if any.
*/
export type PromptEntry<V = undefined> = V extends undefined ? string
: PromptEntryWithValue<V>;
/**
* A {@linkcode PromptEntry} with an underlying value.
*
* @typeParam V The value of the underlying Entry.
*/
export interface PromptEntryWithValue<V> {
/** The label for this entry. */
label: string;
/** The underlying value representing this entry. */
value: V;
}
const ETX = "\x03";
const ARROW_UP = "\u001B[A";
const ARROW_DOWN = "\u001B[B";
const CR = "\r";
const DELETE = "\u007F";
const CHECKED = "◉";
const UNCHECKED = "◯";
/**
* Shows the given message and waits for the user's input. Returns the user's selected value as string.
*
* Also supports filtering of the options by typing.
*
* @typeParam V The value of the underlying Entry, if any.
* @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 or `null` if stdin is not a TTY.
*
* @example Usage
* ```ts ignore
* import { promptMultipleSelect } from "@std/cli/unstable-prompt-multiple-select";
*
* const browsers = promptMultipleSelect(
* "Please select browsers:",
* ["safari", "chrome", "firefox"],
* { clear: true },
* );
* ```
*
* @example With title and value
* ```ts ignore
* import { promptMultipleSelect } from "@std/cli/unstable-prompt-multiple-select";
*
* const browsers = promptMultipleSelect(
* "Please select browsers:",
* [{
* label: "safari",
* value: 1,
* }, {
* label: "chrome",
* value: 2,
* }, {
* label: "firefox",
* value: 3,
* }],
* { clear: true },
* );
* ```
*
* @example With multiple options
* ```ts ignore
* import { promptMultipleSelect } from "@std/cli/unstable-prompt-multiple-select";
*
* const browsers = promptMultipleSelect(
* "Select your favorite numbers below 100:",
* [...Array(100).keys()].map(String),
* { clear: true, visibleLines: 5, indicator: "→" },
* );
* ```
*/
export function promptMultipleSelect<V = undefined>(
message: string,
values: PromptEntry<V>[],
options: PromptMultipleSelectOptions = {},
): PromptEntry<V>[] | null {
if (!Deno.stdin.isTerminal()) return null;
const selectedAbsoluteIndexes = new Set<number>();
handlePromptSelect(
message,
options.indicator ?? "❯",
values,
options.clear,
options.visibleLines,
options.fitToRemainingHeight,
(_active, absoluteIndex) => {
const checked = selectedAbsoluteIndexes.has(absoluteIndex);
return checked ? CHECKED : UNCHECKED;
},
(str, absoluteIndex, {
etx,
up,
down,
remove,
inputStr,
}) => {
switch (str) {
case ETX:
return etx();
case ARROW_UP:
up();
break;
case ARROW_DOWN:
down();
break;
case CR:
return true;
case " ": {
if (absoluteIndex !== undefined) {
if (selectedAbsoluteIndexes.has(absoluteIndex)) {
selectedAbsoluteIndexes.delete(absoluteIndex);
} else {
selectedAbsoluteIndexes.add(absoluteIndex);
}
}
break;
}
case DELETE:
remove();
break;
default:
inputStr();
break;
}
return false;
},
);
return [...selectedAbsoluteIndexes].map((it) => values[it]!);
}
|