@typecad/ui 1.0.0-alpha.13 → 1.0.0-alpha.14
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/dist/cli.js +22 -19
- package/dist/preview/host-ui-runtime.js +0 -1
- package/dist/ui-engine/model.js +7 -1
- package/dist/ui-engine/runtime-header/node-draw-body.js +23 -21
- package/dist/ui-engine/shadcn-kit.d.ts +1 -1
- package/dist/ui-engine/shadcn-kit.js +4 -0
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.js +1 -0
- package/dist/wizard/integration-wizard.d.ts +3 -0
- package/dist/wizard/integration-wizard.js +67 -7
- package/dist/wizard/package-writer.d.ts +20 -0
- package/dist/wizard/package-writer.js +79 -0
- package/package.json +4 -4
- package/src/cli.ts +90 -87
- package/src/preview/host-ui-runtime.ts +0 -1
- package/src/ui-engine/model.ts +6 -1
- package/src/ui-engine/runtime-header/node-draw-body.ts +23 -21
- package/src/ui-engine/shadcn-kit.ts +4 -0
- package/src/wizard/index.ts +46 -38
- package/src/wizard/integration-wizard.ts +689 -619
- package/src/wizard/package-writer.ts +98 -0
|
@@ -1,619 +1,689 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// @typecad/ui integration wizard — `npx @typecad/ui --config`.
|
|
3
|
-
//
|
|
4
|
-
// Walks the user through wiring a display (and optional touch controller)
|
|
5
|
-
// into their project's cuttlefish.config.ts: pick a display, answer bus/pin/
|
|
6
|
-
// speed questions with hardware-aware defaults, then splice the resulting
|
|
7
|
-
// `display` section into the config without touching any other section.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import
|
|
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
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
xMin:
|
|
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
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
defaultValue:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
existingTouch.calibration
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
{ label: "
|
|
363
|
-
{ label: "
|
|
364
|
-
]);
|
|
365
|
-
record.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
record.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
record.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
record.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
console.log(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/ui integration wizard — `npx @typecad/ui --config`.
|
|
3
|
+
//
|
|
4
|
+
// Walks the user through wiring a display (and optional touch controller)
|
|
5
|
+
// into their project's cuttlefish.config.ts: pick a display, answer bus/pin/
|
|
6
|
+
// speed questions with hardware-aware defaults, then splice the resulting
|
|
7
|
+
// `display` section into the config without touching any other section.
|
|
8
|
+
// Also adds a `preview` npm script to package.json so the desktop preview
|
|
9
|
+
// renderer starts via `npm run preview`. Offers to create a starter .ui entry
|
|
10
|
+
// file when none exists and prints the next build/flash/library steps at the
|
|
11
|
+
// end.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import {
|
|
18
|
+
createPromptInterface,
|
|
19
|
+
promptConfirm,
|
|
20
|
+
promptInt,
|
|
21
|
+
promptSelect,
|
|
22
|
+
promptText,
|
|
23
|
+
type ReadlineInterface,
|
|
24
|
+
} from "./prompts.js";
|
|
25
|
+
import {
|
|
26
|
+
DISPLAY_CATALOG,
|
|
27
|
+
TOUCH_CATALOG,
|
|
28
|
+
findPinConflicts,
|
|
29
|
+
type DisplayCatalogEntry,
|
|
30
|
+
type TouchCatalogEntry,
|
|
31
|
+
} from "./display-catalog.js";
|
|
32
|
+
import {
|
|
33
|
+
findCuttlefishConfig,
|
|
34
|
+
findSyntaxError,
|
|
35
|
+
readConfigSection,
|
|
36
|
+
readEntryPath,
|
|
37
|
+
renderDisplayProperty,
|
|
38
|
+
upsertDisplaySection,
|
|
39
|
+
type ConfigRecord,
|
|
40
|
+
} from "./config-writer.js";
|
|
41
|
+
import {
|
|
42
|
+
findPackageJson,
|
|
43
|
+
previewScriptCommand,
|
|
44
|
+
readPackageScript,
|
|
45
|
+
upsertPackageScript,
|
|
46
|
+
} from "./package-writer.js";
|
|
47
|
+
import { renderStarterUi } from "./starter-ui.js";
|
|
48
|
+
|
|
49
|
+
/** Keys the wizard manages — anything else found in an existing display
|
|
50
|
+
* section is carried over untouched instead of being silently dropped. */
|
|
51
|
+
const MANAGED_DISPLAY_KEYS: ReadonlySet<string> = new Set([
|
|
52
|
+
"profile", "driver", "bus", "cs", "dc", "rst", "address", "reset",
|
|
53
|
+
"backlight", "spiFrequency", "spiPins", "width", "height",
|
|
54
|
+
"nativeWidth", "nativeHeight", "colorFormat", "displayClass",
|
|
55
|
+
"rotation", "colorOrder", "invertDisplay", "antialias",
|
|
56
|
+
"themeCss", "themeClass", "touch",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function asRecord(value: unknown): ConfigRecord {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as ConfigRecord) : {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function existingNumber(section: ConfigRecord, key: string): number | undefined {
|
|
64
|
+
const value = section[key];
|
|
65
|
+
return typeof value === "number" ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function existingString(section: ConfigRecord, key: string): string | undefined {
|
|
69
|
+
const value = section[key];
|
|
70
|
+
return typeof value === "string" ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Parse "15", "0x38" into a number; undefined when not numeric. */
|
|
74
|
+
function parseNumeric(text: string): number | undefined {
|
|
75
|
+
const trimmed = text.trim().toLowerCase();
|
|
76
|
+
if (/^0x[0-9a-f]+$/.test(trimmed)) return parseInt(trimmed, 16);
|
|
77
|
+
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Calibration {
|
|
82
|
+
xMin: number;
|
|
83
|
+
xMax: number;
|
|
84
|
+
yMin: number;
|
|
85
|
+
yMax: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function asCalibration(value: unknown): Calibration | undefined {
|
|
89
|
+
const record = asRecord(value);
|
|
90
|
+
const { xMin, xMax, yMin, yMax } = record;
|
|
91
|
+
if ([xMin, xMax, yMin, yMax].every((bound) => typeof bound === "number")) {
|
|
92
|
+
return { xMin: xMin as number, xMax: xMax as number, yMin: yMin as number, yMax: yMax as number };
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Ask the four calibration bounds, prefilled with `defaults`. */
|
|
98
|
+
async function promptCalibration(rl: ReadlineInterface, defaults: Calibration): Promise<Calibration> {
|
|
99
|
+
return {
|
|
100
|
+
xMin: (await promptInt(rl, " xMin (raw at left edge)", { defaultValue: defaults.xMin }))!,
|
|
101
|
+
xMax: (await promptInt(rl, " xMax (raw at right edge)", { defaultValue: defaults.xMax }))!,
|
|
102
|
+
yMin: (await promptInt(rl, " yMin (raw at top edge)", { defaultValue: defaults.yMin }))!,
|
|
103
|
+
yMax: (await promptInt(rl, " yMax (raw at bottom edge)", { defaultValue: defaults.yMax }))!,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** SPI wiring answers, copied into the display record in a fixed key order. */
|
|
108
|
+
interface SpiWiring {
|
|
109
|
+
cs: number;
|
|
110
|
+
dc: number;
|
|
111
|
+
rst: number;
|
|
112
|
+
backlight?: number;
|
|
113
|
+
spiFrequency: number;
|
|
114
|
+
spiPins?: { mosi: number; sck: number; miso: number };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function promptSpiWiring(
|
|
118
|
+
rl: ReadlineInterface,
|
|
119
|
+
display: DisplayCatalogEntry,
|
|
120
|
+
existing: ConfigRecord,
|
|
121
|
+
): Promise<SpiWiring> {
|
|
122
|
+
const cs = (await promptInt(rl, "CS (chip select) GPIO", {
|
|
123
|
+
defaultValue: existingNumber(existing, "cs") ?? display.defaults.cs,
|
|
124
|
+
}))!;
|
|
125
|
+
const dc = (await promptInt(rl, "DC (data/command) GPIO", {
|
|
126
|
+
defaultValue: existingNumber(existing, "dc") ?? display.defaults.dc,
|
|
127
|
+
}))!;
|
|
128
|
+
const rst = (await promptInt(rl, "RST (reset) GPIO", {
|
|
129
|
+
defaultValue: existingNumber(existing, "rst") ?? display.defaults.rst,
|
|
130
|
+
}))!;
|
|
131
|
+
|
|
132
|
+
const backlightDefault = existingNumber(existing, "backlight");
|
|
133
|
+
const backlightHint = display.defaults.backlightProfileDefault !== undefined
|
|
134
|
+
? `blank = profile default ${display.defaults.backlightProfileDefault}`
|
|
135
|
+
: "blank = not controlled";
|
|
136
|
+
const backlight = await promptInt(rl, `Backlight GPIO (${backlightHint})`, {
|
|
137
|
+
defaultValue: backlightDefault,
|
|
138
|
+
allowBlank: backlightDefault === undefined,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const defaultMhz =
|
|
142
|
+
(existingNumber(existing, "spiFrequency") ?? display.defaults.spiFrequency ?? 40_000_000) / 1_000_000;
|
|
143
|
+
const frequencyMhz = await promptText(rl, "SPI frequency in MHz", {
|
|
144
|
+
defaultValue: String(defaultMhz),
|
|
145
|
+
validate: (value) => {
|
|
146
|
+
const mhz = Number(value);
|
|
147
|
+
if (!Number.isFinite(mhz) || mhz <= 0 || mhz > 200) return "Enter a frequency between 1 and 200 MHz.";
|
|
148
|
+
return null;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let spiPins: SpiWiring["spiPins"];
|
|
153
|
+
const overridePins = await promptConfirm(
|
|
154
|
+
rl,
|
|
155
|
+
"Override the SPI data pins (SCK/MOSI/MISO)?",
|
|
156
|
+
existing.spiPins !== undefined,
|
|
157
|
+
);
|
|
158
|
+
if (overridePins) {
|
|
159
|
+
const current = asRecord(existing.spiPins);
|
|
160
|
+
const sck = (await promptInt(rl, " SCK GPIO", { defaultValue: existingNumber(current, "sck") ?? 18 }))!;
|
|
161
|
+
const mosi = (await promptInt(rl, " MOSI GPIO", { defaultValue: existingNumber(current, "mosi") ?? 23 }))!;
|
|
162
|
+
const miso = await promptInt(rl, " MISO GPIO (blank = write-only)", {
|
|
163
|
+
defaultValue: existingNumber(current, "miso"),
|
|
164
|
+
allowBlank: existingNumber(current, "miso") === undefined,
|
|
165
|
+
});
|
|
166
|
+
spiPins = { mosi, sck, miso: miso ?? -1 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
cs,
|
|
171
|
+
dc,
|
|
172
|
+
rst,
|
|
173
|
+
...(backlight !== undefined ? { backlight } : {}),
|
|
174
|
+
spiFrequency: Math.round(Number(frequencyMhz) * 1_000_000),
|
|
175
|
+
...(spiPins ? { spiPins } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function promptTouch(
|
|
180
|
+
rl: ReadlineInterface,
|
|
181
|
+
touchEntry: TouchCatalogEntry,
|
|
182
|
+
context: { existingTouch: ConfigRecord; panelWidth: number; panelHeight: number },
|
|
183
|
+
): Promise<ConfigRecord> {
|
|
184
|
+
const { existingTouch, panelWidth, panelHeight } = context;
|
|
185
|
+
const touch: ConfigRecord = {};
|
|
186
|
+
|
|
187
|
+
if (touchEntry.kind === "adapter") {
|
|
188
|
+
touch.adapter = await promptText(rl, "Adapter file path (relative to project root)", {
|
|
189
|
+
defaultValue: existingString(existingTouch, "adapter") ?? "./touch-adapter",
|
|
190
|
+
});
|
|
191
|
+
const identity = { xMin: 0, xMax: panelWidth, yMin: 0, yMax: panelHeight };
|
|
192
|
+
const useIdentity = await promptConfirm(
|
|
193
|
+
rl,
|
|
194
|
+
"Use identity pixel calibration?",
|
|
195
|
+
existingTouch.calibration === undefined,
|
|
196
|
+
);
|
|
197
|
+
touch.calibration = useIdentity ? identity : await promptCalibration(rl, asCalibration(existingTouch.calibration) ?? identity);
|
|
198
|
+
return touch;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
touch.library = touchEntry.id;
|
|
202
|
+
|
|
203
|
+
if (touchEntry.kind === "spi") {
|
|
204
|
+
touch.cs = (await promptInt(rl, "Touch CS GPIO", {
|
|
205
|
+
defaultValue: existingNumber(existingTouch, "cs") ?? touchEntry.defaults.cs,
|
|
206
|
+
}))!;
|
|
207
|
+
const irq = await promptInt(rl, "Touch IRQ GPIO (blank = none)", {
|
|
208
|
+
defaultValue: existingNumber(existingTouch, "irq"),
|
|
209
|
+
allowBlank: existingNumber(existingTouch, "irq") === undefined,
|
|
210
|
+
});
|
|
211
|
+
if (irq !== undefined) touch.irq = irq;
|
|
212
|
+
} else if (touchEntry.kind === "i2c") {
|
|
213
|
+
const addressText = await promptText(rl, "Touch I2C address", {
|
|
214
|
+
defaultValue: `0x${(existingNumber(existingTouch, "i2cAddress") ?? touchEntry.defaults.i2cAddress ?? 0x38).toString(16)}`,
|
|
215
|
+
validate: (value) => (parseNumeric(value) === undefined ? "Enter an address like 0x38 or 56." : null),
|
|
216
|
+
});
|
|
217
|
+
touch.i2cAddress = parseNumeric(addressText)!;
|
|
218
|
+
const defaultKhz =
|
|
219
|
+
(existingNumber(existingTouch, "i2cFrequency") ?? touchEntry.defaults.i2cFrequency ?? 400_000) / 1000;
|
|
220
|
+
const frequencyKhz = await promptText(rl, "I2C bus speed in kHz", {
|
|
221
|
+
defaultValue: String(defaultKhz),
|
|
222
|
+
validate: (value) => {
|
|
223
|
+
const khz = Number(value);
|
|
224
|
+
if (!Number.isFinite(khz) || khz <= 0 || khz > 3400) return "Enter a speed between 1 and 3400 kHz.";
|
|
225
|
+
return null;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
touch.i2cFrequency = Math.round(Number(frequencyKhz) * 1000);
|
|
229
|
+
const irq = await promptInt(rl, "Touch IRQ GPIO (blank = none)", {
|
|
230
|
+
defaultValue: existingNumber(existingTouch, "irq"),
|
|
231
|
+
allowBlank: existingNumber(existingTouch, "irq") === undefined,
|
|
232
|
+
});
|
|
233
|
+
if (irq !== undefined) touch.irq = irq;
|
|
234
|
+
const resetPin = await promptInt(rl, "Touch reset GPIO (blank = none)", {
|
|
235
|
+
defaultValue: existingNumber(existingTouch, "resetPin"),
|
|
236
|
+
allowBlank: existingNumber(existingTouch, "resetPin") === undefined,
|
|
237
|
+
});
|
|
238
|
+
if (resetPin !== undefined) touch.resetPin = resetPin;
|
|
239
|
+
} else if (touchEntry.kind === "analog") {
|
|
240
|
+
const current = asRecord(existingTouch.analogPins);
|
|
241
|
+
const defaults = touchEntry.defaults.analogPins!;
|
|
242
|
+
touch.analogPins = {
|
|
243
|
+
xp: (await promptInt(rl, "XP GPIO", { defaultValue: existingNumber(current, "xp") ?? defaults.xp }))!,
|
|
244
|
+
yp: (await promptInt(rl, "YP GPIO", { defaultValue: existingNumber(current, "yp") ?? defaults.yp }))!,
|
|
245
|
+
xm: (await promptInt(rl, "XM GPIO", { defaultValue: existingNumber(current, "xm") ?? defaults.xm }))!,
|
|
246
|
+
ym: (await promptInt(rl, "YM GPIO", { defaultValue: existingNumber(current, "ym") ?? defaults.ym }))!,
|
|
247
|
+
rx: (await promptInt(rl, "Rx resistance (ohms)", { defaultValue: existingNumber(current, "rx") ?? defaults.rx }))!,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (touchEntry.kind === "spi" || touchEntry.kind === "analog") {
|
|
252
|
+
// Resistive controllers report raw ADC values — typical defaults exist but
|
|
253
|
+
// per-panel calibration is expected sooner or later.
|
|
254
|
+
const typical = touchEntry.defaults.calibration ?? { xMin: 0, xMax: 4095, yMin: 0, yMax: 4095 };
|
|
255
|
+
const useTypical = await promptConfirm(
|
|
256
|
+
rl,
|
|
257
|
+
"Use typical raw-ADC calibration values?",
|
|
258
|
+
existingTouch.calibration === undefined,
|
|
259
|
+
);
|
|
260
|
+
touch.calibration = useTypical
|
|
261
|
+
? typical
|
|
262
|
+
: await promptCalibration(rl, asCalibration(existingTouch.calibration) ?? typical);
|
|
263
|
+
} else {
|
|
264
|
+
// Capacitive controllers report pixel coordinates; default to the panel's
|
|
265
|
+
// native (unrotated) size — the runtime applies rotation after mapping.
|
|
266
|
+
const pixel = { xMin: 0, xMax: panelWidth, yMin: 0, yMax: panelHeight };
|
|
267
|
+
const useDefault = await promptConfirm(
|
|
268
|
+
rl,
|
|
269
|
+
`Use pixel calibration (0..${panelWidth} × 0..${panelHeight})?`,
|
|
270
|
+
existingTouch.calibration === undefined,
|
|
271
|
+
);
|
|
272
|
+
touch.calibration = useDefault
|
|
273
|
+
? pixel
|
|
274
|
+
: await promptCalibration(rl, asCalibration(existingTouch.calibration) ?? pixel);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return touch;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface WizardRunResult {
|
|
281
|
+
exitCode: number;
|
|
282
|
+
/** Absolute path of the config that was written, when a write happened. */
|
|
283
|
+
configPath?: string;
|
|
284
|
+
/** Absolute path of the package.json holding the preview script, when the
|
|
285
|
+
* script was added or already present. */
|
|
286
|
+
packageJsonPath?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Optional stream overrides so tests can drive the interactive flow. */
|
|
290
|
+
export interface WizardStreams {
|
|
291
|
+
input?: NodeJS.ReadableStream;
|
|
292
|
+
output?: NodeJS.WritableStream;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Run the interactive integration wizard. Returns a process exit code
|
|
297
|
+
* (0 = wrote (or deliberately declined), 1 = blocked before writing).
|
|
298
|
+
*/
|
|
299
|
+
export async function runIntegrationWizard(
|
|
300
|
+
cwd: string = process.cwd(),
|
|
301
|
+
streams: WizardStreams = {},
|
|
302
|
+
): Promise<WizardRunResult> {
|
|
303
|
+
const rl = createPromptInterface(streams.input, streams.output);
|
|
304
|
+
try {
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(chalk.cyan("⤳ @typecad/ui") + chalk.dim(" — Display Integration Wizard"));
|
|
307
|
+
console.log(chalk.dim(" Configure a display + touch hardware for cuttlefish."));
|
|
308
|
+
console.log();
|
|
309
|
+
|
|
310
|
+
const configPath = findCuttlefishConfig(cwd);
|
|
311
|
+
if (!configPath) {
|
|
312
|
+
console.log(`${chalk.yellow("!")} No ${chalk.white("cuttlefish.config.ts")} found in ${chalk.dim(cwd)} (or any parent).`);
|
|
313
|
+
console.log();
|
|
314
|
+
console.log(" Create a cuttlefish project first, then re-run the wizard:");
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(` ${chalk.cyan("npx @typecad/cuttlefish init")}`);
|
|
317
|
+
console.log(` ${chalk.cyan("npx @typecad/ui --config")}`);
|
|
318
|
+
console.log();
|
|
319
|
+
return { exitCode: 1 };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sourceText = fs.readFileSync(configPath, "utf-8");
|
|
323
|
+
const existingDisplay = readConfigSection(sourceText, "display");
|
|
324
|
+
const existing = existingDisplay ?? {};
|
|
325
|
+
const existingTouch = asRecord(existing.touch);
|
|
326
|
+
const entryPath = readEntryPath(sourceText);
|
|
327
|
+
|
|
328
|
+
console.log(`${chalk.green("✓")} Config: ${chalk.white(path.relative(cwd, configPath))}`);
|
|
329
|
+
if (existingDisplay) {
|
|
330
|
+
console.log(chalk.dim(" An existing display section was found — answers below are prefilled from it."));
|
|
331
|
+
}
|
|
332
|
+
if (entryPath) {
|
|
333
|
+
const entryAbs = path.resolve(path.dirname(configPath), entryPath);
|
|
334
|
+
console.log(
|
|
335
|
+
`${chalk.dim(` Entry: ${entryPath} `)}${fs.existsSync(entryAbs) ? chalk.green("(exists)") : chalk.yellow("(missing — the wizard can create it)")}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
console.log();
|
|
339
|
+
|
|
340
|
+
// --- 1. Display selection -------------------------------------------------
|
|
341
|
+
const displayId = await promptSelect(
|
|
342
|
+
rl,
|
|
343
|
+
"Which display are you integrating?",
|
|
344
|
+
DISPLAY_CATALOG.map((entry) => ({ label: entry.label, value: entry.id, hint: entry.hint })),
|
|
345
|
+
);
|
|
346
|
+
const displayEntry = DISPLAY_CATALOG.find((entry) => entry.id === displayId)!;
|
|
347
|
+
|
|
348
|
+
const record: ConfigRecord = {};
|
|
349
|
+
const comments: Record<string, string> = {};
|
|
350
|
+
|
|
351
|
+
if (displayEntry.profile) {
|
|
352
|
+
record.profile = displayEntry.profile;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// --- 2. Driver-specific basics ---------------------------------------------
|
|
356
|
+
let customBus: "spi" | "i2c" | undefined;
|
|
357
|
+
if (displayId === "custom") {
|
|
358
|
+
record.driver = await promptText(rl, "Driver name", {
|
|
359
|
+
defaultValue: existingString(existing, "driver") ?? "ili9341",
|
|
360
|
+
});
|
|
361
|
+
customBus = await promptSelect<"spi" | "i2c">(rl, "Bus", [
|
|
362
|
+
{ label: "SPI", value: "spi", hint: "TFT-style panels" },
|
|
363
|
+
{ label: "I2C", value: "i2c", hint: "small OLED/mono panels" },
|
|
364
|
+
]);
|
|
365
|
+
record.width = (await promptInt(rl, "Panel width in px", {
|
|
366
|
+
defaultValue: existingNumber(existing, "width") ?? 320,
|
|
367
|
+
}))!;
|
|
368
|
+
record.height = (await promptInt(rl, "Panel height in px", {
|
|
369
|
+
defaultValue: existingNumber(existing, "height") ?? 240,
|
|
370
|
+
}))!;
|
|
371
|
+
const colorFormat = await promptSelect(rl, "Color format", [
|
|
372
|
+
{ label: "rgb565 — 16-bit color TFT", value: "rgb565" },
|
|
373
|
+
{ label: "rgb888 — 24-bit color", value: "rgb888" },
|
|
374
|
+
{ label: "mono — 1-bit monochrome", value: "mono" },
|
|
375
|
+
]);
|
|
376
|
+
record.colorFormat = colorFormat;
|
|
377
|
+
if (colorFormat === "mono") {
|
|
378
|
+
record.displayClass = await promptSelect(rl, "Panel class", [
|
|
379
|
+
{ label: "oled — page-buffered OLED", value: "oled" },
|
|
380
|
+
{ label: "eink — bistable e-paper", value: "eink" },
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
} else if (displayId === "sdl") {
|
|
384
|
+
record.driver = "sdl";
|
|
385
|
+
record.width = (await promptInt(rl, "Window width in px", {
|
|
386
|
+
defaultValue: existingNumber(existing, "width") ?? 320,
|
|
387
|
+
}))!;
|
|
388
|
+
record.height = (await promptInt(rl, "Window height in px", {
|
|
389
|
+
defaultValue: existingNumber(existing, "height") ?? 240,
|
|
390
|
+
}))!;
|
|
391
|
+
record.colorFormat = "rgb888";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- 3. Bus wiring ----------------------------------------------------------
|
|
395
|
+
const bus = displayId === "custom" ? customBus! : displayEntry.bus;
|
|
396
|
+
|
|
397
|
+
if (bus === "spi" && displayId !== "sdl") {
|
|
398
|
+
const wiring = await promptSpiWiring(rl, displayEntry, existing);
|
|
399
|
+
record.cs = wiring.cs;
|
|
400
|
+
record.dc = wiring.dc;
|
|
401
|
+
record.rst = wiring.rst;
|
|
402
|
+
if (wiring.backlight !== undefined) record.backlight = wiring.backlight;
|
|
403
|
+
record.spiFrequency = wiring.spiFrequency;
|
|
404
|
+
comments.spiFrequency = "Hz — lower this if the panel glitches";
|
|
405
|
+
if (wiring.spiPins) record.spiPins = wiring.spiPins;
|
|
406
|
+
} else if (bus === "i2c") {
|
|
407
|
+
record.bus = "I2C";
|
|
408
|
+
const addressText = await promptText(rl, "I2C address", {
|
|
409
|
+
defaultValue: `0x${(existingNumber(existing, "address") ?? displayEntry.defaults.address ?? 0x3c).toString(16)}`,
|
|
410
|
+
validate: (value) => (parseNumeric(value) === undefined ? "Enter an address like 0x3C or 60." : null),
|
|
411
|
+
});
|
|
412
|
+
record.address = parseNumeric(addressText)!;
|
|
413
|
+
const resetPin = await promptInt(rl, "Reset GPIO (blank = none)", {
|
|
414
|
+
defaultValue: existingNumber(existing, "reset"),
|
|
415
|
+
allowBlank: existingNumber(existing, "reset") === undefined,
|
|
416
|
+
});
|
|
417
|
+
if (resetPin !== undefined) record.reset = resetPin;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// --- 4. Orientation + rendering ---------------------------------------------
|
|
421
|
+
record.rotation = (await promptInt(rl, "Rotation (0=portrait, 1=landscape, 2=180°, 3=270°)", {
|
|
422
|
+
defaultValue: existingNumber(existing, "rotation") ?? displayEntry.defaults.rotation ?? 0,
|
|
423
|
+
min: 0,
|
|
424
|
+
max: 3,
|
|
425
|
+
}))!;
|
|
426
|
+
|
|
427
|
+
const antialias = await promptConfirm(
|
|
428
|
+
rl,
|
|
429
|
+
"Enable antialiased rendering (smoother text and edges, uses more RAM)?",
|
|
430
|
+
existing.antialias !== undefined ? existing.antialias === true : (displayEntry.defaults.antialias ?? false),
|
|
431
|
+
);
|
|
432
|
+
if (antialias) record.antialias = true;
|
|
433
|
+
|
|
434
|
+
const advanced = await promptConfirm(rl, "Configure advanced color options (color order / inversion)?", false);
|
|
435
|
+
if (advanced) {
|
|
436
|
+
const colorOrder = await promptSelect(rl, "Pixel color order", [
|
|
437
|
+
{ label: "rgb — normal", value: "rgb" },
|
|
438
|
+
{ label: "bgr — swapped (red and blue flipped)", value: "bgr" },
|
|
439
|
+
]);
|
|
440
|
+
record.colorOrder = colorOrder;
|
|
441
|
+
if (await promptConfirm(rl, "Invert display colors?", false)) {
|
|
442
|
+
record.invertDisplay = true;
|
|
443
|
+
}
|
|
444
|
+
} else if (displayEntry.defaults.colorOrder === "bgr") {
|
|
445
|
+
// ST7796S modules expect BGR — carry the proven default like the demos.
|
|
446
|
+
record.colorOrder = "bgr";
|
|
447
|
+
}
|
|
448
|
+
if (displayEntry.defaults.invert === false && record.invertDisplay === undefined) {
|
|
449
|
+
// ST7796S panels ship non-inverted; pin it so a future profile change
|
|
450
|
+
// cannot silently flip colors (matches demos/demo-display).
|
|
451
|
+
record.invertDisplay = false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --- 5. Touch ----------------------------------------------------------------
|
|
455
|
+
let touchRecord: ConfigRecord | undefined;
|
|
456
|
+
let touchEntry: TouchCatalogEntry | undefined;
|
|
457
|
+
|
|
458
|
+
if (displayId === "sdl") {
|
|
459
|
+
touchRecord = {
|
|
460
|
+
library: "sdl",
|
|
461
|
+
calibration: { xMin: 0, xMax: record.width ?? 320, yMin: 0, yMax: record.height ?? 240 },
|
|
462
|
+
};
|
|
463
|
+
touchEntry = TOUCH_CATALOG.find((entry) => entry.id === "none")!;
|
|
464
|
+
console.log(`${chalk.cyan("?")} Touch: ${chalk.white("mouse via SDL (configured automatically)")}`);
|
|
465
|
+
} else {
|
|
466
|
+
const touchId = await promptSelect(
|
|
467
|
+
rl,
|
|
468
|
+
"Touch controller",
|
|
469
|
+
TOUCH_CATALOG.map((entry) => ({ label: entry.label, value: entry.id, hint: entry.hint })),
|
|
470
|
+
);
|
|
471
|
+
touchEntry = TOUCH_CATALOG.find((entry) => entry.id === touchId)!;
|
|
472
|
+
if (touchId !== "none") {
|
|
473
|
+
touchRecord = await promptTouch(rl, touchEntry, {
|
|
474
|
+
existingTouch,
|
|
475
|
+
panelWidth:
|
|
476
|
+
displayEntry.defaults.nativeWidth
|
|
477
|
+
?? displayEntry.defaults.width
|
|
478
|
+
?? (typeof record.width === "number" ? record.width : 320),
|
|
479
|
+
panelHeight:
|
|
480
|
+
displayEntry.defaults.nativeHeight
|
|
481
|
+
?? displayEntry.defaults.height
|
|
482
|
+
?? (typeof record.height === "number" ? record.height : 240),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// --- 6. Theme ------------------------------------------------------------------
|
|
488
|
+
const themeCss = await promptText(rl, "Theme CSS file path (blank = none)", {
|
|
489
|
+
defaultValue: existingString(existing, "themeCss") ?? "",
|
|
490
|
+
allowBlank: true,
|
|
491
|
+
});
|
|
492
|
+
if (themeCss) record.themeCss = themeCss;
|
|
493
|
+
const themeClass = await promptText(rl, "Theme class (e.g. dark; blank = none)", {
|
|
494
|
+
defaultValue: existingString(existing, "themeClass") ?? "",
|
|
495
|
+
allowBlank: true,
|
|
496
|
+
});
|
|
497
|
+
if (themeClass) record.themeClass = themeClass;
|
|
498
|
+
|
|
499
|
+
// Carry over display keys the wizard does not manage (scroll, scanlineSync,
|
|
500
|
+
// capabilities, …) so a re-run never silently drops user configuration.
|
|
501
|
+
const carried: string[] = [];
|
|
502
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
503
|
+
if (MANAGED_DISPLAY_KEYS.has(key) || value === undefined) continue;
|
|
504
|
+
record[key] = value;
|
|
505
|
+
carried.push(key);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (touchRecord) record.touch = touchRecord;
|
|
509
|
+
|
|
510
|
+
// --- 7. Pin conflict check -------------------------------------------------------
|
|
511
|
+
const conflicts = findPinConflicts(
|
|
512
|
+
{
|
|
513
|
+
cs: typeof record.cs === "number" ? record.cs : undefined,
|
|
514
|
+
dc: typeof record.dc === "number" ? record.dc : undefined,
|
|
515
|
+
rst: typeof record.rst === "number" ? record.rst : undefined,
|
|
516
|
+
backlight: typeof record.backlight === "number" ? record.backlight : undefined,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
cs: existingNumber(touchRecord ?? {}, "cs"),
|
|
520
|
+
irq: existingNumber(touchRecord ?? {}, "irq"),
|
|
521
|
+
resetPin: existingNumber(touchRecord ?? {}, "resetPin"),
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
if (conflicts.length > 0) {
|
|
525
|
+
for (const conflict of conflicts) {
|
|
526
|
+
console.log(` ${chalk.yellow("!")} ${conflict}`);
|
|
527
|
+
}
|
|
528
|
+
if (!(await promptConfirm(rl, "Continue with these conflicting pins anyway?", false))) {
|
|
529
|
+
console.log(chalk.dim(" Aborted — nothing was written."));
|
|
530
|
+
return { exitCode: 1 };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// --- 8. package.json preview script ------------------------------------------
|
|
535
|
+
// `npm run preview` should start the desktop preview renderer, pointing at
|
|
536
|
+
// the config the wizard just verified. An existing identical script is
|
|
537
|
+
// left untouched; a differing one is only replaced with consent.
|
|
538
|
+
let packageJsonPath: string | undefined;
|
|
539
|
+
let packageJsonText: string | undefined;
|
|
540
|
+
let previewCommand: string | undefined;
|
|
541
|
+
let existingPreview: string | undefined;
|
|
542
|
+
let replacePreview = false;
|
|
543
|
+
|
|
544
|
+
const foundPackageJson = findPackageJson(path.dirname(configPath));
|
|
545
|
+
if (foundPackageJson) {
|
|
546
|
+
try {
|
|
547
|
+
packageJsonText = fs.readFileSync(foundPackageJson, "utf-8");
|
|
548
|
+
existingPreview = readPackageScript(packageJsonText, "preview");
|
|
549
|
+
packageJsonPath = foundPackageJson;
|
|
550
|
+
previewCommand = previewScriptCommand(foundPackageJson, configPath);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.log(` ${chalk.yellow("!")} ${err instanceof Error ? err.message : String(err)} — skipping the npm preview script.`);
|
|
553
|
+
}
|
|
554
|
+
if (previewCommand && existingPreview !== undefined && existingPreview !== previewCommand) {
|
|
555
|
+
console.log(` ${chalk.yellow("!")} package.json already has a preview script: ${chalk.white(existingPreview)}`);
|
|
556
|
+
replacePreview = await promptConfirm(rl, "Replace it with the wizard's preview command?", true);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
console.log(`${chalk.yellow("!")} No package.json found next to ${path.relative(cwd, configPath)} — skipping the npm preview script.`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// --- 9. Summary + write -------------------------------------------------------
|
|
563
|
+
console.log();
|
|
564
|
+
console.log(chalk.dim(" The following display section will be written to"));
|
|
565
|
+
console.log(chalk.dim(` ${path.relative(cwd, configPath)}:`));
|
|
566
|
+
console.log();
|
|
567
|
+
console.log(
|
|
568
|
+
renderDisplayProperty(record, { comments })
|
|
569
|
+
.split("\n")
|
|
570
|
+
.map((line) => ` ${line}`)
|
|
571
|
+
.join("\n"),
|
|
572
|
+
);
|
|
573
|
+
console.log();
|
|
574
|
+
if (displayEntry.wiringNote) {
|
|
575
|
+
console.log(` ${chalk.dim(displayEntry.wiringNote)}`);
|
|
576
|
+
}
|
|
577
|
+
if (carried.length > 0) {
|
|
578
|
+
console.log(
|
|
579
|
+
` ${chalk.dim(`Kept existing display settings the wizard does not manage: ${carried.join(", ")}`)}`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (previewCommand && packageJsonPath) {
|
|
583
|
+
if (existingPreview === previewCommand) {
|
|
584
|
+
console.log(` ${chalk.dim(`npm script already configured: preview — "${previewCommand}"`)}`);
|
|
585
|
+
} else if (existingPreview === undefined || replacePreview) {
|
|
586
|
+
console.log(` ${chalk.dim("Also writes npm script:")} ${chalk.white("preview")}${chalk.dim(` — "${previewCommand}"`)}`);
|
|
587
|
+
} else {
|
|
588
|
+
console.log(` ${chalk.dim(`Kept existing preview script: "${existingPreview}"`)}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
console.log();
|
|
592
|
+
|
|
593
|
+
if (!(await promptConfirm(rl, "Write this configuration?", true))) {
|
|
594
|
+
console.log(chalk.dim(" Aborted — nothing was written."));
|
|
595
|
+
return { exitCode: 0 };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const updated = upsertDisplaySection(sourceText, record, { comments });
|
|
599
|
+
const syntaxError = findSyntaxError(updated.text);
|
|
600
|
+
if (syntaxError) {
|
|
601
|
+
console.log(` ${chalk.red("✗")} Refusing to write — the edited config no longer parses: ${syntaxError}`);
|
|
602
|
+
return { exitCode: 1 };
|
|
603
|
+
}
|
|
604
|
+
fs.writeFileSync(configPath, updated.text, "utf-8");
|
|
605
|
+
console.log(
|
|
606
|
+
`${chalk.green("✓")} ${updated.mode === "replaced" ? "Updated" : "Added"} the display section in ${chalk.white(path.relative(cwd, configPath))}`,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
// The preview script rides along with the confirmed write: added when
|
|
610
|
+
// missing, confirmed verbatim when already the wizard's command, and only
|
|
611
|
+
// replaced when the user consented above.
|
|
612
|
+
if (packageJsonPath && packageJsonText !== undefined && previewCommand
|
|
613
|
+
&& (existingPreview === undefined || existingPreview === previewCommand || replacePreview)) {
|
|
614
|
+
const scriptUpsert = upsertPackageScript(packageJsonText, "preview", previewCommand);
|
|
615
|
+
if (scriptUpsert.changed) {
|
|
616
|
+
fs.writeFileSync(packageJsonPath, scriptUpsert.text, "utf-8");
|
|
617
|
+
console.log(
|
|
618
|
+
`${chalk.green("✓")} ${scriptUpsert.previous !== undefined ? "Updated" : "Added"} npm script ${chalk.white("preview")} in ${chalk.white(path.relative(cwd, packageJsonPath))}`,
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
console.log(
|
|
622
|
+
`${chalk.green("✓")} npm script ${chalk.white("preview")} already configured in ${chalk.white(path.relative(cwd, packageJsonPath))}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// --- 10. Starter entry file ------------------------------------------------------
|
|
628
|
+
if (entryPath) {
|
|
629
|
+
const entryAbs = path.resolve(path.dirname(configPath), entryPath);
|
|
630
|
+
if (!fs.existsSync(entryAbs) && entryAbs.endsWith(".ui")) {
|
|
631
|
+
if (await promptConfirm(rl, `Create a starter ${entryPath}?`, true)) {
|
|
632
|
+
fs.mkdirSync(path.dirname(entryAbs), { recursive: true });
|
|
633
|
+
fs.writeFileSync(
|
|
634
|
+
entryAbs,
|
|
635
|
+
renderStarterUi(
|
|
636
|
+
typeof record.width === "number" ? record.width : 320,
|
|
637
|
+
typeof record.height === "number" ? record.height : 240,
|
|
638
|
+
),
|
|
639
|
+
"utf-8",
|
|
640
|
+
);
|
|
641
|
+
console.log(`${chalk.green("✓")} Created ${chalk.white(entryPath)} — open it and start editing your UI.`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// --- 11. Next steps ---------------------------------------------------------------
|
|
647
|
+
console.log();
|
|
648
|
+
console.log(chalk.cyan("Next steps"));
|
|
649
|
+
console.log();
|
|
650
|
+
|
|
651
|
+
const libraries: string[] = [];
|
|
652
|
+
if (displayEntry.arduinoLibraries) libraries.push(...displayEntry.arduinoLibraries);
|
|
653
|
+
if (touchEntry?.arduinoLibrary && touchRecord?.library) libraries.push(touchEntry.arduinoLibrary);
|
|
654
|
+
if (libraries.length > 0) {
|
|
655
|
+
console.log(" 1. Install the display/touch Arduino libraries (once per machine):");
|
|
656
|
+
console.log();
|
|
657
|
+
console.log(` ${chalk.cyan(`arduino-cli lib install ${libraries.map((lib) => `"${lib}"`).join(" ")}`)}`);
|
|
658
|
+
console.log();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const firstStep = libraries.length > 0 ? 2 : 1;
|
|
662
|
+
console.log(` ${firstStep}. Preview your UI on the desktop:`);
|
|
663
|
+
console.log();
|
|
664
|
+
console.log(` ${chalk.cyan(previewCommand ? "npm run preview" : "npx @typecad/cuttlefish preview")}`);
|
|
665
|
+
console.log();
|
|
666
|
+
console.log(` ${firstStep + 1}. Compile for your board:`);
|
|
667
|
+
console.log();
|
|
668
|
+
console.log(` ${chalk.cyan("npx @typecad/cuttlefish build --compile")}`);
|
|
669
|
+
console.log();
|
|
670
|
+
console.log(` ${firstStep + 2}. Flash it (set console.port in the config, or pass --port):`);
|
|
671
|
+
console.log();
|
|
672
|
+
console.log(` ${chalk.cyan("npx @typecad/cuttlefish build --compile --upload")}`);
|
|
673
|
+
console.log();
|
|
674
|
+
|
|
675
|
+
if (touchRecord && touchRecord.library === "XPT2046_Touchscreen") {
|
|
676
|
+
console.log(chalk.dim(" Touch tip: the written calibration uses typical raw-ADC values. If taps land"));
|
|
677
|
+
console.log(chalk.dim(" off-target, calibrate your panel and update display.touch.calibration."));
|
|
678
|
+
console.log();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
exitCode: 0,
|
|
683
|
+
configPath,
|
|
684
|
+
...(packageJsonPath && previewCommand ? { packageJsonPath } : {}),
|
|
685
|
+
};
|
|
686
|
+
} finally {
|
|
687
|
+
rl.close();
|
|
688
|
+
}
|
|
689
|
+
}
|