WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 894fe2c

Browse files
crowlKatskt3k
andauthored
refactor(cli/unstable): dedupe logic between promptSelect and promptMultipleSelect (#6769)
--------- Co-authored-by: Yoshiya Hinosawa <[email protected]>
1 parent 4db142b commit 894fe2c

File tree

3 files changed

+275
-286
lines changed

3 files changed

+275
-286
lines changed

cli/_prompt_select.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import type { PromptEntry } from "./unstable_prompt_select.ts";
4+
5+
const SAFE_PADDING = 4;
6+
7+
const MORE_CONTENT_BEFORE_INDICATOR = "...";
8+
const MORE_CONTENT_AFTER_INDICATOR = "...";
9+
10+
const input = Deno.stdin;
11+
const output = Deno.stdout;
12+
const encoder = new TextEncoder();
13+
const decoder = new TextDecoder();
14+
15+
const CLEAR_ALL = encoder.encode("\x1b[J"); // Clear all lines after cursor
16+
const HIDE_CURSOR = encoder.encode("\x1b[?25l");
17+
const SHOW_CURSOR = encoder.encode("\x1b[?25h");
18+
19+
/**
20+
* @param message The prompt message to show to the user.
21+
* @param indicator The string to indicate the selected item.
22+
* @param values The values for the prompt.
23+
* @param clear Whether to clear the lines after the user's input.
24+
* @param visibleLinesInit The initial number of lines to be visible at once.
25+
* @param valueChange A function that is called when the value changes.
26+
* @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).
27+
*/
28+
export function handlePromptSelect<V>(
29+
message: string,
30+
indicator: string,
31+
values: PromptEntry<V>[],
32+
clear: boolean | undefined,
33+
visibleLinesInit: number | undefined,
34+
valueChange: (active: boolean, absoluteIndex: number) => string | void,
35+
handleInput: (str: string, absoluteIndex: number | undefined, actions: {
36+
etx(): "return";
37+
up(): void;
38+
down(): void;
39+
remove(): void;
40+
inputStr(): void;
41+
}) => boolean | "return",
42+
) {
43+
const indexedValues = values.map((value, absoluteIndex) => ({
44+
value,
45+
absoluteIndex,
46+
}));
47+
let clearLength = indexedValues.length + 1;
48+
49+
const PADDING = " ".repeat(indicator.length);
50+
const ARROW_PADDING = " ".repeat(indicator.length + 1);
51+
52+
// Deno.consoleSize().rows - 3 because we need to output the message, the up arrow, the terminal line and the down arrow
53+
let visibleLines = visibleLinesInit ?? Math.min(
54+
Deno.consoleSize().rows - SAFE_PADDING,
55+
values.length,
56+
);
57+
58+
let activeIndex = 0;
59+
let offset = 0;
60+
let searchBuffer = "";
61+
const buffer = new Uint8Array(4);
62+
63+
input.setRaw(true);
64+
output.writeSync(HIDE_CURSOR);
65+
66+
while (true) {
67+
output.writeSync(
68+
encoder.encode(
69+
`${message + (searchBuffer ? ` (filter: ${searchBuffer})` : "")}\r\n`,
70+
),
71+
);
72+
const filteredChunks = indexedValues.filter((item) => {
73+
if (searchBuffer === "") {
74+
return true;
75+
} else {
76+
return (typeof item.value === "string" ? item.value : item.value.label)
77+
.toLowerCase().includes(searchBuffer.toLowerCase());
78+
}
79+
});
80+
const visibleChunks = filteredChunks.slice(offset, visibleLines + offset);
81+
const length = visibleChunks.length;
82+
83+
const hasUpArrow = offset !== 0;
84+
const hasDownArrow = (length + offset) < filteredChunks.length;
85+
86+
if (hasUpArrow) {
87+
output.writeSync(
88+
encoder.encode(`${ARROW_PADDING}${MORE_CONTENT_BEFORE_INDICATOR}\r\n`),
89+
);
90+
}
91+
92+
for (
93+
const [
94+
index,
95+
{
96+
absoluteIndex,
97+
value,
98+
},
99+
] of visibleChunks.entries()
100+
) {
101+
const active = index === (activeIndex - offset);
102+
const start = active ? indicator : PADDING;
103+
const maybePrefix = valueChange(active, absoluteIndex);
104+
output.writeSync(
105+
encoder.encode(
106+
`${start}${maybePrefix ? ` ${maybePrefix}` : ""} ${
107+
typeof value === "string" ? value : value.label
108+
}\r\n`,
109+
),
110+
);
111+
}
112+
113+
if (hasDownArrow) {
114+
output.writeSync(
115+
encoder.encode(`${ARROW_PADDING}${MORE_CONTENT_AFTER_INDICATOR}\r\n`),
116+
);
117+
}
118+
const n = input.readSync(buffer);
119+
if (n === null || n === 0) break;
120+
const string = decoder.decode(buffer.slice(0, n));
121+
122+
const processedInput = handleInput(
123+
string,
124+
filteredChunks[activeIndex]?.absoluteIndex,
125+
{
126+
etx: () => {
127+
output.writeSync(SHOW_CURSOR);
128+
Deno.exit(0);
129+
return "return";
130+
},
131+
up: () => {
132+
if (activeIndex === 0) {
133+
activeIndex = filteredChunks.length - 1;
134+
offset = Math.max(filteredChunks.length - visibleLines, 0);
135+
} else {
136+
activeIndex--;
137+
offset = Math.max(offset - 1, 0);
138+
}
139+
},
140+
down: () => {
141+
if (activeIndex === (filteredChunks.length - 1)) {
142+
activeIndex = 0;
143+
offset = 0;
144+
} else {
145+
activeIndex++;
146+
147+
if (activeIndex >= visibleLines) {
148+
offset++;
149+
}
150+
}
151+
},
152+
remove: () => {
153+
activeIndex = 0;
154+
searchBuffer = searchBuffer.slice(0, -1);
155+
},
156+
inputStr: () => {
157+
activeIndex = 0;
158+
searchBuffer += string;
159+
},
160+
},
161+
);
162+
163+
if (processedInput === "return") {
164+
return;
165+
} else if (processedInput) {
166+
break;
167+
}
168+
169+
visibleLines = Math.min(
170+
Deno.consoleSize().rows - SAFE_PADDING,
171+
visibleLines,
172+
);
173+
174+
clearLength = 1 + // message
175+
(hasUpArrow ? 1 : 0) +
176+
length +
177+
(hasDownArrow ? 1 : 0);
178+
179+
output.writeSync(encoder.encode(`\x1b[${clearLength}A`));
180+
output.writeSync(CLEAR_ALL);
181+
}
182+
183+
if (clear) {
184+
output.writeSync(encoder.encode(`\x1b[${clearLength}A`));
185+
output.writeSync(CLEAR_ALL);
186+
}
187+
188+
output.writeSync(SHOW_CURSOR);
189+
input.setRaw(false);
190+
}

0 commit comments

Comments
 (0)