instant-cli 1.0.33 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/__tests__/multiSelect.test.ts +236 -0
- package/__tests__/select.test.ts +224 -0
- package/__tests__/webhooks.test.ts +728 -0
- package/dist/commands/webhooks/add.d.ts +9 -0
- package/dist/commands/webhooks/add.d.ts.map +1 -0
- package/dist/commands/webhooks/add.js +75 -0
- package/dist/commands/webhooks/add.js.map +1 -0
- package/dist/commands/webhooks/delete.d.ts +6 -0
- package/dist/commands/webhooks/delete.d.ts.map +1 -0
- package/dist/commands/webhooks/delete.js +17 -0
- package/dist/commands/webhooks/delete.js.map +1 -0
- package/dist/commands/webhooks/disable.d.ts +7 -0
- package/dist/commands/webhooks/disable.d.ts.map +1 -0
- package/dist/commands/webhooks/disable.js +18 -0
- package/dist/commands/webhooks/disable.js.map +1 -0
- package/dist/commands/webhooks/enable.d.ts +6 -0
- package/dist/commands/webhooks/enable.d.ts.map +1 -0
- package/dist/commands/webhooks/enable.js +18 -0
- package/dist/commands/webhooks/enable.js.map +1 -0
- package/dist/commands/webhooks/events/list.d.ts +7 -0
- package/dist/commands/webhooks/events/list.d.ts.map +1 -0
- package/dist/commands/webhooks/events/list.js +31 -0
- package/dist/commands/webhooks/events/list.js.map +1 -0
- package/dist/commands/webhooks/events/payload.d.ts +8 -0
- package/dist/commands/webhooks/events/payload.d.ts.map +1 -0
- package/dist/commands/webhooks/events/payload.js +39 -0
- package/dist/commands/webhooks/events/payload.js.map +1 -0
- package/dist/commands/webhooks/events/resend.d.ts +8 -0
- package/dist/commands/webhooks/events/resend.d.ts.map +1 -0
- package/dist/commands/webhooks/events/resend.js +43 -0
- package/dist/commands/webhooks/events/resend.js.map +1 -0
- package/dist/commands/webhooks/list.d.ts +8 -0
- package/dist/commands/webhooks/list.d.ts.map +1 -0
- package/dist/commands/webhooks/list.js +29 -0
- package/dist/commands/webhooks/list.js.map +1 -0
- package/dist/commands/webhooks/shared.d.ts +40 -0
- package/dist/commands/webhooks/shared.d.ts.map +1 -0
- package/dist/commands/webhooks/shared.js +248 -0
- package/dist/commands/webhooks/shared.js.map +1 -0
- package/dist/commands/webhooks/update.d.ts +10 -0
- package/dist/commands/webhooks/update.d.ts.map +1 -0
- package/dist/commands/webhooks/update.js +189 -0
- package/dist/commands/webhooks/update.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -1
- package/dist/layer.d.ts +2 -2
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +30 -1
- package/dist/layer.js.map +1 -1
- package/dist/lib/webhooks.d.ts +28 -0
- package/dist/lib/webhooks.d.ts.map +1 -0
- package/dist/lib/webhooks.js +102 -0
- package/dist/lib/webhooks.js.map +1 -0
- package/dist/ui/index.d.ts +39 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +387 -25
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/lib.d.ts +7 -0
- package/dist/ui/lib.d.ts.map +1 -1
- package/dist/ui/lib.js +40 -1
- package/dist/ui/lib.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/webhooks/add.ts +111 -0
- package/src/commands/webhooks/delete.ts +23 -0
- package/src/commands/webhooks/disable.ts +24 -0
- package/src/commands/webhooks/enable.ts +24 -0
- package/src/commands/webhooks/events/list.ts +38 -0
- package/src/commands/webhooks/events/payload.ts +56 -0
- package/src/commands/webhooks/events/resend.ts +66 -0
- package/src/commands/webhooks/list.ts +41 -0
- package/src/commands/webhooks/shared.ts +339 -0
- package/src/commands/webhooks/update.ts +276 -0
- package/src/index.ts +242 -0
- package/src/layer.ts +33 -1
- package/src/lib/webhooks.ts +127 -0
- package/src/ui/index.ts +465 -32
- package/src/ui/lib.ts +41 -1
package/src/ui/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ import stringWidth from 'string-width';
|
|
|
4
4
|
import { Prompt, SelectState } from './lib.ts';
|
|
5
5
|
import type { AnyKey, ModifyOutputFn } from './lib.ts';
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
clearPromptTrail,
|
|
9
|
+
render,
|
|
10
|
+
renderUnwrap,
|
|
11
|
+
setRawModeWindowsFriendly,
|
|
12
|
+
} from './lib.ts';
|
|
8
13
|
|
|
9
14
|
export namespace UI {
|
|
10
15
|
type Status = 'idle' | 'submitted' | 'aborted';
|
|
@@ -92,6 +97,7 @@ export namespace UI {
|
|
|
92
97
|
options: {
|
|
93
98
|
value: T;
|
|
94
99
|
label: string;
|
|
100
|
+
expandableLabel?: string | (() => string | Promise<string>);
|
|
95
101
|
secondary?: boolean;
|
|
96
102
|
}[];
|
|
97
103
|
promptText: string;
|
|
@@ -100,25 +106,30 @@ export namespace UI {
|
|
|
100
106
|
};
|
|
101
107
|
export class Select<T> extends Prompt<T> {
|
|
102
108
|
config(status: 'idle' | 'submitted' | 'aborted'): string {
|
|
103
|
-
console.log('config', status);
|
|
104
109
|
return status;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
private readonly data: SelectState<T>;
|
|
108
113
|
private readonly options: SelectProps<T>['options'];
|
|
109
114
|
private readonly params: SelectProps<T>;
|
|
115
|
+
private expandedIdx: number | null = null;
|
|
116
|
+
private stickyExpanded = false;
|
|
117
|
+
private readonly expansionCache: Map<number, string> = new Map();
|
|
118
|
+
private readonly expansionLoading: Set<number> = new Set();
|
|
110
119
|
|
|
111
120
|
constructor(params: SelectProps<T>) {
|
|
112
121
|
super(params.modifyOutput);
|
|
113
122
|
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
|
114
|
-
this.on('input', (input) => {
|
|
115
|
-
if (input === 'j') {
|
|
123
|
+
this.on('input', (input, key) => {
|
|
124
|
+
if (input === 'j' || (key?.ctrl && key.name === 'n')) {
|
|
116
125
|
this.data.selectedIdx =
|
|
117
126
|
(this.data.selectedIdx + 1) % this.options.length;
|
|
118
|
-
|
|
127
|
+
this.applyNavigation();
|
|
128
|
+
} else if (input === 'k' || (key?.ctrl && key.name === 'p')) {
|
|
119
129
|
this.data.selectedIdx =
|
|
120
130
|
(this.data.selectedIdx - 1 + this.options.length) %
|
|
121
131
|
this.options.length;
|
|
132
|
+
this.applyNavigation();
|
|
122
133
|
}
|
|
123
134
|
this.requestLayout();
|
|
124
135
|
});
|
|
@@ -142,6 +153,82 @@ export namespace UI {
|
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
this.data.bind(this as any);
|
|
156
|
+
|
|
157
|
+
this.on('input', (_input, key) => {
|
|
158
|
+
if (key?.name === 'tab') {
|
|
159
|
+
const hasExpandable = this.options.some((o) => o.expandableLabel);
|
|
160
|
+
if (!hasExpandable) return;
|
|
161
|
+
this.stickyExpanded = !this.stickyExpanded;
|
|
162
|
+
if (this.stickyExpanded) {
|
|
163
|
+
this.expandFocused();
|
|
164
|
+
} else {
|
|
165
|
+
this.expandedIdx = null;
|
|
166
|
+
}
|
|
167
|
+
this.requestLayout();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key?.name === 'up' || key?.name === 'down') {
|
|
171
|
+
this.applyNavigation();
|
|
172
|
+
this.requestLayout();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private applyNavigation() {
|
|
178
|
+
if (this.stickyExpanded) {
|
|
179
|
+
this.expandFocused();
|
|
180
|
+
} else {
|
|
181
|
+
this.expandedIdx = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private expandFocused() {
|
|
186
|
+
const focused = this.data.selectedIdx;
|
|
187
|
+
const focusedOption = this.options[focused];
|
|
188
|
+
const exp = focusedOption?.expandableLabel;
|
|
189
|
+
if (!exp) {
|
|
190
|
+
this.expandedIdx = null;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.expandedIdx = focused;
|
|
194
|
+
if (
|
|
195
|
+
typeof exp === 'function' &&
|
|
196
|
+
!this.expansionCache.has(focused) &&
|
|
197
|
+
!this.expansionLoading.has(focused)
|
|
198
|
+
) {
|
|
199
|
+
this.expansionLoading.add(focused);
|
|
200
|
+
Promise.resolve()
|
|
201
|
+
.then(() => (exp as () => string | Promise<string>)())
|
|
202
|
+
.then(
|
|
203
|
+
(content) => {
|
|
204
|
+
this.expansionLoading.delete(focused);
|
|
205
|
+
this.expansionCache.set(focused, content);
|
|
206
|
+
if (this.expandedIdx === focused) this.requestLayout();
|
|
207
|
+
},
|
|
208
|
+
(err) => {
|
|
209
|
+
this.expansionLoading.delete(focused);
|
|
210
|
+
this.expansionCache.set(
|
|
211
|
+
focused,
|
|
212
|
+
chalk.red(
|
|
213
|
+
` Error loading expansion: ${err?.message ?? err}`,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
if (this.expandedIdx === focused) this.requestLayout();
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private getExpansionContent(
|
|
223
|
+
option: SelectProps<T>['options'][number],
|
|
224
|
+
idx: number,
|
|
225
|
+
): string | null {
|
|
226
|
+
const exp = option.expandableLabel;
|
|
227
|
+
if (!exp) return null;
|
|
228
|
+
if (typeof exp === 'string') return exp;
|
|
229
|
+
if (this.expansionLoading.has(idx)) return chalk.dim(' Loading…');
|
|
230
|
+
const cached = this.expansionCache.get(idx);
|
|
231
|
+
return cached ?? chalk.dim(' Loading…');
|
|
145
232
|
}
|
|
146
233
|
|
|
147
234
|
result(): T {
|
|
@@ -154,35 +241,375 @@ export namespace UI {
|
|
|
154
241
|
${chalk.hex('#EA570B').bold('●')} ${this.params.options[this.data.selectedIdx]?.label}`;
|
|
155
242
|
}
|
|
156
243
|
|
|
157
|
-
const
|
|
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
|
-
|
|
244
|
+
const renderRow = (
|
|
245
|
+
option: SelectProps<T>['options'][number],
|
|
246
|
+
originalIdx: number,
|
|
247
|
+
) => {
|
|
248
|
+
const isSelected = originalIdx === this.data.selectedIdx;
|
|
249
|
+
const cursor = isSelected ? chalk.hex('#EA570B').bold('●') : '○';
|
|
250
|
+
const label = isSelected
|
|
251
|
+
? chalk.bold(option.label)
|
|
252
|
+
: chalk.dim(option.label);
|
|
253
|
+
const expandedContent =
|
|
254
|
+
isSelected && this.expandedIdx === originalIdx
|
|
255
|
+
? this.getExpansionContent(option, originalIdx)
|
|
256
|
+
: null;
|
|
257
|
+
const expanded = expandedContent !== null ? '\n' + expandedContent : '';
|
|
258
|
+
return `${cursor} ${label}${expanded}`;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const rowLineCount = (originalIdx: number) => {
|
|
262
|
+
const opt = this.options[originalIdx]!;
|
|
263
|
+
const labelLines = opt.label.split('\n').length;
|
|
264
|
+
const isFocused = originalIdx === this.data.selectedIdx;
|
|
265
|
+
const expContent =
|
|
266
|
+
isFocused && this.expandedIdx === originalIdx
|
|
267
|
+
? this.getExpansionContent(opt, originalIdx)
|
|
268
|
+
: null;
|
|
269
|
+
const expLines =
|
|
270
|
+
expContent !== null ? expContent.split('\n').length : 0;
|
|
271
|
+
return labelLines + expLines;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const mainEntries: {
|
|
275
|
+
opt: SelectProps<T>['options'][number];
|
|
276
|
+
originalIdx: number;
|
|
277
|
+
}[] = [];
|
|
278
|
+
const secondaryEntries: typeof mainEntries = [];
|
|
279
|
+
this.options.forEach((o, i) => {
|
|
280
|
+
if (o.secondary) secondaryEntries.push({ opt: o, originalIdx: i });
|
|
281
|
+
else mainEntries.push({ opt: o, originalIdx: i });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const totalRows = process.stdout.rows ?? 24;
|
|
285
|
+
const secondaryHeight =
|
|
286
|
+
secondaryEntries.length === 0
|
|
287
|
+
? 0
|
|
288
|
+
: secondaryEntries.reduce(
|
|
289
|
+
(acc, { originalIdx }) => acc + rowLineCount(originalIdx),
|
|
290
|
+
0,
|
|
291
|
+
) + 1; // +1 for divider
|
|
292
|
+
const hasExpandable = this.options.some((o) => o.expandableLabel);
|
|
293
|
+
// chrome = prompt(1) + secondaries(+divider) + hint + safety(2)
|
|
294
|
+
const chrome = 1 + secondaryHeight + (hasExpandable ? 1 : 0) + 2;
|
|
295
|
+
const mainBudget = Math.max(3, totalRows - chrome);
|
|
296
|
+
|
|
297
|
+
const focusedMainPos = mainEntries.findIndex(
|
|
298
|
+
(e) => e.originalIdx === this.data.selectedIdx,
|
|
299
|
+
);
|
|
300
|
+
let visiblePositions: number[] = [];
|
|
301
|
+
|
|
302
|
+
if (focusedMainPos >= 0) {
|
|
303
|
+
let used = rowLineCount(mainEntries[focusedMainPos]!.originalIdx);
|
|
304
|
+
visiblePositions = [focusedMainPos];
|
|
305
|
+
let lo = focusedMainPos - 1;
|
|
306
|
+
let hi = focusedMainPos + 1;
|
|
307
|
+
while (lo >= 0 || hi < mainEntries.length) {
|
|
308
|
+
let progressed = false;
|
|
309
|
+
if (lo >= 0) {
|
|
310
|
+
const h = rowLineCount(mainEntries[lo]!.originalIdx);
|
|
311
|
+
if (used + h <= mainBudget) {
|
|
312
|
+
visiblePositions.unshift(lo);
|
|
313
|
+
used += h;
|
|
314
|
+
lo--;
|
|
315
|
+
progressed = true;
|
|
316
|
+
} else {
|
|
317
|
+
lo = -1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (hi < mainEntries.length) {
|
|
321
|
+
const h = rowLineCount(mainEntries[hi]!.originalIdx);
|
|
322
|
+
if (used + h <= mainBudget) {
|
|
323
|
+
visiblePositions.push(hi);
|
|
324
|
+
used += h;
|
|
325
|
+
hi++;
|
|
326
|
+
progressed = true;
|
|
327
|
+
} else {
|
|
328
|
+
hi = mainEntries.length;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!progressed) break;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
// Cursor is on a secondary — show as many mains as fit, top-down.
|
|
335
|
+
let used = 0;
|
|
336
|
+
for (let i = 0; i < mainEntries.length; i++) {
|
|
337
|
+
const h = rowLineCount(mainEntries[i]!.originalIdx);
|
|
338
|
+
if (used + h > mainBudget) break;
|
|
339
|
+
visiblePositions.push(i);
|
|
340
|
+
used += h;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const firstVisible = visiblePositions[0] ?? 0;
|
|
345
|
+
const lastVisible =
|
|
346
|
+
visiblePositions[visiblePositions.length - 1] ?? mainEntries.length - 1;
|
|
347
|
+
const aboveCount = firstVisible;
|
|
348
|
+
const belowCount = Math.max(0, mainEntries.length - 1 - lastVisible);
|
|
349
|
+
|
|
350
|
+
const mainLines: string[] = [];
|
|
351
|
+
if (aboveCount > 0) mainLines.push(chalk.dim(` ↑ ${aboveCount} more`));
|
|
352
|
+
for (const pos of visiblePositions) {
|
|
353
|
+
mainLines.push(
|
|
354
|
+
renderRow(mainEntries[pos]!.opt, mainEntries[pos]!.originalIdx),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (belowCount > 0) mainLines.push(chalk.dim(` ↓ ${belowCount} more`));
|
|
358
|
+
const mainBlock = mainLines.join('\n');
|
|
359
|
+
|
|
360
|
+
const secondaryBlock =
|
|
361
|
+
secondaryEntries.length === 0
|
|
362
|
+
? ''
|
|
363
|
+
: chalk.gray(
|
|
364
|
+
'\n───────────────── Additional Options ─────────────────\n',
|
|
365
|
+
) +
|
|
366
|
+
secondaryEntries
|
|
367
|
+
.map((e) => renderRow(e.opt, e.originalIdx))
|
|
368
|
+
.join('\n');
|
|
369
|
+
|
|
370
|
+
const expandHint = hasExpandable
|
|
371
|
+
? '\n' +
|
|
372
|
+
chalk.dim(
|
|
373
|
+
this.stickyExpanded ? ' (tab to collapse)' : ' (tab to expand)',
|
|
374
|
+
)
|
|
375
|
+
: '';
|
|
183
376
|
|
|
184
377
|
return `${this.params.promptText}
|
|
185
|
-
${
|
|
378
|
+
${mainBlock}${secondaryBlock}${expandHint}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export type MultiSelectOption<T> = {
|
|
383
|
+
value: T;
|
|
384
|
+
label: string;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
type MultiSelectProps<T> = {
|
|
388
|
+
options: MultiSelectOption<T>[];
|
|
389
|
+
promptText: string;
|
|
390
|
+
initialSelected?: T[];
|
|
391
|
+
filter?: (filter: string, option: MultiSelectOption<T>) => boolean;
|
|
392
|
+
minSelected?: number;
|
|
393
|
+
pageSize?: number;
|
|
394
|
+
modifyOutput?: ModifyOutputFn;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const DEFAULT_MULTI_SELECT_PAGE_SIZE = 10;
|
|
398
|
+
|
|
399
|
+
const defaultMultiSelectFilter = <T>(
|
|
400
|
+
filter: string,
|
|
401
|
+
option: MultiSelectOption<T>,
|
|
402
|
+
) => option.label.toLowerCase().includes(filter.toLowerCase());
|
|
403
|
+
|
|
404
|
+
export class MultiSelect<T> extends Prompt<T[]> {
|
|
405
|
+
private filterText = '';
|
|
406
|
+
private cursorIdx = 0;
|
|
407
|
+
private windowStart = 0;
|
|
408
|
+
private selected: Set<number>;
|
|
409
|
+
private errorText: string | undefined;
|
|
410
|
+
private readonly props: MultiSelectProps<T>;
|
|
411
|
+
private readonly pageSize: number;
|
|
412
|
+
|
|
413
|
+
constructor(props: MultiSelectProps<T>) {
|
|
414
|
+
super(props.modifyOutput);
|
|
415
|
+
this.props = props;
|
|
416
|
+
this.pageSize = props.pageSize ?? DEFAULT_MULTI_SELECT_PAGE_SIZE;
|
|
417
|
+
this.selected = new Set(
|
|
418
|
+
(props.initialSelected ?? [])
|
|
419
|
+
.map((v) => props.options.findIndex((o) => o.value === v))
|
|
420
|
+
.filter((i) => i >= 0),
|
|
421
|
+
);
|
|
422
|
+
this.on('attach', (terminal) => {
|
|
423
|
+
terminal.setAllowInteraction(false);
|
|
424
|
+
terminal.toggleCursor('hide');
|
|
425
|
+
});
|
|
426
|
+
this.on('detach', (terminal) => terminal.toggleCursor('show'));
|
|
427
|
+
this.on('input', (input, key) => this.handleKey(input, key));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private visibleIndices(): number[] {
|
|
431
|
+
const fn = this.props.filter ?? defaultMultiSelectFilter;
|
|
432
|
+
if (!this.filterText) return this.props.options.map((_, i) => i);
|
|
433
|
+
return this.props.options
|
|
434
|
+
.map((opt, i) => ({ opt, i }))
|
|
435
|
+
.filter(({ opt }) => fn(this.filterText, opt))
|
|
436
|
+
.map(({ i }) => i);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private clampCursor(visible: number[]) {
|
|
440
|
+
if (visible.length === 0) {
|
|
441
|
+
this.cursorIdx = 0;
|
|
442
|
+
} else if (this.cursorIdx >= visible.length) {
|
|
443
|
+
this.cursorIdx = visible.length - 1;
|
|
444
|
+
} else if (this.cursorIdx < 0) {
|
|
445
|
+
this.cursorIdx = visible.length - 1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private adjustWindow(visible: number[]) {
|
|
450
|
+
if (visible.length <= this.pageSize) {
|
|
451
|
+
this.windowStart = 0;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (this.cursorIdx < this.windowStart) {
|
|
455
|
+
this.windowStart = this.cursorIdx;
|
|
456
|
+
} else if (this.cursorIdx >= this.windowStart + this.pageSize) {
|
|
457
|
+
this.windowStart = this.cursorIdx - this.pageSize + 1;
|
|
458
|
+
}
|
|
459
|
+
const maxStart = Math.max(0, visible.length - this.pageSize);
|
|
460
|
+
if (this.windowStart > maxStart) this.windowStart = maxStart;
|
|
461
|
+
if (this.windowStart < 0) this.windowStart = 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private handleKey(input: string | undefined, key: AnyKey) {
|
|
465
|
+
if (key.name === 'escape') {
|
|
466
|
+
return this.terminal?.resolve({ data: undefined, status: 'aborted' });
|
|
467
|
+
}
|
|
468
|
+
if (key.name === 'return') {
|
|
469
|
+
const min = this.props.minSelected ?? 0;
|
|
470
|
+
if (this.selected.size < min) {
|
|
471
|
+
this.errorText =
|
|
472
|
+
min === 1
|
|
473
|
+
? 'Select at least one option.'
|
|
474
|
+
: `Select at least ${min} options.`;
|
|
475
|
+
this.requestLayout();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
return this.terminal?.resolve({
|
|
479
|
+
data: this.result(),
|
|
480
|
+
status: 'submitted',
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const visible = this.visibleIndices();
|
|
485
|
+
|
|
486
|
+
if (key.name === 'up' || (key.ctrl && key.name === 'p')) {
|
|
487
|
+
this.cursorIdx -= 1;
|
|
488
|
+
this.clampCursor(visible);
|
|
489
|
+
this.adjustWindow(visible);
|
|
490
|
+
this.requestLayout();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (key.name === 'down' || (key.ctrl && key.name === 'n')) {
|
|
494
|
+
this.cursorIdx += 1;
|
|
495
|
+
if (visible.length > 0) this.cursorIdx %= visible.length;
|
|
496
|
+
this.adjustWindow(visible);
|
|
497
|
+
this.requestLayout();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (key.name === 'pageup' || (key.ctrl && key.name === 'u')) {
|
|
501
|
+
this.cursorIdx -= this.pageSize;
|
|
502
|
+
if (this.cursorIdx < 0) this.cursorIdx = 0;
|
|
503
|
+
this.adjustWindow(visible);
|
|
504
|
+
this.requestLayout();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (key.name === 'pagedown' || (key.ctrl && key.name === 'd')) {
|
|
508
|
+
this.cursorIdx += this.pageSize;
|
|
509
|
+
if (this.cursorIdx >= visible.length) {
|
|
510
|
+
this.cursorIdx = Math.max(0, visible.length - 1);
|
|
511
|
+
}
|
|
512
|
+
this.adjustWindow(visible);
|
|
513
|
+
this.requestLayout();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (key.name === 'space') {
|
|
517
|
+
if (visible.length === 0) return;
|
|
518
|
+
const optIdx = visible[this.cursorIdx]!;
|
|
519
|
+
if (this.selected.has(optIdx)) {
|
|
520
|
+
this.selected.delete(optIdx);
|
|
521
|
+
} else {
|
|
522
|
+
this.selected.add(optIdx);
|
|
523
|
+
}
|
|
524
|
+
this.errorText = undefined;
|
|
525
|
+
this.requestLayout();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (key.name === 'backspace') {
|
|
529
|
+
if (this.filterText.length > 0) {
|
|
530
|
+
this.filterText = this.filterText.slice(0, -1);
|
|
531
|
+
this.cursorIdx = 0;
|
|
532
|
+
this.windowStart = 0;
|
|
533
|
+
this.requestLayout();
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (key.name?.length === 1 && !key.ctrl && !key.meta && input) {
|
|
538
|
+
this.filterText += input;
|
|
539
|
+
this.cursorIdx = 0;
|
|
540
|
+
this.windowStart = 0;
|
|
541
|
+
this.requestLayout();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
result(): T[] {
|
|
546
|
+
return [...this.selected]
|
|
547
|
+
.sort((a, b) => a - b)
|
|
548
|
+
.map((i) => this.props.options[i]!.value);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private renderOption(optIdx: number, isCursor: boolean) {
|
|
552
|
+
const option = this.props.options[optIdx]!;
|
|
553
|
+
const isSelected = this.selected.has(optIdx);
|
|
554
|
+
const checkbox = isSelected ? chalk.green('◉') : '○';
|
|
555
|
+
const cursorMark = isCursor ? chalk.hex('#EA570B').bold('❯') : ' ';
|
|
556
|
+
const label = isCursor ? chalk.bold(option.label) : option.label;
|
|
557
|
+
return `${cursorMark} ${checkbox} ${label}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
|
561
|
+
if (status === 'submitted') {
|
|
562
|
+
const labels = [...this.selected]
|
|
563
|
+
.sort((a, b) => a - b)
|
|
564
|
+
.map((i) => this.props.options[i]!.label);
|
|
565
|
+
return `${this.props.promptText}
|
|
566
|
+
${chalk.hex('#EA570B').bold('●')} ${labels.length === 0 ? chalk.dim('(none)') : labels.join(', ')}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const visible = this.visibleIndices();
|
|
570
|
+
this.adjustWindow(visible);
|
|
571
|
+
const filterLine = `${chalk.dim('filter:')} ${this.filterText}${chalk.inverse(' ')}`;
|
|
572
|
+
const errorLine = this.errorText ? ` ${chalk.red(this.errorText)}` : '';
|
|
573
|
+
|
|
574
|
+
let optionsBlock: string;
|
|
575
|
+
if (visible.length === 0) {
|
|
576
|
+
optionsBlock = chalk.dim(' (no matches)');
|
|
577
|
+
} else {
|
|
578
|
+
const windowEnd = Math.min(
|
|
579
|
+
this.windowStart + this.pageSize,
|
|
580
|
+
visible.length,
|
|
581
|
+
);
|
|
582
|
+
const aboveCount = this.windowStart;
|
|
583
|
+
const belowCount = visible.length - windowEnd;
|
|
584
|
+
const lines: string[] = [];
|
|
585
|
+
lines.push(
|
|
586
|
+
aboveCount > 0
|
|
587
|
+
? chalk.dim(` ↑ ${aboveCount} more`)
|
|
588
|
+
: chalk.dim(' '),
|
|
589
|
+
);
|
|
590
|
+
for (let i = this.windowStart; i < windowEnd; i++) {
|
|
591
|
+
lines.push(this.renderOption(visible[i]!, i === this.cursorIdx));
|
|
592
|
+
}
|
|
593
|
+
lines.push(
|
|
594
|
+
belowCount > 0
|
|
595
|
+
? chalk.dim(` ↓ ${belowCount} more`)
|
|
596
|
+
: chalk.dim(' '),
|
|
597
|
+
);
|
|
598
|
+
optionsBlock = lines.join('\n');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const hint = chalk.dim(
|
|
602
|
+
` ↑↓ or ctrl-n/p navigate · pgup/pgdn or ctrl-u/d page · space toggle · enter submit · esc cancel`,
|
|
603
|
+
);
|
|
604
|
+
const counts = chalk.dim(
|
|
605
|
+
` ${visible.length} of ${this.props.options.length} shown · ${this.selected.size} selected`,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
return `${this.props.promptText}${errorLine}
|
|
609
|
+
${filterLine}
|
|
610
|
+
${optionsBlock}
|
|
611
|
+
${counts}
|
|
612
|
+
${hint}`;
|
|
186
613
|
}
|
|
187
614
|
}
|
|
188
615
|
|
|
@@ -311,6 +738,8 @@ ${inputDisplay}`;
|
|
|
311
738
|
const validationResult = this.props.validate(this.value);
|
|
312
739
|
if (validationResult) {
|
|
313
740
|
this.errorText = validationResult;
|
|
741
|
+
this.requestLayout();
|
|
742
|
+
return;
|
|
314
743
|
} else {
|
|
315
744
|
return this.terminal?.resolve({
|
|
316
745
|
data: this.value,
|
|
@@ -326,12 +755,16 @@ ${inputDisplay}`;
|
|
|
326
755
|
}
|
|
327
756
|
if (keyInfo.name === 'backspace') {
|
|
328
757
|
this.value = this.value.slice(0, -1);
|
|
758
|
+
this.errorText = undefined;
|
|
329
759
|
} else if (keyInfo.name?.length === 1) {
|
|
330
760
|
this.value += input;
|
|
761
|
+
this.errorText = undefined;
|
|
331
762
|
} else if (keyInfo.name === 'space') {
|
|
332
763
|
this.value += ' ';
|
|
764
|
+
this.errorText = undefined;
|
|
333
765
|
} else if (input !== undefined) {
|
|
334
766
|
this.value += input;
|
|
767
|
+
this.errorText = undefined;
|
|
335
768
|
}
|
|
336
769
|
this.requestLayout();
|
|
337
770
|
});
|
package/src/ui/lib.ts
CHANGED
|
@@ -169,6 +169,26 @@ type Prompted<T> =
|
|
|
169
169
|
status: 'submitted';
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Module-level counter that tracks the cumulative line count of submitted
|
|
174
|
+
* prompts. A command opts in to "erase the trail at the very end" by calling
|
|
175
|
+
* {@link clearPromptTrail} right before printing its final summary.
|
|
176
|
+
*/
|
|
177
|
+
let promptTrailLineCount = 0;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Walk the cursor up by however many lines submitted prompts have written, and
|
|
181
|
+
* erase from there to the end of the screen. Resets the counter. No-op on a
|
|
182
|
+
* non-TTY stdout (e.g. when output is piped). Vanished prompts don't
|
|
183
|
+
* contribute, so calling this only erases the dimmed trail.
|
|
184
|
+
*/
|
|
185
|
+
export function clearPromptTrail(): void {
|
|
186
|
+
if (process.stdout.isTTY && promptTrailLineCount > 0) {
|
|
187
|
+
process.stdout.write(cursor.up(promptTrailLineCount) + erase.down());
|
|
188
|
+
}
|
|
189
|
+
promptTrailLineCount = 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
export class Terminal implements ITerminal {
|
|
173
193
|
private text = '';
|
|
174
194
|
private status: 'idle' | 'submitted' | 'aborted' = 'idle';
|
|
@@ -254,6 +274,23 @@ export class Terminal implements ITerminal {
|
|
|
254
274
|
this.stdin.removeListener('keypress', keypress);
|
|
255
275
|
if (this.stdin.isTTY) setRawModeWindowsFriendly(this.stdin, false);
|
|
256
276
|
this.closable.close();
|
|
277
|
+
// Track rows the cursor advanced past so a command can erase the whole
|
|
278
|
+
// prompt trail at the end. Counts each \n plus wraps for lines wider than
|
|
279
|
+
// the terminal — mirrors `clear()`'s formula so wrapped output is still
|
|
280
|
+
// fully cleared. Skip vanished output (whitespace-only after the modifier
|
|
281
|
+
// ran).
|
|
282
|
+
if (this.status === 'submitted' && this.text.trim().length > 0) {
|
|
283
|
+
const cols = this.stdout.columns || 0;
|
|
284
|
+
const lines = this.text.split(/\r?\n/);
|
|
285
|
+
let rows = lines.length - 1; // one cursor-down per newline
|
|
286
|
+
if (cols > 0) {
|
|
287
|
+
for (const line of lines) {
|
|
288
|
+
const w = stringWidth(line);
|
|
289
|
+
if (w > 0) rows += Math.floor((w - 1) / cols);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
promptTrailLineCount += rows;
|
|
293
|
+
}
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
result(): Promise<{}> {
|
|
@@ -271,7 +308,10 @@ export class Terminal implements ITerminal {
|
|
|
271
308
|
requestLayout() {
|
|
272
309
|
const string = this.view.fullRender(this.status);
|
|
273
310
|
let realString = string;
|
|
274
|
-
|
|
311
|
+
// Only ensure a trailing newline when there's content. Vanished renders
|
|
312
|
+
// (empty string after modifyOutput) would otherwise inject a phantom line
|
|
313
|
+
// that pushes the next prompt down on each iteration.
|
|
314
|
+
if (realString.length > 0 && !realString.endsWith('\n')) {
|
|
275
315
|
realString += '\n';
|
|
276
316
|
}
|
|
277
317
|
const realText = this.text.endsWith('\n') ? this.text : `${this.text}\n`;
|