@boba-cli/dsl 0.1.0-alpha.1

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/index.cjs ADDED
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ var tea = require('@boba-cli/tea');
4
+ var key = require('@boba-cli/key');
5
+ var chapstick = require('@boba-cli/chapstick');
6
+ var spinner$1 = require('@boba-cli/spinner');
7
+ var textinput = require('@boba-cli/textinput');
8
+
9
+ // src/app-builder.ts
10
+ function render(node) {
11
+ if (typeof node === "string") {
12
+ return node;
13
+ }
14
+ if (isTextNode(node)) {
15
+ return renderTextNode(node);
16
+ }
17
+ if (isLayoutNode(node)) {
18
+ return renderLayoutNode(node);
19
+ }
20
+ if (isComponentView(node)) {
21
+ return node.view;
22
+ }
23
+ return "";
24
+ }
25
+ function isTextNode(node) {
26
+ return typeof node === "object" && "_type" in node && node._type === "text";
27
+ }
28
+ function isLayoutNode(node) {
29
+ return typeof node === "object" && "_type" in node && (node._type === "vstack" || node._type === "hstack");
30
+ }
31
+ function isComponentView(node) {
32
+ return typeof node === "object" && "_type" in node && node._type === "component";
33
+ }
34
+ function renderTextNode(node) {
35
+ let style = new chapstick.Style();
36
+ if (node._bold) {
37
+ style = style.bold(true);
38
+ }
39
+ if (node._dim && !node._foreground) {
40
+ style = style.foreground("#888888");
41
+ }
42
+ if (node._italic) {
43
+ style = style.italic(true);
44
+ }
45
+ if (node._foreground) {
46
+ style = style.foreground(node._foreground);
47
+ }
48
+ if (node._background) {
49
+ style = style.background(node._background);
50
+ }
51
+ return style.render(node.content);
52
+ }
53
+ function renderLayoutNode(node) {
54
+ const renderedChildren = node.children.map((child) => render(child)).filter((s) => s.length > 0);
55
+ if (node._type === "vstack") {
56
+ const separator2 = node.spacing > 0 ? "\n".repeat(node.spacing + 1) : "\n";
57
+ return renderedChildren.join(separator2);
58
+ }
59
+ const separator = node.spacing > 0 ? " ".repeat(node.spacing) : "";
60
+ return renderedChildren.join(separator);
61
+ }
62
+
63
+ // src/view/nodes.ts
64
+ function text(content) {
65
+ return createTextNode(content, false, false, false, void 0, void 0);
66
+ }
67
+ function createTextNode(content, bold, dim, italic, foreground, background) {
68
+ return {
69
+ _type: "text",
70
+ content,
71
+ _bold: bold,
72
+ _dim: dim,
73
+ _italic: italic,
74
+ _foreground: foreground,
75
+ _background: background,
76
+ bold() {
77
+ return createTextNode(content, true, dim, italic, foreground, background);
78
+ },
79
+ dim() {
80
+ return createTextNode(content, bold, true, italic, foreground, background);
81
+ },
82
+ italic() {
83
+ return createTextNode(content, bold, dim, true, foreground, background);
84
+ },
85
+ foreground(color) {
86
+ return createTextNode(content, bold, dim, italic, color, background);
87
+ },
88
+ background(color) {
89
+ return createTextNode(content, bold, dim, italic, foreground, color);
90
+ }
91
+ };
92
+ }
93
+ function vstack(...children) {
94
+ return {
95
+ _type: "vstack",
96
+ children,
97
+ spacing: 0
98
+ };
99
+ }
100
+ function hstack(...children) {
101
+ return {
102
+ _type: "hstack",
103
+ children,
104
+ spacing: 0
105
+ };
106
+ }
107
+ function spacer(height = 1) {
108
+ return "\n".repeat(height);
109
+ }
110
+ function divider(char = "\u2500", width = 40) {
111
+ return char.repeat(width);
112
+ }
113
+ function when(condition, node) {
114
+ return condition ? node : "";
115
+ }
116
+ function choose(condition, ifTrue, ifFalse) {
117
+ return condition ? ifTrue : ifFalse;
118
+ }
119
+ function map(items, render2) {
120
+ return items.map(render2);
121
+ }
122
+ function componentView(view) {
123
+ return {
124
+ _type: "component",
125
+ view
126
+ };
127
+ }
128
+
129
+ // src/app-builder.ts
130
+ var AppBuilder = class _AppBuilder {
131
+ #initialState;
132
+ #components;
133
+ #keyHandlers;
134
+ #viewFn;
135
+ constructor(initialState, components, keyHandlers, viewFn) {
136
+ this.#initialState = initialState;
137
+ this.#components = components;
138
+ this.#keyHandlers = keyHandlers;
139
+ this.#viewFn = viewFn;
140
+ }
141
+ /**
142
+ * Create a new AppBuilder instance.
143
+ * @internal
144
+ */
145
+ static create() {
146
+ return new _AppBuilder(void 0, [], [], void 0);
147
+ }
148
+ /**
149
+ * Set the initial application state.
150
+ *
151
+ * @remarks
152
+ * This should typically be called early in the builder chain. If called after
153
+ * registering key handlers or a view function, those will be preserved but
154
+ * their type information will be updated to reflect the new state type.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * createApp()
159
+ * .state({ count: 0, name: 'World' })
160
+ * ```
161
+ *
162
+ * @typeParam S - The application state type
163
+ * @param initial - The initial state object
164
+ * @returns A new {@link AppBuilder} with the state type parameter set
165
+ *
166
+ * @public
167
+ */
168
+ state(initial) {
169
+ return new _AppBuilder(
170
+ initial,
171
+ this.#components,
172
+ this.#keyHandlers,
173
+ this.#viewFn
174
+ );
175
+ }
176
+ /**
177
+ * Register a component with a unique key.
178
+ *
179
+ * @remarks
180
+ * Components are TEA models wrapped in a {@link ComponentBuilder} that
181
+ * manages their lifecycle. The component's rendered view is available in
182
+ * the view function via `components[key]`.
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * createApp()
187
+ * .component('loading', spinner())
188
+ * .component('input', textInput())
189
+ * ```
190
+ *
191
+ * @typeParam K - The component key (string literal type)
192
+ * @typeParam M - The component model type
193
+ * @param key - Unique identifier for this component
194
+ * @param builder - Component builder implementing init/update/view
195
+ * @returns A new {@link AppBuilder} with the component registered
196
+ *
197
+ * @public
198
+ */
199
+ component(key, builder) {
200
+ const newComponents = [...this.#components, { key, builder }];
201
+ return new _AppBuilder(
202
+ this.#initialState,
203
+ newComponents,
204
+ this.#keyHandlers,
205
+ this.#viewFn
206
+ );
207
+ }
208
+ /**
209
+ * Register a key handler.
210
+ *
211
+ * @remarks
212
+ * Key handlers receive an {@link EventContext} with the current state and
213
+ * components. Multiple keys can be bound to the same handler by passing an
214
+ * array of key strings. Key strings support modifiers like 'ctrl+c', 'alt+enter'.
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * createApp()
219
+ * .onKey('q', ({ quit }) => quit())
220
+ * .onKey(['up', 'k'], ({ state, update }) => update({ index: state.index - 1 }))
221
+ * .onKey('ctrl+c', ({ quit }) => quit())
222
+ * ```
223
+ *
224
+ * @param keys - Single key string or array of key strings
225
+ * @param handler - Function to call when any of the keys are pressed
226
+ * @returns A new {@link AppBuilder} with the key handler registered
227
+ *
228
+ * @public
229
+ */
230
+ onKey(keys, handler) {
231
+ const keyArray = Array.isArray(keys) ? keys : [keys];
232
+ const newHandlers = [...this.#keyHandlers, { keys: keyArray, handler }];
233
+ return new _AppBuilder(this.#initialState, this.#components, newHandlers, this.#viewFn);
234
+ }
235
+ /**
236
+ * Set the view function.
237
+ *
238
+ * @remarks
239
+ * The view function is called on every render cycle and receives the current
240
+ * state and component views. It should return a {@link ViewNode} tree
241
+ * describing the UI to display.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * createApp()
246
+ * .view(({ state, components }) => vstack(
247
+ * text('Hello ' + state.name),
248
+ * components.spinner
249
+ * ))
250
+ * ```
251
+ *
252
+ * @param fn - Function that returns a {@link ViewNode} tree
253
+ * @returns A new {@link AppBuilder} with the view function set
254
+ *
255
+ * @public
256
+ */
257
+ view(fn) {
258
+ return new _AppBuilder(this.#initialState, this.#components, this.#keyHandlers, fn);
259
+ }
260
+ /**
261
+ * Build the application.
262
+ *
263
+ * @remarks
264
+ * Finalizes the builder chain and creates an {@link App} instance ready
265
+ * to run. This method must be called after setting a view function via
266
+ * {@link AppBuilder.view}.
267
+ *
268
+ * @throws Error if no view function has been set
269
+ *
270
+ * @returns The built {@link App} ready to run
271
+ *
272
+ * @public
273
+ */
274
+ build() {
275
+ if (this.#viewFn === void 0) {
276
+ throw new Error("AppBuilder: view() must be called before build()");
277
+ }
278
+ const initialState = this.#initialState;
279
+ const components = this.#components;
280
+ const keyHandlers = this.#keyHandlers;
281
+ const viewFn = this.#viewFn;
282
+ const model = new GeneratedModel(initialState, components, keyHandlers, viewFn);
283
+ return {
284
+ async run() {
285
+ const program = new tea.Program(model);
286
+ const result = await program.run();
287
+ return { state: result.model.getUserState() };
288
+ },
289
+ getModel() {
290
+ return model;
291
+ }
292
+ };
293
+ }
294
+ };
295
+ function createApp() {
296
+ return AppBuilder.create();
297
+ }
298
+ var GeneratedModel = class _GeneratedModel {
299
+ #userState;
300
+ #componentModels;
301
+ #componentBuilders;
302
+ #keyHandlers;
303
+ #viewFn;
304
+ constructor(userState, components, keyHandlers, viewFn, componentModels) {
305
+ this.#userState = userState;
306
+ this.#keyHandlers = keyHandlers;
307
+ this.#viewFn = viewFn;
308
+ this.#componentBuilders = /* @__PURE__ */ new Map();
309
+ for (const { key, builder } of components) {
310
+ this.#componentBuilders.set(key, builder);
311
+ }
312
+ this.#componentModels = componentModels ?? /* @__PURE__ */ new Map();
313
+ }
314
+ getUserState() {
315
+ return this.#userState;
316
+ }
317
+ init() {
318
+ const cmds = [];
319
+ for (const [key, builder] of this.#componentBuilders) {
320
+ const [model, cmd] = builder.init();
321
+ this.#componentModels.set(key, model);
322
+ if (cmd) {
323
+ cmds.push(cmd);
324
+ }
325
+ }
326
+ return cmds.length > 0 ? tea.batch(...cmds) : null;
327
+ }
328
+ update(msg) {
329
+ if (msg instanceof tea.KeyMsg) {
330
+ for (const { keys, handler } of this.#keyHandlers) {
331
+ const binding = key.newBinding({ keys });
332
+ if (key.matches(msg, binding)) {
333
+ let nextUserState = this.#userState;
334
+ let shouldQuit = false;
335
+ const ctx = {
336
+ state: this.#userState,
337
+ components: this.#buildComponentViews(),
338
+ update: (patch) => {
339
+ nextUserState = { ...nextUserState, ...patch };
340
+ },
341
+ setState: (newState) => {
342
+ nextUserState = newState;
343
+ },
344
+ quit: () => {
345
+ shouldQuit = true;
346
+ }
347
+ };
348
+ handler(ctx);
349
+ if (shouldQuit) {
350
+ return [this, tea.quit()];
351
+ }
352
+ if (nextUserState !== this.#userState) {
353
+ return [this.#withUserState(nextUserState), null];
354
+ }
355
+ return [this, null];
356
+ }
357
+ }
358
+ }
359
+ const cmds = [];
360
+ let anyComponentChanged = false;
361
+ const newComponentModels = new Map(this.#componentModels);
362
+ for (const [key, builder] of this.#componentBuilders) {
363
+ const currentModel = this.#componentModels.get(key);
364
+ if (currentModel === void 0) {
365
+ continue;
366
+ }
367
+ const [nextModel, cmd] = builder.update(currentModel, msg);
368
+ if (nextModel !== currentModel) {
369
+ newComponentModels.set(key, nextModel);
370
+ anyComponentChanged = true;
371
+ }
372
+ if (cmd) {
373
+ cmds.push(cmd);
374
+ }
375
+ }
376
+ if (anyComponentChanged) {
377
+ const next = new _GeneratedModel(
378
+ this.#userState,
379
+ Array.from(this.#componentBuilders.entries()).map(([key, builder]) => ({
380
+ key,
381
+ builder
382
+ })),
383
+ this.#keyHandlers,
384
+ this.#viewFn,
385
+ newComponentModels
386
+ );
387
+ return [next, cmds.length > 0 ? tea.batch(...cmds) : null];
388
+ }
389
+ return [this, cmds.length > 0 ? tea.batch(...cmds) : null];
390
+ }
391
+ view() {
392
+ if (!this.#viewFn) {
393
+ return "";
394
+ }
395
+ const componentViews = this.#buildComponentViews();
396
+ const node = this.#viewFn({
397
+ state: this.#userState,
398
+ components: componentViews
399
+ });
400
+ return render(node);
401
+ }
402
+ #buildComponentViews() {
403
+ const views = {};
404
+ for (const [key, builder] of this.#componentBuilders) {
405
+ const model = this.#componentModels.get(key);
406
+ if (model !== void 0) {
407
+ views[key] = componentView(builder.view(model));
408
+ }
409
+ }
410
+ return views;
411
+ }
412
+ #withUserState(newUserState) {
413
+ return new _GeneratedModel(
414
+ newUserState,
415
+ Array.from(this.#componentBuilders.entries()).map(([key, builder]) => ({
416
+ key,
417
+ builder
418
+ })),
419
+ this.#keyHandlers,
420
+ this.#viewFn,
421
+ this.#componentModels
422
+ );
423
+ }
424
+ };
425
+ function spinner(options = {}) {
426
+ const spinnerOpts = {
427
+ spinner: options.spinner ?? spinner$1.line,
428
+ style: options.style ?? new chapstick.Style()
429
+ };
430
+ return {
431
+ init() {
432
+ const model = new spinner$1.SpinnerModel(spinnerOpts);
433
+ return [model, model.tick()];
434
+ },
435
+ update(model, msg) {
436
+ return model.update(msg);
437
+ },
438
+ view(model) {
439
+ return model.view();
440
+ }
441
+ };
442
+ }
443
+ function textInput(options = {}) {
444
+ const inputOpts = {
445
+ placeholder: options.placeholder ?? "",
446
+ width: options.width,
447
+ echoMode: options.echoMode ?? textinput.EchoMode.Normal,
448
+ charLimit: options.charLimit ?? 0,
449
+ prompt: options.prompt ?? "",
450
+ promptStyle: options.promptStyle ?? new chapstick.Style(),
451
+ textStyle: options.textStyle ?? new chapstick.Style(),
452
+ placeholderStyle: options.placeholderStyle ?? new chapstick.Style(),
453
+ validate: options.validate
454
+ };
455
+ return {
456
+ init() {
457
+ const model = textinput.TextInputModel.new(inputOpts);
458
+ const [focused, cmd] = model.focus();
459
+ return [focused, cmd];
460
+ },
461
+ update(model, msg) {
462
+ return model.update(msg);
463
+ },
464
+ view(model) {
465
+ return model.view();
466
+ }
467
+ };
468
+ }
469
+
470
+ Object.defineProperty(exports, "Style", {
471
+ enumerable: true,
472
+ get: function () { return chapstick.Style; }
473
+ });
474
+ Object.defineProperty(exports, "dot", {
475
+ enumerable: true,
476
+ get: function () { return spinner$1.dot; }
477
+ });
478
+ Object.defineProperty(exports, "ellipsis", {
479
+ enumerable: true,
480
+ get: function () { return spinner$1.ellipsis; }
481
+ });
482
+ Object.defineProperty(exports, "line", {
483
+ enumerable: true,
484
+ get: function () { return spinner$1.line; }
485
+ });
486
+ Object.defineProperty(exports, "meter", {
487
+ enumerable: true,
488
+ get: function () { return spinner$1.meter; }
489
+ });
490
+ Object.defineProperty(exports, "miniDot", {
491
+ enumerable: true,
492
+ get: function () { return spinner$1.miniDot; }
493
+ });
494
+ Object.defineProperty(exports, "moon", {
495
+ enumerable: true,
496
+ get: function () { return spinner$1.moon; }
497
+ });
498
+ Object.defineProperty(exports, "points", {
499
+ enumerable: true,
500
+ get: function () { return spinner$1.points; }
501
+ });
502
+ Object.defineProperty(exports, "pulse", {
503
+ enumerable: true,
504
+ get: function () { return spinner$1.pulse; }
505
+ });
506
+ Object.defineProperty(exports, "CursorMode", {
507
+ enumerable: true,
508
+ get: function () { return textinput.CursorMode; }
509
+ });
510
+ Object.defineProperty(exports, "EchoMode", {
511
+ enumerable: true,
512
+ get: function () { return textinput.EchoMode; }
513
+ });
514
+ exports.AppBuilder = AppBuilder;
515
+ exports.choose = choose;
516
+ exports.createApp = createApp;
517
+ exports.divider = divider;
518
+ exports.hstack = hstack;
519
+ exports.map = map;
520
+ exports.render = render;
521
+ exports.spacer = spacer;
522
+ exports.spinner = spinner;
523
+ exports.text = text;
524
+ exports.textInput = textInput;
525
+ exports.vstack = vstack;
526
+ exports.when = when;
527
+ //# sourceMappingURL=index.cjs.map
528
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/view/renderer.ts","../src/view/nodes.ts","../src/app-builder.ts","../src/components/spinner.ts","../src/components/textinput.ts"],"names":["Style","separator","render","Program","batch","KeyMsg","newBinding","matches","teaQuit","line","SpinnerModel","EchoMode","TextInputModel"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Style } from '@boba-cli/chapstick'\nimport type { ViewNode, TextNode, LayoutNode, ComponentView } from '../types.js'\n\n/**\n * Render a view node tree to a string.\n *\n * @remarks\n * Recursively renders a {@link ViewNode} tree into a terminal-ready string.\n * Handles text styling, layout stacking, and component views. This function\n * is typically called internally by the DSL, but can be used directly for\n * testing or custom rendering.\n *\n * @example\n * ```typescript\n * const tree = vstack(\n * text('Hello').bold(),\n * text('World').foreground('#ff79c6')\n * )\n * const output = render(tree)\n * console.log(output)\n * ```\n *\n * @param node - The view node tree to render\n * @returns A string ready to display in the terminal\n *\n * @public\n */\nexport function render(node: ViewNode): string {\n if (typeof node === 'string') {\n return node\n }\n\n if (isTextNode(node)) {\n return renderTextNode(node)\n }\n\n if (isLayoutNode(node)) {\n return renderLayoutNode(node)\n }\n\n if (isComponentView(node)) {\n return node.view\n }\n\n // Should not reach here, but TypeScript doesn't know that\n return ''\n}\n\nfunction isTextNode(node: ViewNode): node is TextNode {\n return typeof node === 'object' && '_type' in node && node._type === 'text'\n}\n\nfunction isLayoutNode(node: ViewNode): node is LayoutNode {\n return (\n typeof node === 'object' &&\n '_type' in node &&\n (node._type === 'vstack' || node._type === 'hstack')\n )\n}\n\nfunction isComponentView(node: ViewNode): node is ComponentView {\n return typeof node === 'object' && '_type' in node && node._type === 'component'\n}\n\nfunction renderTextNode(node: TextNode): string {\n let style = new Style()\n\n if (node._bold) {\n style = style.bold(true)\n }\n // Note: dim is rendered as a darker color when foreground is set\n // If dim and no foreground, use a gray color\n if (node._dim && !node._foreground) {\n style = style.foreground('#888888')\n }\n if (node._italic) {\n style = style.italic(true)\n }\n if (node._foreground) {\n style = style.foreground(node._foreground)\n }\n if (node._background) {\n style = style.background(node._background)\n }\n\n return style.render(node.content)\n}\n\nfunction renderLayoutNode(node: LayoutNode): string {\n const renderedChildren = node.children\n .map((child) => render(child))\n .filter((s) => s.length > 0)\n\n if (node._type === 'vstack') {\n const separator = node.spacing > 0 ? '\\n'.repeat(node.spacing + 1) : '\\n'\n return renderedChildren.join(separator)\n }\n\n // hstack\n const separator = node.spacing > 0 ? ' '.repeat(node.spacing) : ''\n return renderedChildren.join(separator)\n}\n","import type { TextNode, LayoutNode, ViewNode, ComponentView } from '../types.js'\n\n/**\n * Create a text node with chainable style methods.\n *\n * @remarks\n * Text nodes support fluent styling via methods like `bold()`,\n * `dim()`, `italic()`, `foreground()`, and `background()`.\n *\n * @example\n * ```typescript\n * text('Hello').bold().foreground('#ff79c6')\n * text('Warning').dim()\n * ```\n *\n * @param content - The text content to display\n * @returns A new {@link TextNode}\n *\n * @public\n */\nexport function text(content: string): TextNode {\n return createTextNode(content, false, false, false, undefined, undefined)\n}\n\nfunction createTextNode(\n content: string,\n bold: boolean,\n dim: boolean,\n italic: boolean,\n foreground: string | undefined,\n background: string | undefined,\n): TextNode {\n return {\n _type: 'text',\n content,\n _bold: bold,\n _dim: dim,\n _italic: italic,\n _foreground: foreground,\n _background: background,\n bold() {\n return createTextNode(content, true, dim, italic, foreground, background)\n },\n dim() {\n return createTextNode(content, bold, true, italic, foreground, background)\n },\n italic() {\n return createTextNode(content, bold, dim, true, foreground, background)\n },\n foreground(color: string) {\n return createTextNode(content, bold, dim, italic, color, background)\n },\n background(color: string) {\n return createTextNode(content, bold, dim, italic, foreground, color)\n },\n }\n}\n\n/**\n * Create a vertical stack layout.\n *\n * @remarks\n * Arranges child views vertically with newlines between them. Children are\n * rendered in order from top to bottom.\n *\n * @example\n * ```typescript\n * vstack(\n * text('Line 1'),\n * text('Line 2'),\n * text('Line 3')\n * )\n * ```\n *\n * @param children - View nodes to stack vertically\n * @returns A new {@link LayoutNode} with vertical stacking\n *\n * @public\n */\nexport function vstack(...children: ViewNode[]): LayoutNode {\n return {\n _type: 'vstack',\n children,\n spacing: 0,\n }\n}\n\n/**\n * Create a horizontal stack layout.\n *\n * @remarks\n * Arranges child views horizontally on the same line. Children are\n * rendered in order from left to right.\n *\n * @example\n * ```typescript\n * hstack(\n * text('Left'),\n * text(' | '),\n * text('Right')\n * )\n * ```\n *\n * @param children - View nodes to stack horizontally\n * @returns A new {@link LayoutNode} with horizontal stacking\n *\n * @public\n */\nexport function hstack(...children: ViewNode[]): LayoutNode {\n return {\n _type: 'hstack',\n children,\n spacing: 0,\n }\n}\n\n/**\n * Create empty vertical space.\n *\n * @remarks\n * Useful for adding vertical spacing between sections of your UI.\n *\n * @example\n * ```typescript\n * vstack(\n * text('Header'),\n * spacer(2),\n * text('Content')\n * )\n * ```\n *\n * @param height - Number of blank lines to insert (default: 1)\n * @returns A string containing the specified number of newlines\n *\n * @public\n */\nexport function spacer(height = 1): string {\n return '\\n'.repeat(height)\n}\n\n/**\n * Create a divider line.\n *\n * @remarks\n * Renders a horizontal line using a repeated character.\n *\n * @example\n * ```typescript\n * vstack(\n * text('Section 1'),\n * divider(),\n * text('Section 2'),\n * divider('=', 50)\n * )\n * ```\n *\n * @param char - Character to repeat (default: '─')\n * @param width - Number of times to repeat the character (default: 40)\n * @returns A string containing the divider line\n *\n * @public\n */\nexport function divider(char = '─', width = 40): string {\n return char.repeat(width)\n}\n\n/**\n * Conditionally render a node.\n *\n * @remarks\n * Returns the node if the condition is true, otherwise returns an empty string.\n *\n * @example\n * ```typescript\n * vstack(\n * text('Always visible'),\n * when(state.showHelp, text('Help text'))\n * )\n * ```\n *\n * @param condition - Boolean condition to test\n * @param node - View node to render if condition is true\n * @returns The node if condition is true, empty string otherwise\n *\n * @public\n */\nexport function when(condition: boolean, node: ViewNode): ViewNode {\n return condition ? node : ''\n}\n\n/**\n * Choose between two nodes based on condition.\n *\n * @remarks\n * Returns one node if the condition is true, another if false.\n *\n * @example\n * ```typescript\n * vstack(\n * choose(\n * state.isLoading,\n * text('Loading...').dim(),\n * text('Ready!').bold()\n * )\n * )\n * ```\n *\n * @param condition - Boolean condition to test\n * @param ifTrue - View node to render if condition is true\n * @param ifFalse - View node to render if condition is false\n * @returns Either ifTrue or ifFalse depending on condition\n *\n * @public\n */\nexport function choose(condition: boolean, ifTrue: ViewNode, ifFalse: ViewNode): ViewNode {\n return condition ? ifTrue : ifFalse\n}\n\n/**\n * Map items to view nodes.\n *\n * @remarks\n * Transforms an array of items into an array of view nodes. The render\n * function receives each item and its index.\n *\n * @example\n * ```typescript\n * vstack(\n * ...map(state.items, (item, index) =>\n * text(`${index + 1}. ${item.name}`)\n * )\n * )\n * ```\n *\n * @typeParam T - The type of items in the array\n * @param items - Array of items to map\n * @param render - Function to transform each item into a view node\n * @returns Array of view nodes\n *\n * @public\n */\nexport function map<T>(items: T[], render: (item: T, index: number) => ViewNode): ViewNode[] {\n return items.map(render)\n}\n\n/**\n * Create a component view wrapper.\n * @internal\n */\nexport function componentView(view: string): ComponentView {\n return {\n _type: 'component',\n view,\n }\n}\n","import {\n Program,\n KeyMsg,\n quit as teaQuit,\n batch,\n type Cmd,\n type Model,\n type Msg,\n} from '@boba-cli/tea'\nimport { newBinding, matches } from '@boba-cli/key'\nimport type {\n App,\n ComponentBuilder,\n EventContext,\n KeyHandler,\n ViewFunction,\n ComponentView,\n} from './types.js'\nimport { render } from './view/renderer.js'\nimport { componentView } from './view/nodes.js'\n\n/**\n * Internal key handler registration.\n * @internal\n */\ninterface KeyHandlerEntry<State, Components extends Record<string, unknown>> {\n keys: string[]\n handler: KeyHandler<State, Components>\n}\n\n/**\n * Internal component registration.\n * @internal\n */\ninterface ComponentEntry {\n key: string\n builder: ComponentBuilder<unknown>\n}\n\n/**\n * Builder for creating declarative CLI applications.\n *\n * @example\n * ```typescript\n * const app = createApp()\n * .state({ count: 0 })\n * .component('spinner', spinner())\n * .onKey('q', ({ quit }) => quit())\n * .view(({ state, components }) => vstack(\n * text('Count: ' + state.count),\n * components.spinner\n * ))\n * .build()\n *\n * await app.run()\n * ```\n *\n * @public\n */\nexport class AppBuilder<\n State = undefined,\n Components extends Record<string, unknown> = Record<string, never>,\n> {\n readonly #initialState: State | undefined\n readonly #components: ComponentEntry[]\n readonly #keyHandlers: KeyHandlerEntry<State, Components>[]\n readonly #viewFn: ViewFunction<State, Components> | undefined\n\n private constructor(\n initialState: State | undefined,\n components: ComponentEntry[],\n keyHandlers: KeyHandlerEntry<State, Components>[],\n viewFn: ViewFunction<State, Components> | undefined,\n ) {\n this.#initialState = initialState\n this.#components = components\n this.#keyHandlers = keyHandlers\n this.#viewFn = viewFn\n }\n\n /**\n * Create a new AppBuilder instance.\n * @internal\n */\n static create(): AppBuilder<undefined, Record<string, never>> {\n return new AppBuilder(undefined, [], [], undefined)\n }\n\n /**\n * Set the initial application state.\n *\n * @remarks\n * This should typically be called early in the builder chain. If called after\n * registering key handlers or a view function, those will be preserved but\n * their type information will be updated to reflect the new state type.\n *\n * @example\n * ```typescript\n * createApp()\n * .state({ count: 0, name: 'World' })\n * ```\n *\n * @typeParam S - The application state type\n * @param initial - The initial state object\n * @returns A new {@link AppBuilder} with the state type parameter set\n *\n * @public\n */\n state<S>(initial: S): AppBuilder<S, Components> {\n // Preserve existing handlers and view function with updated type\n // This is safe because the handlers/view will receive the new state type\n return new AppBuilder(\n initial,\n this.#components,\n this.#keyHandlers as unknown as KeyHandlerEntry<S, Components>[],\n this.#viewFn as unknown as ViewFunction<S, Components> | undefined,\n )\n }\n\n /**\n * Register a component with a unique key.\n *\n * @remarks\n * Components are TEA models wrapped in a {@link ComponentBuilder} that\n * manages their lifecycle. The component's rendered view is available in\n * the view function via `components[key]`.\n *\n * @example\n * ```typescript\n * createApp()\n * .component('loading', spinner())\n * .component('input', textInput())\n * ```\n *\n * @typeParam K - The component key (string literal type)\n * @typeParam M - The component model type\n * @param key - Unique identifier for this component\n * @param builder - Component builder implementing init/update/view\n * @returns A new {@link AppBuilder} with the component registered\n *\n * @public\n */\n component<K extends string, M>(\n key: K,\n builder: ComponentBuilder<M>,\n ): AppBuilder<State, Components & Record<K, M>> {\n const newComponents = [...this.#components, { key, builder: builder as ComponentBuilder<unknown> }]\n return new AppBuilder(\n this.#initialState,\n newComponents,\n this.#keyHandlers as KeyHandlerEntry<State, Components & Record<K, M>>[],\n this.#viewFn as ViewFunction<State, Components & Record<K, M>> | undefined,\n )\n }\n\n /**\n * Register a key handler.\n *\n * @remarks\n * Key handlers receive an {@link EventContext} with the current state and\n * components. Multiple keys can be bound to the same handler by passing an\n * array of key strings. Key strings support modifiers like 'ctrl+c', 'alt+enter'.\n *\n * @example\n * ```typescript\n * createApp()\n * .onKey('q', ({ quit }) => quit())\n * .onKey(['up', 'k'], ({ state, update }) => update({ index: state.index - 1 }))\n * .onKey('ctrl+c', ({ quit }) => quit())\n * ```\n *\n * @param keys - Single key string or array of key strings\n * @param handler - Function to call when any of the keys are pressed\n * @returns A new {@link AppBuilder} with the key handler registered\n *\n * @public\n */\n onKey(\n keys: string | string[],\n handler: KeyHandler<State, Components>,\n ): AppBuilder<State, Components> {\n const keyArray = Array.isArray(keys) ? keys : [keys]\n const newHandlers = [...this.#keyHandlers, { keys: keyArray, handler }]\n return new AppBuilder(this.#initialState, this.#components, newHandlers, this.#viewFn)\n }\n\n /**\n * Set the view function.\n *\n * @remarks\n * The view function is called on every render cycle and receives the current\n * state and component views. It should return a {@link ViewNode} tree\n * describing the UI to display.\n *\n * @example\n * ```typescript\n * createApp()\n * .view(({ state, components }) => vstack(\n * text('Hello ' + state.name),\n * components.spinner\n * ))\n * ```\n *\n * @param fn - Function that returns a {@link ViewNode} tree\n * @returns A new {@link AppBuilder} with the view function set\n *\n * @public\n */\n view(fn: ViewFunction<State, Components>): AppBuilder<State, Components> {\n return new AppBuilder(this.#initialState, this.#components, this.#keyHandlers, fn)\n }\n\n /**\n * Build the application.\n *\n * @remarks\n * Finalizes the builder chain and creates an {@link App} instance ready\n * to run. This method must be called after setting a view function via\n * {@link AppBuilder.view}.\n *\n * @throws Error if no view function has been set\n *\n * @returns The built {@link App} ready to run\n *\n * @public\n */\n build(): App<State, Components> {\n if (this.#viewFn === undefined) {\n throw new Error('AppBuilder: view() must be called before build()')\n }\n\n const initialState = this.#initialState as State\n const components = this.#components\n const keyHandlers = this.#keyHandlers\n const viewFn = this.#viewFn\n\n // Create the generated model\n const model = new GeneratedModel(initialState, components, keyHandlers, viewFn)\n\n return {\n async run() {\n const program = new Program(model)\n const result = await program.run()\n return { state: result.model.getUserState() }\n },\n getModel() {\n return model\n },\n }\n }\n}\n\n/**\n * Create a new application builder.\n *\n * @example\n * ```typescript\n * const app = createApp()\n * .state({ count: 0 })\n * .onKey('q', ({ quit }) => quit())\n * .view(({ state }) => text('Count: ' + state.count))\n * .build()\n * ```\n *\n * @public\n */\nexport function createApp(): AppBuilder<undefined, Record<string, never>> {\n return AppBuilder.create()\n}\n\n/**\n * Generated TEA model from the builder configuration.\n * @internal\n */\nclass GeneratedModel<State, Components extends Record<string, unknown>>\n implements Model<Msg, GeneratedModel<State, Components>>\n{\n readonly #userState: State\n readonly #componentModels: Map<string, unknown>\n readonly #componentBuilders: Map<string, ComponentBuilder<unknown>>\n readonly #keyHandlers: KeyHandlerEntry<State, Components>[]\n readonly #viewFn: ViewFunction<State, Components> | undefined\n\n constructor(\n userState: State,\n components: ComponentEntry[],\n keyHandlers: KeyHandlerEntry<State, Components>[],\n viewFn: ViewFunction<State, Components> | undefined,\n componentModels?: Map<string, unknown>,\n ) {\n this.#userState = userState\n this.#keyHandlers = keyHandlers\n this.#viewFn = viewFn\n\n // Build component builders map\n this.#componentBuilders = new Map()\n for (const { key, builder } of components) {\n this.#componentBuilders.set(key, builder)\n }\n\n // Use provided component models or empty map (init will populate)\n this.#componentModels = componentModels ?? new Map<string, unknown>()\n }\n\n getUserState(): State {\n return this.#userState\n }\n\n init(): Cmd<Msg> {\n const cmds: Cmd<Msg>[] = []\n\n // Initialize all components\n for (const [key, builder] of this.#componentBuilders) {\n const [model, cmd] = builder.init()\n this.#componentModels.set(key, model)\n if (cmd) {\n cmds.push(cmd)\n }\n }\n\n return cmds.length > 0 ? batch(...cmds) : null\n }\n\n update(msg: Msg): [GeneratedModel<State, Components>, Cmd<Msg>] {\n // Check key handlers first\n if (msg instanceof KeyMsg) {\n for (const { keys, handler } of this.#keyHandlers) {\n const binding = newBinding({ keys })\n if (matches(msg, binding)) {\n // Create event context and call handler\n let nextUserState = this.#userState\n let shouldQuit = false\n\n const ctx: EventContext<State, Components> = {\n state: this.#userState,\n components: this.#buildComponentViews(),\n update: (patch) => {\n nextUserState = { ...nextUserState, ...patch }\n },\n setState: (newState) => {\n nextUserState = newState\n },\n quit: () => {\n shouldQuit = true\n },\n }\n\n handler(ctx)\n\n if (shouldQuit) {\n return [this, teaQuit()]\n }\n\n if (nextUserState !== this.#userState) {\n return [this.#withUserState(nextUserState), null]\n }\n\n return [this, null]\n }\n }\n }\n\n // Route message to all components\n const cmds: Cmd<Msg>[] = []\n let anyComponentChanged = false\n const newComponentModels = new Map(this.#componentModels)\n\n for (const [key, builder] of this.#componentBuilders) {\n const currentModel = this.#componentModels.get(key)\n if (currentModel === undefined) {\n continue\n }\n\n const [nextModel, cmd] = builder.update(currentModel, msg)\n\n if (nextModel !== currentModel) {\n newComponentModels.set(key, nextModel)\n anyComponentChanged = true\n }\n\n if (cmd) {\n cmds.push(cmd)\n }\n }\n\n if (anyComponentChanged) {\n const next = new GeneratedModel(\n this.#userState,\n Array.from(this.#componentBuilders.entries()).map(([key, builder]) => ({\n key,\n builder,\n })),\n this.#keyHandlers,\n this.#viewFn,\n newComponentModels,\n )\n return [next, cmds.length > 0 ? batch(...cmds) : null]\n }\n\n return [this, cmds.length > 0 ? batch(...cmds) : null]\n }\n\n view(): string {\n if (!this.#viewFn) {\n return ''\n }\n\n const componentViews = this.#buildComponentViews()\n const node = this.#viewFn({\n state: this.#userState,\n components: componentViews,\n })\n\n return render(node)\n }\n\n #buildComponentViews(): { [K in keyof Components]: ComponentView } {\n const views: Record<string, ComponentView> = {}\n\n for (const [key, builder] of this.#componentBuilders) {\n const model = this.#componentModels.get(key)\n if (model !== undefined) {\n views[key] = componentView(builder.view(model))\n }\n }\n\n return views as { [K in keyof Components]: ComponentView }\n }\n\n #withUserState(newUserState: State): GeneratedModel<State, Components> {\n return new GeneratedModel(\n newUserState,\n Array.from(this.#componentBuilders.entries()).map(([key, builder]) => ({\n key,\n builder,\n })),\n this.#keyHandlers,\n this.#viewFn,\n this.#componentModels,\n )\n }\n}\n","import { type Cmd, type Msg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport { SpinnerModel, type Spinner, line, type SpinnerOptions } from '@boba-cli/spinner'\nimport type { ComponentBuilder } from '../types.js'\n\n/**\n * Options for the spinner component builder.\n *\n * @remarks\n * Configure the spinner animation and styling when creating a spinner component.\n *\n * @public\n */\nexport interface SpinnerBuilderOptions {\n /**\n * Spinner animation to use (default: `line`).\n *\n * @remarks\n * Available spinners include `line`, `dot`, `miniDot`,\n * `pulse`, `points`, `moon`, `meter`, and `ellipsis`.\n */\n spinner?: Spinner\n /**\n * Style for rendering the spinner.\n *\n * @remarks\n * Uses `Style` from `@boba-cli/chapstick` to apply terminal colors and formatting.\n */\n style?: Style\n}\n\n/**\n * Create a spinner component builder.\n *\n * @remarks\n * Creates a {@link ComponentBuilder} wrapping the `@boba-cli/spinner` package.\n * The spinner automatically animates and can be styled with custom colors.\n *\n * @example\n * Basic usage:\n * ```typescript\n * const app = createApp()\n * .component('loading', spinner())\n * .view(({ components }) => components.loading)\n * .build()\n * ```\n *\n * @example\n * With custom styling:\n * ```typescript\n * const app = createApp()\n * .component('loading', spinner({\n * style: new Style().foreground('#50fa7b')\n * }))\n * .view(({ components }) => hstack(\n * components.loading,\n * text('Loading...')\n * ))\n * .build()\n * ```\n *\n * @param options - Configuration options for the spinner\n * @returns A {@link ComponentBuilder} ready to use with {@link AppBuilder.component}\n *\n * @public\n */\nexport function spinner(options: SpinnerBuilderOptions = {}): ComponentBuilder<SpinnerModel> {\n const spinnerOpts: SpinnerOptions = {\n spinner: options.spinner ?? line,\n style: options.style ?? new Style(),\n }\n\n return {\n init(): [SpinnerModel, Cmd<Msg>] {\n const model = new SpinnerModel(spinnerOpts)\n return [model, model.tick() as Cmd<Msg>]\n },\n\n update(model: SpinnerModel, msg: Msg): [SpinnerModel, Cmd<Msg>] {\n return model.update(msg)\n },\n\n view(model: SpinnerModel): string {\n return model.view()\n },\n }\n}\n","import { type Cmd, type Msg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport {\n TextInputModel,\n EchoMode,\n type TextInputOptions,\n type ValidateFunc,\n} from '@boba-cli/textinput'\nimport type { ComponentBuilder } from '../types.js'\n\n/**\n * Options for the textInput component builder.\n * @public\n */\nexport interface TextInputBuilderOptions {\n /** Placeholder text shown when input is empty. */\n placeholder?: string\n /** Width constraint for the input field. */\n width?: number\n /** Echo mode for input display (Normal, Password, or None). */\n echoMode?: EchoMode\n /** Character limit for input. */\n charLimit?: number\n /** Prompt string shown before the input. */\n prompt?: string\n /** Style for the prompt. */\n promptStyle?: Style\n /** Style for the input text. */\n textStyle?: Style\n /** Style for the placeholder text. */\n placeholderStyle?: Style\n /** Validation function. */\n validate?: ValidateFunc\n}\n\n/**\n * Create a textInput component builder.\n *\n * @example\n * ```typescript\n * const app = createApp()\n * .component('nameInput', textInput({\n * placeholder: 'Enter your name...',\n * width: 40,\n * validate: (value) => value.length < 3 ? new Error('Too short') : null\n * }))\n * .view(({ components }) => components.nameInput)\n * .build()\n * ```\n *\n * @public\n */\nexport function textInput(\n options: TextInputBuilderOptions = {},\n): ComponentBuilder<TextInputModel> {\n const inputOpts: TextInputOptions = {\n placeholder: options.placeholder ?? '',\n width: options.width,\n echoMode: options.echoMode ?? EchoMode.Normal,\n charLimit: options.charLimit ?? 0,\n prompt: options.prompt ?? '',\n promptStyle: options.promptStyle ?? new Style(),\n textStyle: options.textStyle ?? new Style(),\n placeholderStyle: options.placeholderStyle ?? new Style(),\n validate: options.validate,\n }\n\n return {\n init(): [TextInputModel, Cmd<Msg>] {\n const model = TextInputModel.new(inputOpts)\n const [focused, cmd] = model.focus()\n return [focused, cmd]\n },\n\n update(model: TextInputModel, msg: Msg): [TextInputModel, Cmd<Msg>] {\n return model.update(msg)\n },\n\n view(model: TextInputModel): string {\n return model.view()\n },\n }\n}\n"]}