@typecad/ui 1.0.0-alpha.11 → 1.0.0-alpha.12

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 CHANGED
@@ -13,6 +13,26 @@ npm install --save-dev @typecad/cuttlefish
13
13
 
14
14
  `@typecad/ui` is compile-time only — none of its code is shipped to the device. The transpiler intercepts `ui.mount` / `ui.signal` / `ui.bind` / ... calls and lowers them to device variables and binding-table entries, so the package can be safely kept in `dependencies`.
15
15
 
16
+ ### Integration wizard
17
+
18
+ Integrating a display is the one step with real hardware decisions — which panel, which bus, which pins, whether there's touch and how it connects. Run the wizard from your project directory:
19
+
20
+ ```bash
21
+ npx @typecad/ui --config
22
+ ```
23
+
24
+ It asks:
25
+
26
+ 1. **Display** — a built-in profile (ILI9341 320×240 SPI TFT, ST7796S 320×480 SPI TFT, SSD1309 128×64 I2C OLED), the desktop SDL simulator, or a fully custom driver.
27
+ 2. **Bus wiring** — SPI pins (CS/DC/RST/backlight, frequency in MHz, optional SCK/MOSI/MISO override) or the I2C address. Defaults match the framework profiles and the repo's demo wiring (ESP32 VSPI), and any existing `display` section in your config pre-fills the answers.
28
+ 3. **Orientation & rendering** — rotation and antialiasing, with advanced color-order/inversion options behind a confirm.
29
+ 4. **Touch** — none, resistive (XPT2046, STMPE610, 4-wire analog), capacitive (FT6336U, GT911, CST816S), or a custom adapter file — each with its pins, address, and a sensible default calibration.
30
+ 5. **Theme** — optional `themeCss` / `themeClass`.
31
+
32
+ After a summary preview and confirmation, the wizard splices only the `display` section into `cuttlefish.config.ts` — every other section (and its comments) is preserved byte-for-byte, unmanaged display keys like `scroll` are carried over, and the result is syntax-checked before anything is written. If your config's `entry` points at a `.ui` file that doesn't exist yet, it offers to generate a starter screen, and it prints the exact `arduino-cli lib install ...`, preview, compile, and flash commands as next steps.
33
+
34
+ If there is no `cuttlefish.config.ts` yet, create the project first with `npx @typecad/cuttlefish init`, then re-run the wizard.
35
+
16
36
  ## Project layout
17
37
 
18
38
  A TypeCAD UI project has one **entry** — the file you point `cuttlefish.config.ts` at. The entry can be either a `.ui` single-file component or a plain `.ts` module. Both intermix freely with regular cuttlefish TypeScript (HAL pin reads, `setInterval`, `console.log`, your own `.ts` modules) — the `<script>` block of a `.ui` file and a standalone `.ts` file are lowered by the same pipeline.
@@ -217,7 +237,7 @@ The fastest path is a single `.ui` file with markup, styling, and behavior toget
217
237
  </screen>
218
238
  ```
219
239
 
220
- Display hardware and wiring live in `cuttlefish.config.ts` under `display`, so the UI source stays focused on UI behavior.
240
+ Display hardware and wiring live in `cuttlefish.config.ts` under `display`, so the UI source stays focused on UI behavior. Run `npx @typecad/ui --config` to generate that section interactively (see [Integration wizard](#integration-wizard)) — the result looks like the `display` line below.
221
241
 
222
242
  ### 2. Point the entry at the `.ui` file
223
243
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // ---------------------------------------------------------------------------
3
+ // @typecad/ui CLI — display integration wizard.
4
+ //
5
+ // npx @typecad/ui --config Run the interactive integration wizard
6
+ // npx @typecad/ui --help Show usage
7
+ // npx @typecad/ui --version Show the installed version
8
+ // ---------------------------------------------------------------------------
9
+ import { runIntegrationWizard } from "./wizard/integration-wizard.js";
10
+ const USAGE = `
11
+ @typecad/ui — HTML/CSS-driven graphics for microcontrollers
12
+
13
+ Usage:
14
+ npx @typecad/ui --config Configure a display (+ touch) for your
15
+ cuttlefish project interactively. Writes the
16
+ \`display\` section of cuttlefish.config.ts.
17
+
18
+ Options:
19
+ --config Run the integration wizard
20
+ --help, -h Show this help
21
+ --version, -v Print the installed @typecad/ui version
22
+
23
+ The wizard asks which display module you are using (ILI9341 / ST7796S SPI TFT,
24
+ SSD1309 I2C OLED, desktop simulator, or custom), then walks through bus pins,
25
+ SPI/I2C speed, rotation, and touch controller wiring with hardware-aware
26
+ defaults. It never touches the rest of your cuttlefish.config.ts.
27
+
28
+ Docs: https://github.com/justind000/typecode/tree/main/packages/ui
29
+ `.trim();
30
+ function printUsage() {
31
+ console.log(USAGE);
32
+ }
33
+ async function main(argv) {
34
+ const args = argv.slice(2);
35
+ if (args.includes("--help") || args.includes("-h")) {
36
+ printUsage();
37
+ return 0;
38
+ }
39
+ if (args.includes("--version") || args.includes("-v")) {
40
+ // Lazily read the version so importing this module stays side-effect free.
41
+ const { readFile } = await import("node:fs/promises");
42
+ const { fileURLToPath } = await import("node:url");
43
+ const { dirname, join } = await import("node:path");
44
+ try {
45
+ const packageJson = JSON.parse(await readFile(join(dirname(dirname(fileURLToPath(import.meta.url))), "package.json"), "utf-8"));
46
+ console.log(packageJson.version ?? "unknown");
47
+ return 0;
48
+ }
49
+ catch {
50
+ console.log("unknown");
51
+ return 0;
52
+ }
53
+ }
54
+ if (args.length === 0 || (args.length === 1 && (args[0] === "--config" || args[0] === "config"))) {
55
+ if (!process.stdin.isTTY) {
56
+ console.error("The integration wizard is interactive and needs a terminal.");
57
+ console.error("Run `npx @typecad/ui --config` from your project directory, or configure");
58
+ console.error("the `display` section of cuttlefish.config.ts manually:");
59
+ console.error("https://github.com/justind000/typecode/tree/main/packages/ui#display-configuration");
60
+ return 1;
61
+ }
62
+ const result = await runIntegrationWizard(process.cwd());
63
+ return result.exitCode;
64
+ }
65
+ console.error(`Unknown option${args.length > 1 ? "s" : ""}: ${args.join(" ")}\n`);
66
+ printUsage();
67
+ return 2;
68
+ }
69
+ main(process.argv).then((code) => {
70
+ process.exitCode = code;
71
+ }, (error) => {
72
+ console.error(error instanceof Error ? error.message : error);
73
+ process.exitCode = 1;
74
+ });
@@ -449,7 +449,7 @@ function parseBoxShadow(style, format) {
449
449
  // Split into a numbers zone (offset/blur) and a color zone. The color
450
450
  // zone starts at the first # hex, rgb(, rgba(, or color name. css-tree
451
451
  // may strip spaces between values, so we can't rely on whitespace split.
452
- const colorStart = part.search(/#|rgba?\(|\b(?:black|white|red|green|blue|gray|grey|yellow|orange|purple|pink|cyan|magenta|silver|gold|brown|tan|navy|teal|maroon|lime|olive|aqua|fuchsia|transparent)\b/i);
452
+ const colorStart = part.search(/#|rgba?\(|hsla?\(|\b(?:black|white|red|green|blue|gray|grey|yellow|orange|purple|pink|cyan|magenta|silver|gold|brown|tan|navy|teal|maroon|lime|olive|aqua|fuchsia|transparent)\b/i);
453
453
  const numZone = colorStart >= 0 ? part.slice(0, colorStart) : part;
454
454
  const colorZone = colorStart >= 0 ? part.slice(colorStart) : "";
455
455
  // Extract numbers from the numeric zone only (safe — no hex digits here).
@@ -29,9 +29,9 @@ static inline CuttlefishCanvas16* ui_create_canvas_best(int16_t w, int16_t h) {
29
29
  if (!psram_logged) {
30
30
  psram_logged = 1;
31
31
  #if defined(ESP32) && defined(ARDUINO)
32
- Serial.printf("[psram] canvas %dx%d allocated in PSRAM (free=%u)\n", w, h, ESP.getFreePsram());
32
+ Serial.printf("[psram] canvas %dx%d allocated in PSRAM (free=%u)\\n", w, h, ESP.getFreePsram());
33
33
  #else
34
- printf("[psram] canvas %dx%d allocated in PSRAM\n", w, h);
34
+ printf("[psram] canvas %dx%d allocated in PSRAM\\n", w, h);
35
35
  #endif
36
36
  }
37
37
  return c;
@@ -47,7 +47,16 @@ struct UINodeDrawCtx {
47
47
  // Returns 1 when the node fully handled its own canvas push, decoration,
48
48
  // coordinate restore, and dirty-clear (NODE_LIST). Returns 0 otherwise, in
49
49
  // which case the caller is responsible for the post-switch epilogue.
50
- static inline uint8_t ui_draw_node_body(int16_t i, const UINodeDrawCtx* ctx) {
50
+ //
51
+ // The ctx parameter is typed const void* (cast back to UINodeDrawCtx* below):
52
+ // the Arduino .ino preprocessor auto-inserts a forward declaration of every
53
+ // function near the top of the sketch, BEFORE this struct is defined, so a
54
+ // struct-typed parameter makes that generated prototype fail to compile
55
+ // ("'UINodeDrawCtx' does not name a type"). Primitive-only parameters keep
56
+ // the auto-generated prototype valid; call sites still pass &ctx, which
57
+ // converts implicitly to const void*.
58
+ static inline uint8_t ui_draw_node_body(int16_t i, const void* rawCtx) {
59
+ const UINodeDrawCtx* ctx = static_cast<const UINodeDrawCtx*>(rawCtx);
51
60
  int16_t drawY = ctx->drawY;
52
61
  UI_COLOR_T bColor = ctx->bColor;
53
62
  UI_COLOR_T fillBg = ctx->fillBg;
@@ -3,6 +3,7 @@
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitTouchKeyboardFwd() {
5
5
  return `
6
+ #include <cstdlib> // abs() for drag-distance thresholds (self-sufficient slice)
6
7
  // ── Touch hit-testing + click dispatch ─────────────────────────────────────
7
8
  // Radio groups for mutual exclusion
8
9
  struct UIRadioGroup {
@@ -0,0 +1,49 @@
1
+ /** Walk up from `startDir` looking for cuttlefish.config.ts. */
2
+ export declare function findCuttlefishConfig(startDir: string): string | undefined;
3
+ /** A plain record as extracted from a config object literal. */
4
+ export type ConfigRecord = Record<string, unknown>;
5
+ /**
6
+ * Read a top-level section of the config (e.g. `display`) as a plain record.
7
+ * Returns undefined when the section is missing or not an object literal.
8
+ */
9
+ export declare function readConfigSection(sourceText: string, section: string): ConfigRecord | undefined;
10
+ /** Read the top-level `entry` scalar (the .ui/.ts entry path). */
11
+ export declare function readEntryPath(sourceText: string): string | undefined;
12
+ export interface RenderDisplayOptions {
13
+ /** Indentation of the property lines inside the braces (default 2 spaces). */
14
+ indent?: string;
15
+ /** Indentation of the closing brace (default: none). */
16
+ closingIndent?: string;
17
+ /** Line ending used in the rendered block (default "\n"). */
18
+ eol?: string;
19
+ /** Optional `// comment` lines rendered above the matching key. */
20
+ comments?: Record<string, string>;
21
+ }
22
+ /**
23
+ * Render the display section as an object-literal body (braces included).
24
+ * Key order follows insertion order of the record, matching the demos
25
+ * (profile/wiring first, touch last). Nested objects render inline.
26
+ */
27
+ export declare function renderDisplayBody(display: ConfigRecord, options?: RenderDisplayOptions): string;
28
+ /** Render the complete `display: { ... }` property text for a property that
29
+ * starts at `options.indent`. The returned text's first line is NOT indented
30
+ * — the caller positions the property start; body lines and the closing
31
+ * brace are indented relative to `options.indent`. */
32
+ export declare function renderDisplayProperty(display: ConfigRecord, options?: RenderDisplayOptions): string;
33
+ export interface UpsertDisplayResult {
34
+ text: string;
35
+ /** "replaced" — an existing display section was rewritten; "inserted" — none existed. */
36
+ mode: "replaced" | "inserted";
37
+ }
38
+ /**
39
+ * Insert or replace the `display` section in a cuttlefish.config.ts source
40
+ * string, preserving every other section and comment. Throws when the config
41
+ * has no recognizable default-export object literal.
42
+ */
43
+ export declare function upsertDisplaySection(sourceText: string, display: ConfigRecord, options?: RenderDisplayOptions): UpsertDisplayResult;
44
+ /**
45
+ * Syntactic sanity check for wizard output: returns the first error message
46
+ * when the text no longer parses as TypeScript, or null when it is clean.
47
+ * (Semantic checking is the build's job — this only guards the splice.)
48
+ */
49
+ export declare function findSyntaxError(text: string): string | null;
@@ -0,0 +1,366 @@
1
+ // ---------------------------------------------------------------------------
2
+ // cuttlefish.config.ts reader/writer for the @typecad/ui integration wizard.
3
+ //
4
+ // The build's config loader (packages/cuttlefish/src/config-loader.ts) is
5
+ // AST-based and deliberately never evaluates user code — only inline literals
6
+ // survive extraction. The writer here plays by the same rules: the wizard
7
+ // emits plain object literals and splices them into the config's default
8
+ // export object via the TypeScript AST, so every other section (and its
9
+ // comments) survives an update byte-for-byte.
10
+ // ---------------------------------------------------------------------------
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import ts from "typescript";
14
+ const CONFIG_FILENAME = "cuttlefish.config.ts";
15
+ /** Walk up from `startDir` looking for cuttlefish.config.ts. */
16
+ export function findCuttlefishConfig(startDir) {
17
+ let dir = path.resolve(startDir);
18
+ for (;;) {
19
+ const candidate = path.join(dir, CONFIG_FILENAME);
20
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
21
+ return candidate;
22
+ }
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir)
25
+ return undefined;
26
+ dir = parent;
27
+ }
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // AST helpers (kept aligned with the config loader's extraction rules)
31
+ // ---------------------------------------------------------------------------
32
+ function unwrapTypeCast(node) {
33
+ let curr = node;
34
+ for (;;) {
35
+ if (ts.isAsExpression(curr) || ts.isTypeAssertionExpression(curr) || ts.isParenthesizedExpression(curr)) {
36
+ curr = curr.expression;
37
+ continue;
38
+ }
39
+ const isSatisfies = ts.isSatisfiesExpression;
40
+ if (typeof isSatisfies === "function" && isSatisfies(curr)) {
41
+ curr = curr.expression;
42
+ continue;
43
+ }
44
+ return curr;
45
+ }
46
+ }
47
+ function propertyKeyName(prop) {
48
+ const name = prop.name;
49
+ if (!name)
50
+ return undefined;
51
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
52
+ }
53
+ function getScalarValue(node) {
54
+ const unwrapped = unwrapTypeCast(node);
55
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
56
+ return unwrapped.text;
57
+ }
58
+ if (ts.isNumericLiteral(unwrapped)) {
59
+ return Number(unwrapped.text);
60
+ }
61
+ if (ts.isPrefixUnaryExpression(unwrapped)
62
+ && (unwrapped.operator === ts.SyntaxKind.MinusToken || unwrapped.operator === ts.SyntaxKind.PlusToken)
63
+ && ts.isNumericLiteral(unwrapped.operand)) {
64
+ const magnitude = Number(unwrapped.operand.text);
65
+ return unwrapped.operator === ts.SyntaxKind.MinusToken ? -magnitude : magnitude;
66
+ }
67
+ if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
68
+ return true;
69
+ if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
70
+ return false;
71
+ return undefined;
72
+ }
73
+ function getStringLiteral(node) {
74
+ const unwrapped = unwrapTypeCast(node);
75
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
76
+ return unwrapped.text;
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Find the config's default-export object literal. Accepts the same shapes as
82
+ * the build's loader:
83
+ * 1. `export default { ... }`
84
+ * 2. `const config: CuttlefishConfig = { ... }; export default config;`
85
+ * with `as` / `satisfies` / parenthesized wrappers unwrapped.
86
+ */
87
+ function findConfigObjectLiteral(sourceFile) {
88
+ const variableDecls = new Map();
89
+ let defaultExportName;
90
+ let inlineDefaultObject;
91
+ for (const stmt of sourceFile.statements) {
92
+ if (ts.isVariableStatement(stmt)) {
93
+ for (const decl of stmt.declarationList.declarations) {
94
+ if (ts.isIdentifier(decl.name)) {
95
+ variableDecls.set(decl.name.text, decl);
96
+ }
97
+ }
98
+ }
99
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
100
+ const expr = unwrapTypeCast(stmt.expression);
101
+ if (ts.isIdentifier(expr)) {
102
+ defaultExportName = expr.text;
103
+ }
104
+ else if (ts.isObjectLiteralExpression(expr)) {
105
+ inlineDefaultObject = expr;
106
+ }
107
+ }
108
+ }
109
+ if (inlineDefaultObject)
110
+ return inlineDefaultObject;
111
+ if (defaultExportName) {
112
+ const decl = variableDecls.get(defaultExportName);
113
+ if (decl?.initializer) {
114
+ const init = unwrapTypeCast(decl.initializer);
115
+ if (ts.isObjectLiteralExpression(init))
116
+ return init;
117
+ }
118
+ }
119
+ return undefined;
120
+ }
121
+ function parseSource(sourceText) {
122
+ return ts.createSourceFile(CONFIG_FILENAME, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
123
+ }
124
+ function objectLiteralToRecord(obj) {
125
+ const result = {};
126
+ for (const prop of obj.properties) {
127
+ if (!ts.isPropertyAssignment(prop))
128
+ continue;
129
+ const key = propertyKeyName(prop);
130
+ if (!key)
131
+ continue;
132
+ const init = unwrapTypeCast(prop.initializer);
133
+ if (ts.isObjectLiteralExpression(init)) {
134
+ result[key] = objectLiteralToRecord(init);
135
+ }
136
+ else if (ts.isArrayLiteralExpression(init)) {
137
+ const items = [];
138
+ for (const elem of init.elements) {
139
+ const s = getStringLiteral(elem);
140
+ if (s === undefined)
141
+ break;
142
+ items.push(s);
143
+ }
144
+ if (items.length === init.elements.length && items.length > 0)
145
+ result[key] = items;
146
+ }
147
+ else {
148
+ const scalar = getScalarValue(init);
149
+ if (scalar !== undefined)
150
+ result[key] = scalar;
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+ /**
156
+ * Read a top-level section of the config (e.g. `display`) as a plain record.
157
+ * Returns undefined when the section is missing or not an object literal.
158
+ */
159
+ export function readConfigSection(sourceText, section) {
160
+ const obj = findConfigObjectLiteral(parseSource(sourceText));
161
+ if (!obj)
162
+ return undefined;
163
+ for (const prop of obj.properties) {
164
+ if (!ts.isPropertyAssignment(prop))
165
+ continue;
166
+ if (propertyKeyName(prop) !== section)
167
+ continue;
168
+ const init = unwrapTypeCast(prop.initializer);
169
+ if (!ts.isObjectLiteralExpression(init))
170
+ return undefined;
171
+ return objectLiteralToRecord(init);
172
+ }
173
+ return undefined;
174
+ }
175
+ /** Read the top-level `entry` scalar (the .ui/.ts entry path). */
176
+ export function readEntryPath(sourceText) {
177
+ const obj = findConfigObjectLiteral(parseSource(sourceText));
178
+ if (!obj)
179
+ return undefined;
180
+ for (const prop of obj.properties) {
181
+ if (!ts.isPropertyAssignment(prop))
182
+ continue;
183
+ if (propertyKeyName(prop) !== "entry")
184
+ continue;
185
+ const value = getStringLiteral(unwrapTypeCast(prop.initializer));
186
+ return value;
187
+ }
188
+ return undefined;
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Rendering — build the `display` object-literal text
192
+ // ---------------------------------------------------------------------------
193
+ /** Keys whose numeric values read best in hex (I2C addresses). */
194
+ const HEX_KEYS = new Set(["address", "i2cAddress"]);
195
+ function escapeString(value) {
196
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
197
+ }
198
+ function isPlainRecord(value) {
199
+ return typeof value === "object" && value !== null && !Array.isArray(value);
200
+ }
201
+ function renderValue(key, value) {
202
+ if (typeof value === "number") {
203
+ if (HEX_KEYS.has(key) && Number.isInteger(value) && value >= 0) {
204
+ return `0x${value.toString(16)}`;
205
+ }
206
+ return String(value);
207
+ }
208
+ if (typeof value === "boolean")
209
+ return value ? "true" : "false";
210
+ if (typeof value === "string")
211
+ return `'${escapeString(value)}'`;
212
+ if (Array.isArray(value)) {
213
+ return `[${value.map((item) => renderValue(key, item)).join(", ")}]`;
214
+ }
215
+ if (isPlainRecord(value)) {
216
+ const inner = Object.entries(value)
217
+ .map(([nestedKey, nestedValue]) => `${nestedKey}: ${renderValue(nestedKey, nestedValue)}`)
218
+ .join(", ");
219
+ return `{ ${inner} }`;
220
+ }
221
+ throw new Error(`Cannot render display config value for key "${key}" (${typeof value}).`);
222
+ }
223
+ /**
224
+ * Render the display section as an object-literal body (braces included).
225
+ * Key order follows insertion order of the record, matching the demos
226
+ * (profile/wiring first, touch last). Nested objects render inline.
227
+ */
228
+ export function renderDisplayBody(display, options = {}) {
229
+ const indent = options.indent ?? " ";
230
+ const closingIndent = options.closingIndent ?? "";
231
+ const eol = options.eol ?? "\n";
232
+ const lines = [];
233
+ for (const [key, value] of Object.entries(display)) {
234
+ const comment = options.comments?.[key];
235
+ if (comment) {
236
+ lines.push(`${indent}// ${comment}`);
237
+ }
238
+ lines.push(`${indent}${key}: ${renderValue(key, value)},`);
239
+ }
240
+ return `{${eol}${lines.join(eol)}${eol}${closingIndent}}`;
241
+ }
242
+ /** Render the complete `display: { ... }` property text for a property that
243
+ * starts at `options.indent`. The returned text's first line is NOT indented
244
+ * — the caller positions the property start; body lines and the closing
245
+ * brace are indented relative to `options.indent`. */
246
+ export function renderDisplayProperty(display, options = {}) {
247
+ const indent = options.indent ?? "";
248
+ return `display: ${renderDisplayBody(display, {
249
+ ...options,
250
+ indent: `${indent} `,
251
+ closingIndent: indent,
252
+ })}`;
253
+ }
254
+ // ---------------------------------------------------------------------------
255
+ // Splicing — insert or replace the display section inside the config
256
+ // ---------------------------------------------------------------------------
257
+ /** Leading whitespace of the line containing `pos` ("" when mid-line). */
258
+ function lineIndentAt(text, pos) {
259
+ const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
260
+ const before = text.slice(lineStart, pos);
261
+ return /^[ \t]*$/.test(before) ? before : "";
262
+ }
263
+ function detectLineEnding(text) {
264
+ return text.includes("\r\n") ? "\r\n" : "\n";
265
+ }
266
+ /** Skip whitespace and comments from `pos`; true when the next char is ','. */
267
+ function spanStartsWithComma(text, pos, end) {
268
+ let i = pos;
269
+ while (i < end) {
270
+ const ch = text[i];
271
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
272
+ i++;
273
+ continue;
274
+ }
275
+ if (ch === "/" && text[i + 1] === "/") {
276
+ const newline = text.indexOf("\n", i);
277
+ if (newline === -1 || newline >= end)
278
+ return false;
279
+ i = newline + 1;
280
+ continue;
281
+ }
282
+ if (ch === "/" && text[i + 1] === "*") {
283
+ const close = text.indexOf("*/", i);
284
+ if (close === -1 || close + 2 > end)
285
+ return false;
286
+ i = close + 2;
287
+ continue;
288
+ }
289
+ return ch === ",";
290
+ }
291
+ return false;
292
+ }
293
+ /**
294
+ * Insert or replace the `display` section in a cuttlefish.config.ts source
295
+ * string, preserving every other section and comment. Throws when the config
296
+ * has no recognizable default-export object literal.
297
+ */
298
+ export function upsertDisplaySection(sourceText, display, options = {}) {
299
+ const sourceFile = parseSource(sourceText);
300
+ const obj = findConfigObjectLiteral(sourceFile);
301
+ if (!obj) {
302
+ throw new Error("cuttlefish.config.ts has no recognizable config object — expected `export default { ... }` or `const config = { ... }; export default config;`.");
303
+ }
304
+ const eol = options.eol ?? detectLineEnding(sourceText);
305
+ const existing = obj.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyKeyName(prop) === "display");
306
+ if (existing) {
307
+ const indent = lineIndentAt(sourceText, existing.getStart());
308
+ const body = renderDisplayBody(display, { ...options, indent: `${indent} `, closingIndent: indent, eol });
309
+ const init = unwrapTypeCast(existing.initializer);
310
+ if (ts.isObjectLiteralExpression(init)) {
311
+ // Replace just the initializer — `display:` name and its comments stay.
312
+ return {
313
+ text: sourceText.slice(0, init.getStart()) + body + sourceText.slice(init.getEnd()),
314
+ mode: "replaced",
315
+ };
316
+ }
317
+ // `display: someExpression` — replace the whole property.
318
+ return {
319
+ text: sourceText.slice(0, existing.getStart()) + `display: ${body}` + sourceText.slice(existing.getEnd()),
320
+ mode: "replaced",
321
+ };
322
+ }
323
+ // No display section — insert before the object literal's closing brace.
324
+ const closeBrace = obj.getEnd() - 1;
325
+ const lastProp = obj.properties[obj.properties.length - 1];
326
+ const indent = lastProp
327
+ ? lineIndentAt(sourceText, lastProp.getStart())
328
+ : lineIndentAt(sourceText, obj.getStart()) + " ";
329
+ const body = renderDisplayBody(display, { ...options, indent: `${indent} `, closingIndent: indent, eol });
330
+ const closingIndent = lineIndentAt(sourceText, closeBrace);
331
+ let prefix = sourceText;
332
+ if (lastProp && !spanStartsWithComma(sourceText, lastProp.getEnd(), closeBrace)) {
333
+ prefix = sourceText.slice(0, lastProp.getEnd()) + "," + sourceText.slice(lastProp.getEnd());
334
+ }
335
+ // Recompute the brace position if a comma was inserted before it.
336
+ const insertedComma = prefix !== sourceText;
337
+ const closeBraceFinal = closeBrace + (insertedComma ? 1 : 0);
338
+ return {
339
+ text: prefix.slice(0, closeBraceFinal)
340
+ // Trailing comma keeps the multi-property style of the demos.
341
+ + `${indent}display: ${body},${eol}${closingIndent}`
342
+ + prefix.slice(closeBraceFinal),
343
+ mode: "inserted",
344
+ };
345
+ }
346
+ /**
347
+ * Syntactic sanity check for wizard output: returns the first error message
348
+ * when the text no longer parses as TypeScript, or null when it is clean.
349
+ * (Semantic checking is the build's job — this only guards the splice.)
350
+ */
351
+ export function findSyntaxError(text) {
352
+ const output = ts.transpileModule(text, {
353
+ reportDiagnostics: true,
354
+ compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext },
355
+ });
356
+ const errors = (output.diagnostics ?? []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
357
+ if (errors.length === 0)
358
+ return null;
359
+ const first = errors[0];
360
+ const message = ts.flattenDiagnosticMessageText(first.messageText, "\n");
361
+ if (typeof first.start === "number") {
362
+ const line = text.slice(0, first.start).split("\n").length;
363
+ return `line ${line}: ${message}`;
364
+ }
365
+ return message;
366
+ }
@@ -0,0 +1,75 @@
1
+ export type WizardBus = "spi" | "i2c" | "none";
2
+ export type WizardTouchKind = "none" | "spi" | "i2c" | "analog" | "adapter" | "sdl";
3
+ export interface DisplayCatalogEntry {
4
+ /** Catalog id — also the `display.profile` value for built-ins. */
5
+ id: string;
6
+ label: string;
7
+ hint: string;
8
+ /** Built-in profile name written to config (omitted for custom/sdl). */
9
+ profile?: string;
10
+ /** Driver name written when no built-in profile applies. */
11
+ driver?: string;
12
+ bus: WizardBus;
13
+ /** Wizard question defaults. */
14
+ defaults: {
15
+ cs?: number;
16
+ dc?: number;
17
+ rst?: number;
18
+ /** Backlight GPIO; undefined = don't ask, null = ask but default blank. */
19
+ backlight?: number | null;
20
+ /** Backlight pin the built-in profile drives when `backlight` is omitted. */
21
+ backlightProfileDefault?: number;
22
+ spiFrequency?: number;
23
+ address?: number;
24
+ width?: number;
25
+ height?: number;
26
+ /** Native panel size before rotation (used for capacitive touch calibration). */
27
+ nativeWidth?: number;
28
+ nativeHeight?: number;
29
+ rotation?: number;
30
+ colorOrder?: "rgb" | "bgr";
31
+ invert?: boolean;
32
+ antialias?: boolean;
33
+ };
34
+ /** Arduino libraries the driver needs (for the printed next steps). */
35
+ arduinoLibraries?: string[];
36
+ /** A wiring note printed with the summary. */
37
+ wiringNote?: string;
38
+ }
39
+ export declare const DISPLAY_CATALOG: readonly DisplayCatalogEntry[];
40
+ export interface TouchCatalogEntry {
41
+ /** `touch.library` value written to config (built-in libraries only). */
42
+ id: string;
43
+ label: string;
44
+ hint: string;
45
+ kind: WizardTouchKind;
46
+ defaults: {
47
+ cs?: number;
48
+ irq?: number;
49
+ i2cAddress?: number;
50
+ i2cFrequency?: number;
51
+ resetPin?: number;
52
+ /** Resistive raw-ADC calibration (capacitive uses panel pixel space). */
53
+ calibration?: {
54
+ xMin: number;
55
+ xMax: number;
56
+ yMin: number;
57
+ yMax: number;
58
+ };
59
+ analogPins?: {
60
+ xp: number;
61
+ yp: number;
62
+ xm: number;
63
+ ym: number;
64
+ rx: number;
65
+ };
66
+ };
67
+ arduinoLibrary?: string;
68
+ }
69
+ export declare const TOUCH_CATALOG: readonly TouchCatalogEntry[];
70
+ /**
71
+ * Collect GPIO collisions between the display wiring and the touch wiring so
72
+ * the wizard can warn before writing the config (a shared pin is almost
73
+ * always a mis-entered default rather than real hardware).
74
+ */
75
+ export declare function findPinConflicts(displayPins: Record<string, number | undefined>, touchPins: Record<string, number | undefined>): string[];