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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190 |
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x31
x126
x126
x31
x31
x31
x31
x31
x31
x31
x31
x31
x31
x31
x31
x31
x31
x139
x139
x139
x139
x499
x778
x499
x580
x139
x139
x139
x139
x139
x139
x143
x143
x143
x139
x139
x139
x139
x139
x139
x139
x139
x139
x426
x426
x426
x426
x426
x426
x426
x426
x426
x139
x152
x152
x152
x139
x139
x139
x139
x139
x139
x139
x141
x141
x141
x139
x139
x143
x145
x145
x145
x145
x145
x145
x139
x139
x172
x177
x177
x172
x200
x200
x204
x204
x200
x139
x139
x151
x151
x139
x139
x156
x156
x156
x139
x139
x141
x139
x272
x272
x218
x218
x218
x218
x139
x139
x139
x139
x139
x139
x31
x33
x33
x33
x58
x58
x31 |
I
I
|
// Copyright 2018-2025 the Deno authors. MIT license.
import type { PromptEntry } from "./unstable_prompt_select.ts";
const SAFE_PADDING = 4;
const MORE_CONTENT_BEFORE_INDICATOR = "...";
const MORE_CONTENT_AFTER_INDICATOR = "...";
const input = Deno.stdin;
const output = Deno.stdout;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const CLEAR_ALL = encoder.encode("\x1b[J"); // Clear all lines after cursor
const HIDE_CURSOR = encoder.encode("\x1b[?25l");
const SHOW_CURSOR = encoder.encode("\x1b[?25h");
/**
* @param message The prompt message to show to the user.
* @param indicator The string to indicate the selected item.
* @param values The values for the prompt.
* @param clear Whether to clear the lines after the user's input.
* @param visibleLinesInit The initial number of lines to be visible at once.
* @param valueChange A function that is called when the value changes.
* @param handleInput A function that handles the input from the user. If it returns false, the prompt will continue. If it returns true, the prompt will exit with clean ups of terminal state (Use this for finalizing the selection). If it returns "return", the prompt will exit immediately without clean ups of terminal state (Use this for exiting the program).
*/
export function handlePromptSelect<V>(
message: string,
indicator: string,
values: PromptEntry<V>[],
clear: boolean | undefined,
visibleLinesInit: number | undefined,
valueChange: (active: boolean, absoluteIndex: number) => string | void,
handleInput: (str: string, absoluteIndex: number | undefined, actions: {
etx(): "return";
up(): void;
down(): void;
remove(): void;
inputStr(): void;
}) => boolean | "return",
) {
const indexedValues = values.map((value, absoluteIndex) => ({
value,
absoluteIndex,
}));
let clearLength = indexedValues.length + 1;
const PADDING = " ".repeat(indicator.length);
const ARROW_PADDING = " ".repeat(indicator.length + 1);
// Deno.consoleSize().rows - 3 because we need to output the message, the up arrow, the terminal line and the down arrow
let visibleLines = visibleLinesInit ?? Math.min(
Deno.consoleSize().rows - SAFE_PADDING,
values.length,
);
let activeIndex = 0;
let offset = 0;
let searchBuffer = "";
const buffer = new Uint8Array(4);
input.setRaw(true);
output.writeSync(HIDE_CURSOR);
while (true) {
output.writeSync(
encoder.encode(
`${message + (searchBuffer ? ` (filter: ${searchBuffer})` : "")}\r\n`,
),
);
const filteredChunks = indexedValues.filter((item) => {
if (searchBuffer === "") {
return true;
} else {
return (typeof item.value === "string" ? item.value : item.value.label)
.toLowerCase().includes(searchBuffer.toLowerCase());
}
});
const visibleChunks = filteredChunks.slice(offset, visibleLines + offset);
const length = visibleChunks.length;
const hasUpArrow = offset !== 0;
const hasDownArrow = (length + offset) < filteredChunks.length;
if (hasUpArrow) {
output.writeSync(
encoder.encode(`${ARROW_PADDING}${MORE_CONTENT_BEFORE_INDICATOR}\r\n`),
);
}
for (
const [
index,
{
absoluteIndex,
value,
},
] of visibleChunks.entries()
) {
const active = index === (activeIndex - offset);
const start = active ? indicator : PADDING;
const maybePrefix = valueChange(active, absoluteIndex);
output.writeSync(
encoder.encode(
`${start}${maybePrefix ? ` ${maybePrefix}` : ""} ${
typeof value === "string" ? value : value.label
}\r\n`,
),
);
}
if (hasDownArrow) {
output.writeSync(
encoder.encode(`${ARROW_PADDING}${MORE_CONTENT_AFTER_INDICATOR}\r\n`),
);
}
const n = input.readSync(buffer);
if (n === null || n === 0) break;
const string = decoder.decode(buffer.slice(0, n));
const processedInput = handleInput(
string,
filteredChunks[activeIndex]?.absoluteIndex,
{
etx: () => {
output.writeSync(SHOW_CURSOR);
Deno.exit(0);
return "return";
},
up: () => {
if (activeIndex === 0) {
activeIndex = filteredChunks.length - 1;
offset = Math.max(filteredChunks.length - visibleLines, 0);
} else {
activeIndex--;
offset = Math.max(offset - 1, 0);
}
},
down: () => {
if (activeIndex === (filteredChunks.length - 1)) {
activeIndex = 0;
offset = 0;
} else {
activeIndex++;
if (activeIndex >= visibleLines) {
offset++;
}
}
},
remove: () => {
activeIndex = 0;
searchBuffer = searchBuffer.slice(0, -1);
},
inputStr: () => {
activeIndex = 0;
searchBuffer += string;
},
},
);
if (processedInput === "return") {
return;
} else if (processedInput) {
break;
}
visibleLines = Math.min(
Deno.consoleSize().rows - SAFE_PADDING,
visibleLines,
);
clearLength = 1 + // message
(hasUpArrow ? 1 : 0) +
length +
(hasDownArrow ? 1 : 0);
output.writeSync(encoder.encode(`\x1b[${clearLength}A`));
output.writeSync(CLEAR_ALL);
}
if (clear) {
output.writeSync(encoder.encode(`\x1b[${clearLength}A`));
output.writeSync(CLEAR_ALL);
}
output.writeSync(SHOW_CURSOR);
input.setRaw(false);
}
|