MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / chooseInteractive

Function chooseInteractive

src/setup/prompt.ts:85–214  ·  view source on GitHub ↗

Arrow-key driven selector. Redraws the list in place as the highlight moves.

(
  question: string,
  options: Array<{ value: T; label: string; hint?: string }>,
  defaultValue: T,
)

Source from the content-addressed store, hash-verified

83
84/** Arrow-key driven selector. Redraws the list in place as the highlight moves. */
85function chooseInteractive<T extends string>(
86 question: string,
87 options: Array<{ value: T; label: string; hint?: string }>,
88 defaultValue: T,
89): Promise<T> {
90 return new Promise<T>((resolve) => {
91 let idx = initialIndex(options, defaultValue);
92 const stdin = process.stdin;
93 const stdout = process.stdout;
94
95 // Window the list so it never grows taller than the terminal. Without this, a list longer
96 // than the visible rows scrolls the top options off-screen, and the in-place redraw
97 // (\x1b[NA) can't reach those rows anymore — so only the bottom few stay visible and the
98 // highlight appears stuck. We show a sliding window of `pageSize` rows that follows the
99 // cursor, with ▲/▼ markers when there's more above/below.
100 const termRows = (stdout.rows && stdout.rows > 0) ? stdout.rows : 24;
101 // Reserve rows for: the question line already printed, the hint line, the optional
102 // more-above / more-below markers, and a little breathing room.
103 const pageSize = Math.max(3, Math.min(options.length, termRows - 5));
104 const windowed = options.length > pageSize;
105 let top = 0; // index of the first visible option
106 // The drawn block = (more-above marker?) + visible rows + (more-below marker?) + hint line.
107 let block = 0;
108 let drawn = false;
109
110 console.log(question);
111
112 const clampWindow = (): void => {
113 if (idx < top) top = idx;
114 else if (idx >= top + pageSize) top = idx - pageSize + 1;
115 if (top < 0) top = 0;
116 if (top > Math.max(0, options.length - pageSize)) top = Math.max(0, options.length - pageSize);
117 };
118
119 const render = (): void => {
120 clampWindow();
121 if (drawn && block > 0) stdout.write(`\x1b[${block}A`); // jump back to the top of the block
122 stdout.write('\x1b[0J'); // clear from cursor down
123
124 // Count PHYSICAL rows (accounting for terminal wrap), not logical lines — a long
125 // option that wraps occupies >1 row, and if we undercount the cursor-up leaves the
126 // top rows behind and every keypress piles up another stale copy.
127 const cols = (stdout.columns && stdout.columns > 0) ? stdout.columns : 80;
128 let physical = 0;
129 const emit = (s: string): void => { stdout.write(s + '\n'); physical += physicalRows(s, cols); };
130
131 const end = Math.min(options.length, top + pageSize);
132
133 if (windowed && top > 0) emit(dim(` ↑ ${top} more above`));
134 for (let i = top; i < end; i++) {
135 const opt = options[i]!;
136 const sel = i === idx;
137 const marker = sel ? '▸' : ' ';
138 const num = String(i + 1).padStart(2);
139 const label = sel ? `\x1b[36m\x1b[1m${opt.label}\x1b[0m` : opt.label;
140 const hint = opt.hint ? ` ${dim(opt.hint)}` : '';
141 emit(` ${marker} ${num}. ${label}${hint}`);
142 }

Callers 1

chooseFunction · 0.85

Calls 4

initialIndexFunction · 0.85
renderFunction · 0.85
resumeMethod · 0.65
writeMethod · 0.45

Tested by

no test coverage detected