@remotedraw/cli 0.1.1 → 0.1.3
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/README.md +189 -49
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +247 -65
- package/dist/index.js +19 -2
- package/dist/setup-wizard.d.ts +9 -1
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +554 -77
- package/dist/update.d.ts +53 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +235 -0
- package/package.json +3 -8
package/dist/setup-wizard.js
CHANGED
|
@@ -118,6 +118,17 @@ export function transitionWizard(state, action, definition) {
|
|
|
118
118
|
}
|
|
119
119
|
return state;
|
|
120
120
|
}
|
|
121
|
+
function completedSteps(state, definition) {
|
|
122
|
+
return definition.fields
|
|
123
|
+
.slice(0, state.fieldIndex)
|
|
124
|
+
.map((field) => {
|
|
125
|
+
const value = state.values[field.key];
|
|
126
|
+
const label = field
|
|
127
|
+
.choices?.(state.values)
|
|
128
|
+
.find((option) => option.value === value)?.label;
|
|
129
|
+
return [field.label, label ?? value];
|
|
130
|
+
});
|
|
131
|
+
}
|
|
121
132
|
export function wizardSemanticView(state, definition) {
|
|
122
133
|
if (state.screen === "field") {
|
|
123
134
|
const field = activeField(state, definition);
|
|
@@ -133,6 +144,8 @@ export function wizardSemanticView(state, definition) {
|
|
|
133
144
|
activeLabel: field.label,
|
|
134
145
|
value: state.values[field.key],
|
|
135
146
|
valueIsDefault: field.kind === "text" && !state.editedFields.includes(field.key),
|
|
147
|
+
valuePreview: field.kind === "text" ? field.preview?.(state.values) : undefined,
|
|
148
|
+
completed: completedSteps(state, definition),
|
|
136
149
|
choices,
|
|
137
150
|
selectedChoice: state.values[field.key],
|
|
138
151
|
selectedDetails: selectedChoice?.details,
|
|
@@ -169,6 +182,7 @@ export function wizardSemanticView(state, definition) {
|
|
|
169
182
|
return {
|
|
170
183
|
screen: state.screen,
|
|
171
184
|
title: "Projectbestanden maken?",
|
|
185
|
+
review: definition.review(state.values),
|
|
172
186
|
choices: [
|
|
173
187
|
{ value: "no", label: "Nee, annuleren" },
|
|
174
188
|
{ value: "yes", label: "Ja, project maken" },
|
|
@@ -196,39 +210,86 @@ export function wizardSemanticView(state, definition) {
|
|
|
196
210
|
}
|
|
197
211
|
export async function runSetupWizard(options) {
|
|
198
212
|
let state = initialWizardState(options.definition);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
const animated = options.terminal.color;
|
|
214
|
+
// Monotonic so the wordmark, caret, and spinner never jump backwards.
|
|
215
|
+
let tick = IDLE_FRAME;
|
|
216
|
+
const paint = (frame) => {
|
|
217
|
+
options.terminal.write(renderWizard(state, options.definition, options.terminal, frame));
|
|
218
|
+
};
|
|
219
|
+
const play = async (frames, delay, from) => {
|
|
220
|
+
if (from != null)
|
|
221
|
+
tick = from;
|
|
222
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
223
|
+
if (options.terminal.hasPendingKey?.() === true)
|
|
224
|
+
break;
|
|
225
|
+
paint(from != null ? tick + frame : (tick += 1));
|
|
226
|
+
if (frame < frames - 1)
|
|
227
|
+
await pause(delay);
|
|
228
|
+
}
|
|
229
|
+
if (from != null)
|
|
230
|
+
tick = from + frames - 1;
|
|
231
|
+
};
|
|
232
|
+
/** Waits for a key while keeping the frame alive, then goes quiet. */
|
|
233
|
+
const nextKey = async () => {
|
|
234
|
+
const readWithin = options.terminal.readKeyWithin;
|
|
235
|
+
if (animated && readWithin) {
|
|
236
|
+
for (let idle = 0; idle < IDLE_TICKS; idle += 1) {
|
|
237
|
+
const key = await readWithin(IDLE_TICK_MS);
|
|
238
|
+
if (key)
|
|
239
|
+
return key;
|
|
240
|
+
tick += 1;
|
|
241
|
+
paint(tick);
|
|
207
242
|
}
|
|
208
243
|
}
|
|
244
|
+
return await options.terminal.readKey();
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
await options.terminal.enter();
|
|
248
|
+
if (animated)
|
|
249
|
+
await play(INTRO_FRAMES, INTRO_FRAME_MS, 0);
|
|
250
|
+
let previousStep = stepSignature(state);
|
|
209
251
|
while (true) {
|
|
210
|
-
|
|
252
|
+
const currentStep = stepSignature(state);
|
|
253
|
+
if (animated && currentStep !== previousStep) {
|
|
254
|
+
await play(STEP_FRAMES, STEP_FRAME_MS);
|
|
255
|
+
}
|
|
256
|
+
previousStep = currentStep;
|
|
257
|
+
paint(tick);
|
|
211
258
|
if (state.screen === "executing") {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
259
|
+
let running = true;
|
|
260
|
+
const execution = options
|
|
261
|
+
.execute(state.values)
|
|
262
|
+
.then((result) => ({ ok: true, result }), (error) => ({ ok: false, error }))
|
|
263
|
+
.finally(() => {
|
|
264
|
+
running = false;
|
|
265
|
+
});
|
|
266
|
+
if (animated) {
|
|
267
|
+
while (running) {
|
|
268
|
+
tick += 1;
|
|
269
|
+
paint(tick);
|
|
270
|
+
await pause(SPINNER_FRAME_MS);
|
|
271
|
+
}
|
|
215
272
|
}
|
|
216
|
-
|
|
217
|
-
|
|
273
|
+
const outcome = await execution;
|
|
274
|
+
state = outcome.ok
|
|
275
|
+
? transitionWizard(state, { type: "executionSucceeded", ...outcome.result }, options.definition)
|
|
276
|
+
: transitionWizard(state, {
|
|
218
277
|
type: "executionFailed",
|
|
219
|
-
message: `${error instanceof Error ? error.message : String(error)}\nControleer de doelmap en de toegangsrechten.`,
|
|
278
|
+
message: `${outcome.error instanceof Error ? outcome.error.message : String(outcome.error)}\nControleer de doelmap en de toegangsrechten.`,
|
|
220
279
|
}, options.definition);
|
|
221
|
-
}
|
|
222
280
|
continue;
|
|
223
281
|
}
|
|
224
282
|
if (state.screen === "success" || state.screen === "cancelled") {
|
|
283
|
+
if (animated && state.screen === "success") {
|
|
284
|
+
await play(SUCCESS_FRAMES, SUCCESS_FRAME_MS);
|
|
285
|
+
}
|
|
225
286
|
return { exitCode: 0, state };
|
|
226
287
|
}
|
|
227
288
|
if (state.screen === "error")
|
|
228
289
|
return { exitCode: 1, state };
|
|
229
290
|
if (state.screen === "interrupted")
|
|
230
291
|
return { exitCode: 130, state };
|
|
231
|
-
const key = await
|
|
292
|
+
const key = await nextKey();
|
|
232
293
|
const selectedDetails = wizardSemanticView(state, options.definition).selectedDetails;
|
|
233
294
|
if (key.name === "d" && selectedDetails) {
|
|
234
295
|
if (!options.terminal.openUrl) {
|
|
@@ -287,8 +348,29 @@ export function actionForKey(key, state) {
|
|
|
287
348
|
}
|
|
288
349
|
return { type: "character", value: "" };
|
|
289
350
|
}
|
|
290
|
-
|
|
291
|
-
|
|
351
|
+
const INK = {
|
|
352
|
+
accent: { rgb: [56, 189, 248], code: "36" },
|
|
353
|
+
glow: { rgb: [186, 230, 253], code: "96" },
|
|
354
|
+
brand: { rgb: [129, 140, 248], code: "94" },
|
|
355
|
+
muted: { rgb: [124, 134, 150], code: "2" },
|
|
356
|
+
success: { rgb: [52, 211, 153], code: "32" },
|
|
357
|
+
danger: { rgb: [248, 113, 113], code: "31" },
|
|
358
|
+
warn: { rgb: [251, 191, 36], code: "33" },
|
|
359
|
+
};
|
|
360
|
+
function ink(terminal, tone, value, heavy = false) {
|
|
361
|
+
if (!terminal.color || value.length === 0)
|
|
362
|
+
return value;
|
|
363
|
+
const entry = INK[tone];
|
|
364
|
+
const sequence = terminal.trueColor
|
|
365
|
+
? truecolor(entry.rgb)
|
|
366
|
+
: `\x1b[${entry.code}m`;
|
|
367
|
+
return `${heavy ? "\x1b[1m" : ""}${sequence}${value}\x1b[0m`;
|
|
368
|
+
}
|
|
369
|
+
function strong(terminal, value) {
|
|
370
|
+
return terminal.color && value.length > 0 ? `\x1b[1m${value}\x1b[0m` : value;
|
|
371
|
+
}
|
|
372
|
+
function truecolor(rgb) {
|
|
373
|
+
return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
292
374
|
}
|
|
293
375
|
function hyperlink(enabled, url, label) {
|
|
294
376
|
return enabled ? `\x1b]8;;${url}\x1b\\${label}\x1b]8;;\x1b\\` : label;
|
|
@@ -346,102 +428,463 @@ function wrapText(value, width) {
|
|
|
346
428
|
lines.push(line);
|
|
347
429
|
return lines;
|
|
348
430
|
}
|
|
431
|
+
function cardLines(terminal, title, rows, width) {
|
|
432
|
+
const border = (value) => ink(terminal, "accent", value);
|
|
433
|
+
const header = `─ ${title} `;
|
|
434
|
+
const lines = [
|
|
435
|
+
`${border("╭")}${ink(terminal, "glow", header, true)}${border("─".repeat(Math.max(0, width + 2 - header.length)))}${border("╮")}`,
|
|
436
|
+
];
|
|
437
|
+
for (const row of rows) {
|
|
438
|
+
const fitted = row.text.length > width
|
|
439
|
+
? { text: `${row.text.slice(0, Math.max(1, width - 1))}…` }
|
|
440
|
+
: row;
|
|
441
|
+
lines.push(`${border("│")} ${fitted.rendered ?? fitted.text}${" ".repeat(Math.max(0, width - fitted.text.length))} ${border("│")}`);
|
|
442
|
+
}
|
|
443
|
+
lines.push(`${border("╰")}${border("─".repeat(width + 2))}${border("╯")}`);
|
|
444
|
+
return lines;
|
|
445
|
+
}
|
|
349
446
|
function renderChoiceDetails(details, terminal) {
|
|
350
447
|
const width = Math.max(20, Math.min(72, terminal.columns - 4));
|
|
351
|
-
const border = (value) => paint(terminal.color, "36", value);
|
|
352
|
-
const contentLine = (value = "", rendered = value) => `${border("│")} ${rendered}${" ".repeat(Math.max(0, width - value.length))} ${border("│")}`;
|
|
353
448
|
const button = `[ ${details.docsLabel} ↗ ]`;
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
449
|
+
const rows = [
|
|
450
|
+
...wrapText(details.summary, width).map((line) => ({
|
|
451
|
+
text: line,
|
|
452
|
+
rendered: strong(terminal, line),
|
|
453
|
+
})),
|
|
454
|
+
{ text: "" },
|
|
455
|
+
...wrapText(details.explanation, width).map((line) => ({ text: line })),
|
|
456
|
+
{ text: "" },
|
|
457
|
+
{
|
|
458
|
+
text: button,
|
|
459
|
+
rendered: hyperlink(terminal.hyperlinks === true, details.docsUrl, ink(terminal, "glow", button, true)),
|
|
460
|
+
},
|
|
362
461
|
];
|
|
363
462
|
if (terminal.hyperlinks !== true) {
|
|
364
|
-
|
|
463
|
+
rows.push(...wrapText(details.docsUrl, width).map((line) => ({
|
|
464
|
+
text: line,
|
|
465
|
+
rendered: ink(terminal, "muted", line),
|
|
466
|
+
})));
|
|
365
467
|
}
|
|
366
|
-
|
|
367
|
-
return lines;
|
|
468
|
+
return cardLines(terminal, "Keuzehulp", rows, width);
|
|
368
469
|
}
|
|
369
470
|
const BANNER_COLORS = ["36", "96", "94", "34"];
|
|
370
|
-
const
|
|
371
|
-
|
|
471
|
+
const BANNER_GRADIENT = [
|
|
472
|
+
[45, 212, 191],
|
|
473
|
+
[56, 189, 248],
|
|
474
|
+
[99, 130, 246],
|
|
475
|
+
[129, 140, 248],
|
|
476
|
+
];
|
|
477
|
+
const BANNER_GHOST = [42, 51, 66];
|
|
478
|
+
const BANNER_CREST = [236, 254, 255];
|
|
479
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
480
|
+
const SPARKLES = ["✦", "✧", "✶", "✧"];
|
|
481
|
+
const INTRO_FRAMES = 28;
|
|
482
|
+
const INTRO_FRAME_MS = 22;
|
|
483
|
+
const IDLE_FRAME = INTRO_FRAMES - 1;
|
|
484
|
+
const REVEAL_SPEED = 0.06;
|
|
485
|
+
const CREST_WIDTH = 0.22;
|
|
486
|
+
const SHIMMER_WIDTH = 0.2;
|
|
487
|
+
const SHIMMER_SPAN = 14;
|
|
488
|
+
const SHIMMER_PERIOD = 52;
|
|
489
|
+
const STEP_FRAMES = 4;
|
|
490
|
+
const STEP_FRAME_MS = 26;
|
|
491
|
+
const SPINNER_FRAME_MS = 80;
|
|
492
|
+
const SUCCESS_FRAMES = 6;
|
|
493
|
+
const SUCCESS_FRAME_MS = 55;
|
|
494
|
+
const IDLE_TICK_MS = 150;
|
|
495
|
+
const IDLE_TICKS = 60;
|
|
372
496
|
function pause(milliseconds) {
|
|
373
497
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
374
498
|
}
|
|
375
|
-
|
|
499
|
+
function stepSignature(state) {
|
|
500
|
+
return `${state.screen}:${state.fieldIndex}`;
|
|
501
|
+
}
|
|
502
|
+
/** Steady while the frame is still settling; blinks once the wizard idles. */
|
|
503
|
+
function caretGlyph(frame) {
|
|
504
|
+
const idleTicks = frame - IDLE_FRAME;
|
|
505
|
+
if (idleTicks <= 0)
|
|
506
|
+
return "▌";
|
|
507
|
+
return Math.floor(idleTicks / 3) % 2 === 0 ? "▌" : " ";
|
|
508
|
+
}
|
|
509
|
+
const WORDMARK = "RemoteDraw";
|
|
510
|
+
/** Pixel rows per glyph; rendered as solid blocks or half blocks. */
|
|
511
|
+
const BOLD_GLYPHS = {
|
|
512
|
+
R: ["######.", "##...##", "##...##", "######.", "##..##.", "##...##"],
|
|
513
|
+
D: ["######.", "##...##", "##...##", "##...##", "##...##", "######."],
|
|
514
|
+
e: ["......", ".####.", "##..##", "######", "##....", ".####."],
|
|
515
|
+
m: [
|
|
516
|
+
"..........",
|
|
517
|
+
"##########",
|
|
518
|
+
"##..##..##",
|
|
519
|
+
"##..##..##",
|
|
520
|
+
"##..##..##",
|
|
521
|
+
"##..##..##",
|
|
522
|
+
],
|
|
523
|
+
o: ["......", ".####.", "##..##", "##..##", "##..##", ".####."],
|
|
524
|
+
t: [".##..", ".##..", "#####", ".##..", ".##..", "..###"],
|
|
525
|
+
r: ["......", "##.###", "####..", "##....", "##....", "##...."],
|
|
526
|
+
a: ["......", ".####.", "....##", ".#####", "##..##", ".#####"],
|
|
527
|
+
w: ["........", "##....##", "##....##", "##.##.##", "##.##.##", ".##..##."],
|
|
528
|
+
};
|
|
529
|
+
const COMPACT_GLYPHS = {
|
|
530
|
+
R: ["####.", "#...#", "#...#", "####.", "#..#.", "#...#"],
|
|
531
|
+
D: ["####.", "#...#", "#...#", "#...#", "#...#", "####."],
|
|
532
|
+
e: [".....", ".###.", "#...#", "#####", "#....", ".###."],
|
|
533
|
+
m: [".....", "#####", "#.#.#", "#.#.#", "#.#.#", "#.#.#"],
|
|
534
|
+
o: [".....", ".###.", "#...#", "#...#", "#...#", ".###."],
|
|
535
|
+
t: [".#...", ".#...", "####.", ".#...", ".#...", "..##."],
|
|
536
|
+
r: [".....", "#.###", "##...", "#....", "#....", "#...."],
|
|
537
|
+
a: [".....", ".###.", "....#", ".####", "#...#", ".####"],
|
|
538
|
+
w: [".....", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#."],
|
|
539
|
+
};
|
|
540
|
+
const BOLD_WIDTH = wordmarkWidth(BOLD_GLYPHS);
|
|
541
|
+
const COMPACT_WIDTH = wordmarkWidth(COMPACT_GLYPHS);
|
|
542
|
+
const BANNER_CACHE = new Map();
|
|
543
|
+
function wordmarkWidth(glyphs) {
|
|
544
|
+
return [...WORDMARK].reduce((total, character) => total + (glyphs[character]?.[0]?.length ?? 0) + 1, -1);
|
|
545
|
+
}
|
|
546
|
+
/** Full pixel rows: one text row per pixel row, solid blocks. */
|
|
547
|
+
function solidWordmark(glyphs) {
|
|
548
|
+
return Array.from({ length: 6 }, (_unused, row) => [...WORDMARK]
|
|
549
|
+
.map((character) => [...(glyphs[character]?.[row] ?? "")]
|
|
550
|
+
.map((pixel) => (pixel === "#" ? "█" : " "))
|
|
551
|
+
.join(""))
|
|
552
|
+
.join(" "));
|
|
553
|
+
}
|
|
554
|
+
/** Half-height: two pixel rows folded into one text row. */
|
|
555
|
+
function halfWordmark(glyphs) {
|
|
556
|
+
return Array.from({ length: 3 }, (_unused, row) => [...WORDMARK]
|
|
557
|
+
.map((character) => {
|
|
558
|
+
const top = glyphs[character]?.[row * 2] ?? "";
|
|
559
|
+
const bottom = glyphs[character]?.[row * 2 + 1] ?? "";
|
|
560
|
+
return [...top]
|
|
561
|
+
.map((pixel, column) => {
|
|
562
|
+
const upper = pixel === "#";
|
|
563
|
+
const lower = bottom[column] === "#";
|
|
564
|
+
return upper && lower ? "█" : upper ? "▀" : lower ? "▄" : " ";
|
|
565
|
+
})
|
|
566
|
+
.join("");
|
|
567
|
+
})
|
|
568
|
+
.join(" "));
|
|
569
|
+
}
|
|
570
|
+
function wordmarkRows(level, columns) {
|
|
571
|
+
const bold = columns >= BOLD_WIDTH + 2;
|
|
572
|
+
const key = `${level}:${bold}`;
|
|
573
|
+
const cached = BANNER_CACHE.get(key);
|
|
574
|
+
if (cached)
|
|
575
|
+
return cached;
|
|
576
|
+
const glyphs = bold ? BOLD_GLYPHS : COMPACT_GLYPHS;
|
|
577
|
+
const rows = level === 2 ? solidWordmark(glyphs) : halfWordmark(glyphs);
|
|
578
|
+
BANNER_CACHE.set(key, rows);
|
|
579
|
+
return rows;
|
|
580
|
+
}
|
|
581
|
+
function bannerLines(terminal, frame, level) {
|
|
582
|
+
if (level === 0)
|
|
583
|
+
return [ink(terminal, "glow", WORDMARK, true)];
|
|
584
|
+
const rows = wordmarkRows(level, terminal.columns);
|
|
585
|
+
return rows.map((row) => paintWordmark(terminal, row, frame));
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The wordmark materialises: a crest sweeps left to right during the intro,
|
|
589
|
+
* turning dim scaffolding into lit blocks. Once settled, a slower highlight
|
|
590
|
+
* passes over it every few seconds while the wizard waits for input.
|
|
591
|
+
*/
|
|
592
|
+
function paintWordmark(terminal, row, frame) {
|
|
593
|
+
if (!terminal.color)
|
|
594
|
+
return row;
|
|
595
|
+
const characters = [...row];
|
|
596
|
+
const span = Math.max(1, characters.length - 1);
|
|
597
|
+
const crest = frame * REVEAL_SPEED - 0.3;
|
|
598
|
+
const idleTicks = frame - IDLE_FRAME;
|
|
599
|
+
const shimmerPhase = idleTicks > 0 ? idleTicks % SHIMMER_PERIOD : -1;
|
|
600
|
+
const shimmer = shimmerPhase >= 0 && shimmerPhase < SHIMMER_SPAN
|
|
601
|
+
? (shimmerPhase / SHIMMER_SPAN) * 1.35 - 0.18
|
|
602
|
+
: undefined;
|
|
603
|
+
let rendered = "";
|
|
604
|
+
let openSequence = "";
|
|
605
|
+
let buffered = "";
|
|
606
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
607
|
+
const position = index / span;
|
|
608
|
+
const sequence = wordmarkSequence(terminal, position, crest, shimmer);
|
|
609
|
+
if (sequence !== openSequence) {
|
|
610
|
+
if (buffered)
|
|
611
|
+
rendered += `${openSequence}${buffered}`;
|
|
612
|
+
openSequence = sequence;
|
|
613
|
+
buffered = "";
|
|
614
|
+
}
|
|
615
|
+
buffered += characters[index];
|
|
616
|
+
}
|
|
617
|
+
return `${rendered}${openSequence}${buffered}\x1b[0m`;
|
|
618
|
+
}
|
|
619
|
+
function wordmarkSequence(terminal, position, crest, shimmer) {
|
|
620
|
+
const arrived = crest - position;
|
|
621
|
+
const highlight = Math.max(arrived >= 0 && arrived < CREST_WIDTH ? 1 - arrived / CREST_WIDTH : 0, shimmer == null
|
|
622
|
+
? 0
|
|
623
|
+
: Math.max(0, 1 - Math.abs(position - shimmer) / SHIMMER_WIDTH));
|
|
624
|
+
if (!terminal.trueColor) {
|
|
625
|
+
if (arrived < 0)
|
|
626
|
+
return "\x1b[2m\x1b[34m";
|
|
627
|
+
if (highlight > 0.45)
|
|
628
|
+
return "\x1b[96m";
|
|
629
|
+
return `\x1b[${BANNER_COLORS[Math.floor(position * 3.5) % BANNER_COLORS.length]}m`;
|
|
630
|
+
}
|
|
631
|
+
if (arrived < 0)
|
|
632
|
+
return truecolor(BANNER_GHOST);
|
|
633
|
+
const scaled = Math.min(0.999, position) * (BANNER_GRADIENT.length - 1);
|
|
634
|
+
const index = Math.floor(scaled);
|
|
635
|
+
const base = mixRgb(BANNER_GRADIENT[index], BANNER_GRADIENT[index + 1], scaled - index);
|
|
636
|
+
return truecolor(highlight > 0
|
|
637
|
+
? mixRgb(base, BANNER_CREST, highlight * highlight * 0.9)
|
|
638
|
+
: base);
|
|
639
|
+
}
|
|
640
|
+
function mixRgb(from, to, amount) {
|
|
641
|
+
const ratio = Math.min(1, Math.max(0, amount));
|
|
642
|
+
return [
|
|
643
|
+
Math.round(from[0] + (to[0] - from[0]) * ratio),
|
|
644
|
+
Math.round(from[1] + (to[1] - from[1]) * ratio),
|
|
645
|
+
Math.round(from[2] + (to[2] - from[2]) * ratio),
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
function stepRail(terminal, current, total, frame) {
|
|
649
|
+
const cells = [];
|
|
650
|
+
for (let index = 0; index < total; index += 1) {
|
|
651
|
+
if (index > 0) {
|
|
652
|
+
cells.push(ink(terminal, index < current ? "accent" : "muted", "─"));
|
|
653
|
+
}
|
|
654
|
+
if (index < current - 1) {
|
|
655
|
+
cells.push(ink(terminal, "accent", "●"));
|
|
656
|
+
}
|
|
657
|
+
else if (index === current - 1) {
|
|
658
|
+
cells.push(ink(terminal, frame % 2 === 0 ? "glow" : "accent", "◉", true));
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
cells.push(ink(terminal, "muted", "○"));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return cells.join("");
|
|
665
|
+
}
|
|
666
|
+
function screenGlyph(terminal, screen, frame) {
|
|
667
|
+
if (screen === "executing") {
|
|
668
|
+
return ink(terminal, "accent", SPINNER[frame % SPINNER.length]);
|
|
669
|
+
}
|
|
670
|
+
if (screen === "success")
|
|
671
|
+
return ink(terminal, "success", "✓", true);
|
|
672
|
+
if (screen === "error")
|
|
673
|
+
return ink(terminal, "danger", "✗", true);
|
|
674
|
+
if (screen === "confirm")
|
|
675
|
+
return ink(terminal, "warn", "?", true);
|
|
676
|
+
if (screen === "cancelled" || screen === "interrupted") {
|
|
677
|
+
return ink(terminal, "warn", "○");
|
|
678
|
+
}
|
|
679
|
+
return ink(terminal, "accent", "▸");
|
|
680
|
+
}
|
|
681
|
+
function choiceLines(terminal, choices, selectedValue, withHints) {
|
|
682
|
+
const lines = [];
|
|
683
|
+
for (const choice of choices) {
|
|
684
|
+
const selected = choice.value === selectedValue;
|
|
685
|
+
lines.push(selected
|
|
686
|
+
? `${ink(terminal, "glow", "❯")} ${ink(terminal, "glow", "◉")} ${strong(terminal, choice.label)}`
|
|
687
|
+
: ` ${ink(terminal, "muted", "○")} ${choice.label}`);
|
|
688
|
+
if (withHints && choice.hint) {
|
|
689
|
+
lines.push(` ${ink(terminal, "muted", choice.hint)}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return lines;
|
|
693
|
+
}
|
|
694
|
+
function progressBar(terminal, frame, size) {
|
|
695
|
+
const head = frame % (size + 6);
|
|
696
|
+
const cells = Array.from({ length: size }, (_unused, index) => {
|
|
697
|
+
const distance = head - index;
|
|
698
|
+
if (distance < 0 || distance > 5)
|
|
699
|
+
return ink(terminal, "muted", "▱");
|
|
700
|
+
return ink(terminal, distance < 2 ? "glow" : "accent", "▰");
|
|
701
|
+
});
|
|
702
|
+
return ` ${cells.join("")}`;
|
|
703
|
+
}
|
|
704
|
+
function controlLines(terminal, controls, width) {
|
|
705
|
+
const separator = ink(terminal, "muted", " · ");
|
|
706
|
+
const lines = [];
|
|
707
|
+
let rendered = "";
|
|
708
|
+
let plainLength = 0;
|
|
709
|
+
for (const control of controls) {
|
|
710
|
+
if (plainLength > 0 && plainLength + 3 + control.length > width) {
|
|
711
|
+
lines.push(rendered);
|
|
712
|
+
rendered = "";
|
|
713
|
+
plainLength = 0;
|
|
714
|
+
}
|
|
715
|
+
if (plainLength === 0) {
|
|
716
|
+
rendered = controlChip(terminal, control);
|
|
717
|
+
plainLength = control.length;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
rendered += `${separator}${controlChip(terminal, control)}`;
|
|
721
|
+
plainLength += 3 + control.length;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (rendered)
|
|
725
|
+
lines.push(rendered);
|
|
726
|
+
return lines;
|
|
727
|
+
}
|
|
728
|
+
function controlChip(terminal, control) {
|
|
729
|
+
const gap = control.indexOf(" ");
|
|
730
|
+
if (gap < 0)
|
|
731
|
+
return ink(terminal, "glow", control);
|
|
732
|
+
return `${ink(terminal, "glow", control.slice(0, gap))}${ink(terminal, "muted", control.slice(gap))}`;
|
|
733
|
+
}
|
|
734
|
+
function labelColumn(pairs) {
|
|
735
|
+
return pairs.reduce((widest, [label]) => Math.max(widest, label.length), 0);
|
|
736
|
+
}
|
|
737
|
+
export function renderWizard(state, definition, terminal, frame = 0) {
|
|
376
738
|
const view = wizardSemanticView(state, definition);
|
|
377
739
|
const narrow = terminal.columns < 60;
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
740
|
+
const rule = Math.max(12, Math.min(terminal.columns - 1, 72));
|
|
741
|
+
const lines = fitWizard(view, terminal, frame, narrow, rule);
|
|
742
|
+
const home = terminal.color ? "\x1b[H" : "\x1b[2J\x1b[H";
|
|
743
|
+
const clearLine = terminal.color ? "\x1b[K" : "";
|
|
744
|
+
const clearBelow = terminal.color ? "\x1b[J" : "";
|
|
745
|
+
return `${home}${lines
|
|
746
|
+
.map((line) => `${line}${clearLine}`)
|
|
747
|
+
.join("\n")}${clearBelow}\n`;
|
|
748
|
+
}
|
|
749
|
+
/** Richest first: the last candidate that still fits the window wins. */
|
|
750
|
+
const DENSITY_LADDER = [
|
|
751
|
+
{ banner: 2, trail: true, hints: true, details: true },
|
|
752
|
+
{ banner: 1, trail: true, hints: true, details: true },
|
|
753
|
+
{ banner: 1, trail: false, hints: true, details: true },
|
|
754
|
+
{ banner: 1, trail: false, hints: false, details: true },
|
|
755
|
+
{ banner: 0, trail: false, hints: false, details: true },
|
|
756
|
+
{ banner: 0, trail: false, hints: false, details: false },
|
|
757
|
+
];
|
|
758
|
+
function fitWizard(view, terminal, frame, narrow, rule) {
|
|
759
|
+
const widest = terminal.columns >= BOLD_WIDTH + 2
|
|
760
|
+
? 2
|
|
761
|
+
: terminal.columns >= COMPACT_WIDTH + 2
|
|
762
|
+
? 1
|
|
763
|
+
: 0;
|
|
764
|
+
const limit = terminal.rows == null ? undefined : terminal.rows - 1;
|
|
765
|
+
let lines = [];
|
|
766
|
+
for (const candidate of DENSITY_LADDER) {
|
|
767
|
+
lines = composeWizard(view, terminal, frame, narrow, rule, {
|
|
768
|
+
banner: Math.min(candidate.banner, widest),
|
|
769
|
+
trail: candidate.trail && !narrow,
|
|
770
|
+
hints: candidate.hints && !narrow,
|
|
771
|
+
details: candidate.details,
|
|
772
|
+
});
|
|
773
|
+
if (limit == null || lines.length <= limit)
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
return lines;
|
|
777
|
+
}
|
|
778
|
+
function composeWizard(view, terminal, frame, narrow, rule, density) {
|
|
387
779
|
const lines = [
|
|
388
|
-
|
|
389
|
-
|
|
780
|
+
...bannerLines(terminal, frame, density.banner),
|
|
781
|
+
ink(terminal, "muted", density.banner > 0
|
|
782
|
+
? "telefoon → canvas · projectsetup"
|
|
783
|
+
: "telefoon → canvas"),
|
|
390
784
|
"",
|
|
391
785
|
];
|
|
392
786
|
if (view.progress) {
|
|
393
|
-
|
|
787
|
+
const step = `Stap ${view.progress.current}/${view.progress.total}`;
|
|
788
|
+
lines.push(narrow
|
|
789
|
+
? `${ink(terminal, "muted", step)} · ${strong(terminal, view.title)}`
|
|
790
|
+
: `${stepRail(terminal, view.progress.current, view.progress.total, frame)} ${ink(terminal, "muted", step)} ${strong(terminal, view.title)}`, "");
|
|
394
791
|
}
|
|
395
792
|
else {
|
|
396
|
-
lines.push(view.
|
|
793
|
+
lines.push(`${screenGlyph(terminal, view.screen, frame)} ${strong(terminal, view.title)}`, "");
|
|
794
|
+
}
|
|
795
|
+
if (density.trail && view.completed && view.completed.length > 0) {
|
|
796
|
+
const column = labelColumn(view.completed);
|
|
797
|
+
for (const [label, value] of view.completed) {
|
|
798
|
+
lines.push(`${ink(terminal, "success", "✓")} ${ink(terminal, "muted", label.padEnd(column))} ${value}`);
|
|
799
|
+
}
|
|
800
|
+
lines.push("");
|
|
397
801
|
}
|
|
398
802
|
if (view.activeLabel) {
|
|
803
|
+
lines.push(ink(terminal, "accent", view.activeLabel, true));
|
|
399
804
|
if (view.choices) {
|
|
400
|
-
lines.push(
|
|
401
|
-
for (const choice of view.choices) {
|
|
402
|
-
const selected = choice.value === view.selectedChoice;
|
|
403
|
-
lines.push(paint(terminal.color && selected, "1;96", `${selected ? ">" : " "} [${selected ? "x" : " "}] ${choice.label}`));
|
|
404
|
-
if (!narrow && choice.hint)
|
|
405
|
-
lines.push(` ${choice.hint}`);
|
|
406
|
-
}
|
|
805
|
+
lines.push(...choiceLines(terminal, view.choices, view.selectedChoice, density.hints));
|
|
407
806
|
}
|
|
408
807
|
else {
|
|
409
|
-
lines.push(
|
|
410
|
-
|
|
411
|
-
|
|
808
|
+
lines.push(`${ink(terminal, "accent", "│")} ${ink(terminal, view.valueIsDefault ? "muted" : "glow", view.value || "_", !view.valueIsDefault)}${ink(terminal, "glow", caretGlyph(frame))}${view.valueIsDefault ? ` ${ink(terminal, "muted", "(standaard)")}` : ""}`);
|
|
809
|
+
if (view.valuePreview) {
|
|
810
|
+
lines.push(`${ink(terminal, "muted", "└")} ${ink(terminal, "muted", view.valuePreview)}`);
|
|
811
|
+
}
|
|
412
812
|
}
|
|
413
|
-
if (view.selectedDetails) {
|
|
813
|
+
if (density.details && view.selectedDetails) {
|
|
414
814
|
lines.push("", ...renderChoiceDetails(view.selectedDetails, terminal));
|
|
415
815
|
}
|
|
416
|
-
if (view.validation)
|
|
417
|
-
lines.push("",
|
|
816
|
+
if (view.validation) {
|
|
817
|
+
lines.push("", ink(terminal, "warn", `⚠ ${view.validation}`));
|
|
818
|
+
}
|
|
418
819
|
}
|
|
419
820
|
if (view.review) {
|
|
420
|
-
|
|
421
|
-
|
|
821
|
+
const column = labelColumn(view.review);
|
|
822
|
+
if (view.screen === "confirm") {
|
|
823
|
+
for (const [label, value] of view.review) {
|
|
824
|
+
lines.push(` ${ink(terminal, "muted", label.padEnd(column))} ${ink(terminal, "muted", value)}`);
|
|
825
|
+
}
|
|
826
|
+
lines.push("");
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
const rows = view.review.map(([label, value]) => ({
|
|
830
|
+
text: `${label.padEnd(column)} ${value}`,
|
|
831
|
+
rendered: `${ink(terminal, "muted", label.padEnd(column))} ${strong(terminal, value)}`,
|
|
832
|
+
}));
|
|
833
|
+
const width = Math.max(24, Math.min(Math.max(20, terminal.columns - 4), rows.reduce((widest, row) => Math.max(widest, row.text.length), 0)));
|
|
834
|
+
lines.push(...cardLines(terminal, "Overzicht", rows, width));
|
|
835
|
+
}
|
|
422
836
|
}
|
|
423
837
|
if (view.choices && !view.activeLabel) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
838
|
+
lines.push(...choiceLines(terminal, view.choices, view.selectedChoice, false));
|
|
839
|
+
}
|
|
840
|
+
if (view.screen === "executing") {
|
|
841
|
+
lines.push(progressBar(terminal, frame, narrow ? 12 : 24), "", ink(terminal, "muted", "Bestanden schrijven, sleutels koppelen en configuratie opslaan…"));
|
|
842
|
+
}
|
|
843
|
+
if (view.createdPath) {
|
|
844
|
+
lines.push(`${ink(terminal, "muted", "Gemaakt in:")} ${view.createdPath}`);
|
|
845
|
+
}
|
|
846
|
+
if (view.nextCommand) {
|
|
847
|
+
lines.push("", ink(terminal, "muted", "Volgende opdracht:"), ` ${ink(terminal, "glow", view.nextCommand, true)}`);
|
|
848
|
+
}
|
|
849
|
+
if (view.screen === "success") {
|
|
850
|
+
lines.push("", `${ink(terminal, "success", SPARKLES[frame % SPARKLES.length])} ${ink(terminal, "muted", "Documentatie: https://docs.remotedraw.com")}`);
|
|
851
|
+
}
|
|
852
|
+
if (view.error) {
|
|
853
|
+
for (const line of view.error.split("\n")) {
|
|
854
|
+
lines.push(ink(terminal, "danger", line));
|
|
427
855
|
}
|
|
428
856
|
}
|
|
429
|
-
if (view.
|
|
430
|
-
lines.push(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (view.error)
|
|
434
|
-
lines.push(view.error);
|
|
435
|
-
if (view.controls.length)
|
|
436
|
-
lines.push("", view.controls.join(" · "));
|
|
437
|
-
return `${lines.join("\n")}\n`;
|
|
857
|
+
if (view.controls.length) {
|
|
858
|
+
lines.push("", ink(terminal, "muted", "─".repeat(rule)), ...controlLines(terminal, view.controls, Math.max(rule, terminal.columns - 1)));
|
|
859
|
+
}
|
|
860
|
+
return lines;
|
|
438
861
|
}
|
|
439
862
|
export function createWizardTerminal(input, output, env) {
|
|
440
863
|
const wasRaw = input.isRaw === true;
|
|
441
864
|
const wasFlowing = input.readableFlowing === true;
|
|
865
|
+
const buffered = [];
|
|
866
|
+
let waiting;
|
|
867
|
+
const onKeypress = (_text, key) => {
|
|
868
|
+
if (waiting) {
|
|
869
|
+
const resolve = waiting;
|
|
870
|
+
waiting = undefined;
|
|
871
|
+
resolve(key);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
buffered.push(key);
|
|
875
|
+
};
|
|
442
876
|
return {
|
|
443
|
-
|
|
877
|
+
get columns() {
|
|
878
|
+
return output.columns || 80;
|
|
879
|
+
},
|
|
880
|
+
get rows() {
|
|
881
|
+
return output.rows || 24;
|
|
882
|
+
},
|
|
444
883
|
color: env.NO_COLOR == null && output.hasColors?.() !== false,
|
|
884
|
+
trueColor: env.NO_COLOR == null &&
|
|
885
|
+
(env.COLORTERM === "truecolor" ||
|
|
886
|
+
env.COLORTERM === "24bit" ||
|
|
887
|
+
output.hasColors?.(16_777_216) === true),
|
|
445
888
|
hyperlinks: env.TERM !== "dumb" && env.NO_HYPERLINKS == null,
|
|
446
889
|
write(value) {
|
|
447
890
|
output.write(value);
|
|
@@ -449,16 +892,50 @@ export function createWizardTerminal(input, output, env) {
|
|
|
449
892
|
enter() {
|
|
450
893
|
emitKeypressEvents(input);
|
|
451
894
|
input.setRawMode?.(true);
|
|
895
|
+
input.on("keypress", onKeypress);
|
|
452
896
|
input.resume();
|
|
453
897
|
output.write("\x1b[?25l");
|
|
454
898
|
},
|
|
899
|
+
hasPendingKey() {
|
|
900
|
+
return buffered.length > 0;
|
|
901
|
+
},
|
|
455
902
|
async readKey() {
|
|
903
|
+
const next = buffered.shift();
|
|
904
|
+
if (next)
|
|
905
|
+
return next;
|
|
906
|
+
return await new Promise((resolve) => {
|
|
907
|
+
waiting = resolve;
|
|
908
|
+
});
|
|
909
|
+
},
|
|
910
|
+
async readKeyWithin(milliseconds) {
|
|
911
|
+
const next = buffered.shift();
|
|
912
|
+
if (next)
|
|
913
|
+
return next;
|
|
456
914
|
return await new Promise((resolve) => {
|
|
457
|
-
|
|
915
|
+
let settled = false;
|
|
916
|
+
const timer = setTimeout(() => {
|
|
917
|
+
if (settled)
|
|
918
|
+
return;
|
|
919
|
+
settled = true;
|
|
920
|
+
if (waiting === listener)
|
|
921
|
+
waiting = undefined;
|
|
922
|
+
resolve(undefined);
|
|
923
|
+
}, milliseconds);
|
|
924
|
+
timer.unref?.();
|
|
925
|
+
const listener = (key) => {
|
|
926
|
+
if (settled)
|
|
927
|
+
return;
|
|
928
|
+
settled = true;
|
|
929
|
+
clearTimeout(timer);
|
|
930
|
+
resolve(key);
|
|
931
|
+
};
|
|
932
|
+
waiting = listener;
|
|
458
933
|
});
|
|
459
934
|
},
|
|
460
935
|
openUrl: openExternalUrl,
|
|
461
936
|
restore() {
|
|
937
|
+
input.off("keypress", onKeypress);
|
|
938
|
+
waiting = undefined;
|
|
462
939
|
if (!wasRaw)
|
|
463
940
|
input.setRawMode?.(false);
|
|
464
941
|
if (!wasFlowing)
|