@zakmandhro/bunti 0.1.4 → 0.1.5

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.
Files changed (92) hide show
  1. package/README.md +66 -25
  2. package/dist/colors.d.ts +74 -0
  3. package/dist/colors.d.ts.map +1 -0
  4. package/dist/colors.js +227 -0
  5. package/dist/colors.js.map +1 -0
  6. package/dist/components/Box.d.ts +5 -0
  7. package/dist/components/Box.d.ts.map +1 -0
  8. package/dist/components/Box.js +4 -0
  9. package/dist/components/Box.js.map +1 -0
  10. package/dist/components/Button.d.ts +14 -0
  11. package/dist/components/Button.d.ts.map +1 -0
  12. package/dist/components/Button.js +91 -0
  13. package/dist/components/Button.js.map +1 -0
  14. package/dist/components/Card.d.ts +11 -0
  15. package/dist/components/Card.d.ts.map +1 -0
  16. package/dist/components/Card.js +37 -0
  17. package/dist/components/Card.js.map +1 -0
  18. package/dist/components/Header.d.ts +13 -0
  19. package/dist/components/Header.d.ts.map +1 -0
  20. package/dist/components/Header.js +54 -0
  21. package/dist/components/Header.js.map +1 -0
  22. package/dist/components/Input.d.ts +16 -0
  23. package/dist/components/Input.d.ts.map +1 -0
  24. package/dist/components/Input.js +101 -0
  25. package/dist/components/Input.js.map +1 -0
  26. package/dist/components/Modal.d.ts +13 -0
  27. package/dist/components/Modal.d.ts.map +1 -0
  28. package/dist/components/Modal.js +28 -0
  29. package/dist/components/Modal.js.map +1 -0
  30. package/dist/components/index.d.ts +7 -0
  31. package/dist/components/index.d.ts.map +1 -0
  32. package/{src/components/index.ts → dist/components/index.js} +3 -0
  33. package/dist/components/index.js.map +1 -0
  34. package/dist/data/glyphs.d.ts +5 -0
  35. package/dist/data/glyphs.d.ts.map +1 -0
  36. package/dist/data/glyphs.js +29 -0
  37. package/dist/data/glyphs.js.map +1 -0
  38. package/dist/detect.d.ts +15 -0
  39. package/dist/detect.d.ts.map +1 -0
  40. package/dist/detect.js +49 -0
  41. package/dist/detect.js.map +1 -0
  42. package/dist/dsl.d.ts +159 -0
  43. package/dist/dsl.d.ts.map +1 -0
  44. package/dist/dsl.js +663 -0
  45. package/dist/dsl.js.map +1 -0
  46. package/dist/easing.d.ts +5 -0
  47. package/dist/easing.d.ts.map +1 -0
  48. package/dist/easing.js +18 -0
  49. package/dist/easing.js.map +1 -0
  50. package/dist/geometry.d.ts +38 -0
  51. package/dist/geometry.d.ts.map +1 -0
  52. package/dist/geometry.js +110 -0
  53. package/dist/geometry.js.map +1 -0
  54. package/dist/icons.d.ts +21 -0
  55. package/dist/icons.d.ts.map +1 -0
  56. package/dist/icons.js +185 -0
  57. package/dist/icons.js.map +1 -0
  58. package/dist/index.d.ts +157 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +36 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/layout.d.ts +89 -0
  63. package/dist/layout.d.ts.map +1 -0
  64. package/dist/layout.js +533 -0
  65. package/dist/layout.js.map +1 -0
  66. package/dist/render.d.ts +17 -0
  67. package/dist/render.d.ts.map +1 -0
  68. package/dist/render.js +335 -0
  69. package/dist/render.js.map +1 -0
  70. package/dist/state.d.ts +92 -0
  71. package/dist/state.d.ts.map +1 -0
  72. package/dist/state.js +105 -0
  73. package/dist/state.js.map +1 -0
  74. package/dist/utils.d.ts +27 -0
  75. package/dist/utils.d.ts.map +1 -0
  76. package/dist/utils.js +157 -0
  77. package/dist/utils.js.map +1 -0
  78. package/package.json +33 -8
  79. package/src/colors.ts +0 -255
  80. package/src/components/Button.ts +0 -104
  81. package/src/components/Card.ts +0 -53
  82. package/src/components/Header.ts +0 -65
  83. package/src/components/Input.ts +0 -124
  84. package/src/data/glyphs.ts +0 -30
  85. package/src/detect.ts +0 -60
  86. package/src/dsl.ts +0 -661
  87. package/src/icons.ts +0 -186
  88. package/src/index.ts +0 -72
  89. package/src/layout.ts +0 -640
  90. package/src/render.ts +0 -302
  91. package/src/state.ts +0 -148
  92. package/src/utils.ts +0 -165
package/dist/dsl.js ADDED
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Bunti High-Level Contextual DSL
3
+ * Scoped closure API with contextual capabilities via trait-based composition.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import pc from 'picocolors';
8
+ import { bg, createGradient, darken, fade, fg, lighten, rgb, } from './colors';
9
+ import { resolveRect as resolveGeometryRect, resolvePlacedRect, splitRect as splitGeometryRect, } from './geometry';
10
+ import { icon, init, replaceEmojis } from './icons';
11
+ import { joinHorizontal, joinVertical, blit as layoutBlit, box as layoutBox, gradient as layoutGradient, list as layoutList, table as layoutTable, viewport as layoutViewport, wallpaper as layoutWallpaper, rect, } from './layout';
12
+ import { flush, loop } from './render';
13
+ import { clearBackBuffer, createScreenState, } from './state';
14
+ import { visibleWidth } from './utils';
15
+ // --- Traits (Contextual Capabilities) ---
16
+ export const KEYS = {
17
+ UP: 'up',
18
+ DOWN: 'down',
19
+ RIGHT: 'right',
20
+ LEFT: 'left',
21
+ ENTER: 'enter',
22
+ ESCAPE: 'escape',
23
+ TAB: 'tab',
24
+ BACKSPACE: 'backspace',
25
+ SPACE: ' ',
26
+ };
27
+ function boxBorderInset(options) {
28
+ return options.border === 'none' || !options.border ? 0 : 1;
29
+ }
30
+ function resolveBoxArea(parent, options, contentWidth, contentHeight) {
31
+ return resolvePlacedRect(parent, {
32
+ x: options.x,
33
+ y: options.y,
34
+ width: options.anchor === 'top' || options.anchor === 'bottom'
35
+ ? parent.width
36
+ : options.width,
37
+ height: options.height,
38
+ anchor: options.anchor,
39
+ contentWidth,
40
+ contentHeight,
41
+ });
42
+ }
43
+ function resolveBoxInnerArea(area, options) {
44
+ const borderInset = boxBorderInset(options);
45
+ const px = options.padding?.[1] ?? 0;
46
+ const py = options.padding?.[0] ?? 0;
47
+ return {
48
+ x: area.x + borderInset + px,
49
+ y: area.y + borderInset + py,
50
+ width: Math.max(0, area.width - borderInset * 2 - px * 2),
51
+ height: Math.max(0, area.height - borderInset * 2 - py * 2),
52
+ };
53
+ }
54
+ function createTransparentBuffer(size) {
55
+ return Array.from({ length: size }, () => ({
56
+ char: ' ',
57
+ fg: undefined,
58
+ bg: undefined,
59
+ fgCode: undefined,
60
+ bgCode: undefined,
61
+ bold: false,
62
+ skip: true,
63
+ }));
64
+ }
65
+ function createLayerScreenState(state) {
66
+ const size = state.width * state.height;
67
+ return {
68
+ ...state,
69
+ frontBuffer: createTransparentBuffer(size),
70
+ backBuffer: createTransparentBuffer(size),
71
+ };
72
+ }
73
+ function compositeLayer(state, buffer) {
74
+ for (let i = 0; i < buffer.length; i++) {
75
+ const source = buffer[i];
76
+ if (source.skip)
77
+ continue;
78
+ const target = state.backBuffer[i];
79
+ target.char = source.char;
80
+ target.fg = source.fg;
81
+ target.bg = source.bg;
82
+ target.bold = source.bold;
83
+ target.fgCode = source.fgCode;
84
+ target.bgCode = source.bgCode;
85
+ target.skip = source.skip;
86
+ target.raw = source.raw;
87
+ }
88
+ }
89
+ function graphemes(text) {
90
+ const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
91
+ return Array.from(segmenter.segment(text), ({ segment }) => segment);
92
+ }
93
+ const buntiColor = {
94
+ ...pc,
95
+ darken,
96
+ lighten,
97
+ rgb,
98
+ fg,
99
+ bg,
100
+ dim: (text) => fg({ r: 96, g: 102, b: 118 }, text),
101
+ };
102
+ /**
103
+ * Common Context Factory: Provided to every closure.
104
+ */
105
+ function createDSLContext(state, dslState, availableW, availableH, offsetX = 0, offsetY = 0, isRoot = false) {
106
+ const ctx = {
107
+ color: buntiColor,
108
+ state,
109
+ width: availableW,
110
+ height: availableH,
111
+ area: { x: offsetX, y: offsetY, width: availableW, height: availableH },
112
+ isRoot,
113
+ offsetX,
114
+ offsetY,
115
+ get cursorX() {
116
+ const currentFlow = dslState.activeContents.join('');
117
+ const lines = currentFlow.split('\n');
118
+ return visibleWidth(lines[lines.length - 1]);
119
+ },
120
+ get cursorY() {
121
+ const currentFlow = dslState.activeContents.join('');
122
+ return Math.max(0, currentFlow.split('\n').length - 1);
123
+ },
124
+ mouseX: state.mouseX,
125
+ mouseY: state.mouseY,
126
+ mouseButton: state.mouseButton,
127
+ isMouseDown: state.isMouseDown,
128
+ lastKey: state.lastKey,
129
+ focusedId: state.focusedId,
130
+ elapsedTime: Date.now() - state.startTime,
131
+ text(str) {
132
+ dslState.activeContents.push(replaceEmojis(String(str)));
133
+ return ctx;
134
+ },
135
+ animate(duration, options = {}) {
136
+ const now = Date.now();
137
+ const start = options.id
138
+ ? ctx.useState(`${options.id}_start`, now)[0]
139
+ : state.startTime;
140
+ const elapsed = now - start - (options.delay || 0);
141
+ if (elapsed < 0)
142
+ return 0;
143
+ if (options.loop)
144
+ return (elapsed % duration) / duration;
145
+ return Math.min(1, elapsed / duration);
146
+ },
147
+ fade,
148
+ typewriter(text, options = {}) {
149
+ const now = Date.now();
150
+ const start = options.id
151
+ ? ctx.useState(`${options.id}_start`, now)[0]
152
+ : state.startTime;
153
+ const delay = options.delay ?? 0;
154
+ const elapsed = Math.max(0, now - start - delay);
155
+ const cps = options.cps ?? 24;
156
+ const chars = graphemes(text);
157
+ const rawIndex = Math.floor((elapsed / 1000) * cps);
158
+ const index = options.loop && chars.length > 0
159
+ ? rawIndex % (chars.length + 1)
160
+ : Math.min(chars.length, rawIndex);
161
+ const blinkRate = options.blinkRate ?? 450;
162
+ const showCursor = options.blink === false ||
163
+ Math.floor(elapsed / blinkRate) % 2 === 0 ||
164
+ index < chars.length;
165
+ return {
166
+ text: chars.slice(0, index).join(''),
167
+ cursor: showCursor ? (options.cursor ?? '█') : ' ',
168
+ done: index >= chars.length,
169
+ index,
170
+ progress: chars.length === 0 ? 1 : index / chars.length,
171
+ };
172
+ },
173
+ flicker(intensity = 0.5) {
174
+ return Math.random() > 1 - intensity;
175
+ },
176
+ useAsync(keyOrFetcher, fetcherOrOptions, maybeOptions) {
177
+ let key;
178
+ let fetcher;
179
+ let options;
180
+ if (typeof keyOrFetcher === 'string') {
181
+ key = keyOrFetcher;
182
+ fetcher = fetcherOrOptions;
183
+ options = maybeOptions;
184
+ }
185
+ else {
186
+ if (state.hookCounter === undefined) {
187
+ state.hookCounter = 0;
188
+ }
189
+ const index = state.hookCounter++;
190
+ key = `_async_hook_${index}`;
191
+ fetcher = keyOrFetcher;
192
+ options = fetcherOrOptions;
193
+ }
194
+ const interval = options?.interval ?? 0;
195
+ const dataKey = `${key}_data`;
196
+ const loadingKey = `${key}_loading`;
197
+ const errorKey = `${key}_error`;
198
+ const lastFetchKey = `${key}_lastFetch`;
199
+ const fetchingKey = `${key}_fetching`;
200
+ if (!state.componentState.has(loadingKey)) {
201
+ state.componentState.set(loadingKey, true);
202
+ }
203
+ const lastFetch = state.componentState.get(lastFetchKey);
204
+ const isFetching = state.componentState.get(fetchingKey);
205
+ const now = Date.now();
206
+ const shouldFetch = !isFetching &&
207
+ (lastFetch === undefined ||
208
+ (interval > 0 && now - lastFetch >= interval));
209
+ if (shouldFetch) {
210
+ state.componentState.set(fetchingKey, true);
211
+ state.componentState.set(lastFetchKey, now);
212
+ fetcher()
213
+ .then((result) => {
214
+ state.componentState.set(dataKey, result);
215
+ state.componentState.set(loadingKey, false);
216
+ state.componentState.set(errorKey, undefined);
217
+ state.requestTick?.();
218
+ })
219
+ .catch((err) => {
220
+ state.componentState.set(errorKey, err);
221
+ state.componentState.set(loadingKey, false);
222
+ state.requestTick?.();
223
+ })
224
+ .finally(() => {
225
+ state.componentState.set(fetchingKey, false);
226
+ });
227
+ }
228
+ return {
229
+ data: state.componentState.get(dataKey),
230
+ loading: state.componentState.get(loadingKey),
231
+ error: state.componentState.get(errorKey),
232
+ };
233
+ },
234
+ useState(keyOrInitial, maybeInitial) {
235
+ let key;
236
+ let initial;
237
+ if (maybeInitial === undefined) {
238
+ if (state.hookCounter === undefined) {
239
+ state.hookCounter = 0;
240
+ }
241
+ const index = state.hookCounter++;
242
+ key = `_state_hook_${index}`;
243
+ initial = keyOrInitial;
244
+ }
245
+ else {
246
+ key = keyOrInitial;
247
+ initial = maybeInitial;
248
+ }
249
+ if (!state.componentState.has(key)) {
250
+ state.componentState.set(key, initial);
251
+ }
252
+ return [
253
+ state.componentState.get(key),
254
+ (val) => {
255
+ state.componentState.set(key, val);
256
+ state.requestTick?.();
257
+ },
258
+ ];
259
+ },
260
+ usePersistentState(key, initial) {
261
+ if (!state.componentState.has(key)) {
262
+ const storeDir = join(process.cwd(), '.tmp');
263
+ const storeFile = join(storeDir, 'bunti_store.json');
264
+ let fileVal;
265
+ try {
266
+ if (existsSync(storeFile)) {
267
+ const data = readFileSync(storeFile, 'utf-8');
268
+ const parsed = JSON.parse(data);
269
+ fileVal = parsed[key];
270
+ }
271
+ }
272
+ catch {
273
+ // Ignore
274
+ }
275
+ const finalVal = fileVal !== undefined ? fileVal : initial;
276
+ state.componentState.set(key, finalVal);
277
+ }
278
+ return [
279
+ state.componentState.get(key),
280
+ (val) => {
281
+ state.componentState.set(key, val);
282
+ const storeDir = join(process.cwd(), '.tmp');
283
+ const storeFile = join(storeDir, 'bunti_store.json');
284
+ try {
285
+ if (!existsSync(storeDir)) {
286
+ mkdirSync(storeDir, { recursive: true });
287
+ }
288
+ let currentStore = {};
289
+ if (existsSync(storeFile)) {
290
+ try {
291
+ currentStore = JSON.parse(readFileSync(storeFile, 'utf-8'));
292
+ }
293
+ catch {
294
+ currentStore = {};
295
+ }
296
+ }
297
+ currentStore[key] = val;
298
+ writeFileSync(storeFile, JSON.stringify(currentStore, null, 2), 'utf-8');
299
+ }
300
+ catch {
301
+ // Ignore
302
+ }
303
+ state.requestTick?.();
304
+ },
305
+ ];
306
+ },
307
+ focusable(id) {
308
+ if (!state.focusableIds.includes(id)) {
309
+ state.focusableIds.push(id);
310
+ }
311
+ if (!state.focusedId)
312
+ state.focusedId = id;
313
+ return state.focusedId === id;
314
+ },
315
+ isFocused(id) {
316
+ return state.focusedId === id;
317
+ },
318
+ focus(id) {
319
+ state.focusedId = id;
320
+ },
321
+ focusNext() {
322
+ if (state.focusableIds.length === 0)
323
+ return;
324
+ const idx = state.focusableIds.indexOf(state.focusedId || '');
325
+ const nextIdx = (idx + 1) % state.focusableIds.length;
326
+ state.focusedId = state.focusableIds[nextIdx];
327
+ },
328
+ hitbox(id, bounds) {
329
+ const rect = ctx.resolveRect(bounds);
330
+ const box = { id, ...rect };
331
+ state.hitboxes.set(id, box);
332
+ const hovered = state.mouseX >= box.x &&
333
+ state.mouseX < box.x + box.width &&
334
+ state.mouseY >= box.y &&
335
+ state.mouseY < box.y + box.height;
336
+ const pressed = hovered && state.isMouseDown && state.mouseButton === 0;
337
+ const clicked = hovered && state.lastKey === 'click';
338
+ return { box, hovered, pressed, clicked };
339
+ },
340
+ resolveRect(bounds) {
341
+ return resolveGeometryRect({ x: offsetX, y: offsetY, width: availableW, height: availableH }, {
342
+ ...bounds,
343
+ y: bounds.y ?? ctx.cursorY,
344
+ });
345
+ },
346
+ resolveLocalRect(bounds, options) {
347
+ return resolvePlacedRect({ x: 0, y: 0, width: availableW, height: availableH }, bounds, options);
348
+ },
349
+ split(options) {
350
+ return splitGeometryRect({ x: 0, y: 0, width: availableW, height: availableH }, options);
351
+ },
352
+ isHovered(id) {
353
+ const box = state.hitboxes.get(id);
354
+ if (!box)
355
+ return false;
356
+ return (state.mouseX >= box.x &&
357
+ state.mouseX < box.x + box.width &&
358
+ state.mouseY >= box.y &&
359
+ state.mouseY < box.y + box.height);
360
+ },
361
+ isPressed(id) {
362
+ return ctx.isHovered(id) && state.isMouseDown && state.mouseButton === 0;
363
+ },
364
+ isClicked(id) {
365
+ return ctx.isHovered(id) && state.lastKey === 'click';
366
+ },
367
+ list(id, items, options = {}) {
368
+ const [selectedIndex, setSelectedIndex] = ctx.useState(`${id}_index`, 0);
369
+ const isFocused = ctx.focusable(id);
370
+ if (isFocused && options.interactive !== false) {
371
+ if (state.lastKey === KEYS.UP)
372
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
373
+ if (state.lastKey === KEYS.DOWN)
374
+ setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1));
375
+ }
376
+ const content = layoutList(items, {
377
+ ...options,
378
+ focusedIndex: selectedIndex,
379
+ focusStyle: isFocused ? options.focusStyle : (s) => pc.dim(s),
380
+ selectedBg: isFocused ? options.selectedBg : undefined,
381
+ }, availableW);
382
+ dslState.activeContents.push(content);
383
+ return ctx;
384
+ },
385
+ table(rows, options = {}) {
386
+ const content = layoutTable(rows, options, availableW);
387
+ dslState.activeContents.push(content);
388
+ return ctx;
389
+ },
390
+ icon(name) {
391
+ return icon(name);
392
+ },
393
+ blit(x, y, content, style = {}) {
394
+ layoutBlit(state, x, y, content, style);
395
+ return ctx;
396
+ },
397
+ rect(x, y, w, h, style) {
398
+ rect(state, x, y, w, h, style);
399
+ return ctx;
400
+ },
401
+ viewport(content, width, height, scrollY = 0) {
402
+ return layoutViewport(content, width, height, scrollY);
403
+ },
404
+ span(options, callback) {
405
+ const subContents = [];
406
+ dslState.stack.push(dslState.activeContents);
407
+ dslState.activeContents = subContents;
408
+ callback(ctx);
409
+ dslState.activeContents = dslState.stack.pop();
410
+ const combined = subContents.join('');
411
+ let styled = combined;
412
+ if (typeof options.color === 'function') {
413
+ styled = options.color(combined);
414
+ }
415
+ else if (options.color !== undefined) {
416
+ styled = fg(options.color, combined);
417
+ }
418
+ dslState.activeContents.push(styled);
419
+ return styled;
420
+ },
421
+ box(options, callback) {
422
+ if (options.zIndex !== undefined) {
423
+ const { zIndex, ...layeredOptions } = options;
424
+ ctx.layer(zIndex, (layerCtx) => {
425
+ layerCtx.box(layeredOptions, callback);
426
+ });
427
+ return '';
428
+ }
429
+ const preContentW = options.width === undefined ? availableW : 0;
430
+ const preContentH = options.height === undefined ? availableH : 0;
431
+ const preArea = resolveBoxArea(ctx.area, options, preContentW, preContentH);
432
+ const innerArea = resolveBoxInnerArea(preArea, options);
433
+ const subContents = [];
434
+ dslState.stack.push(dslState.activeContents);
435
+ dslState.activeContents = subContents;
436
+ const subCtx = createDSLContext(state, dslState, innerArea.width, innerArea.height, innerArea.x, innerArea.y);
437
+ callback(subCtx);
438
+ dslState.activeContents = dslState.stack.pop();
439
+ const innerContent = subContents.join('');
440
+ const styledBox = layoutBox(innerContent, options, availableW, availableH);
441
+ if (!options.detach) {
442
+ dslState.activeContents.push(styledBox);
443
+ }
444
+ return styledBox;
445
+ },
446
+ layer(zIndexOrOptions, maybeCallback) {
447
+ const callback = typeof zIndexOrOptions === 'function' ? zIndexOrOptions : maybeCallback;
448
+ if (!callback)
449
+ return ctx;
450
+ const zIndex = typeof zIndexOrOptions === 'number'
451
+ ? zIndexOrOptions
452
+ : typeof zIndexOrOptions === 'object'
453
+ ? (zIndexOrOptions.zIndex ?? 0)
454
+ : 0;
455
+ const layerState = createLayerScreenState(state);
456
+ const layerDslState = {
457
+ activeContents: [],
458
+ stack: [],
459
+ layers: dslState.layers,
460
+ layerOrder: dslState.layerOrder,
461
+ };
462
+ const layerCtx = createDSLContext(layerState, layerDslState, availableW, availableH, offsetX, offsetY, isRoot);
463
+ layerCtx.box = createDirectBoxRenderer(layerState, layerDslState, layerCtx);
464
+ callback(layerCtx);
465
+ const flow = layerDslState.activeContents.join('');
466
+ if (flow)
467
+ layoutBlit(layerState, offsetX, offsetY, flow);
468
+ dslState.layerOrder = layerDslState.layerOrder + 1;
469
+ dslState.layers.push({
470
+ zIndex,
471
+ order: layerDslState.layerOrder,
472
+ buffer: layerState.backBuffer,
473
+ });
474
+ return ctx;
475
+ },
476
+ joinHorizontal(...blocks) {
477
+ return joinHorizontal(...blocks);
478
+ },
479
+ joinVertical(...blocks) {
480
+ return joinVertical(...blocks);
481
+ },
482
+ wallpaper(input) {
483
+ if (typeof input === 'object' && 'colors' in input) {
484
+ layoutGradient(state, input.colors, { direction: input.direction });
485
+ }
486
+ else if (Array.isArray(input)) {
487
+ layoutGradient(state, input);
488
+ }
489
+ else if (typeof input === 'object' && 'color' in input) {
490
+ ctx.wallpaper(input.color);
491
+ }
492
+ else {
493
+ layoutWallpaper(state, { bg: input });
494
+ }
495
+ },
496
+ gradient: (opts) => ({
497
+ colors: createGradient(opts.colors, opts.steps || 10),
498
+ direction: opts.direction || 'vertical',
499
+ steps: opts.steps || 10,
500
+ }),
501
+ rgb,
502
+ requestStop() {
503
+ state.requestStop?.();
504
+ },
505
+ flushFlow() { },
506
+ };
507
+ return ctx;
508
+ }
509
+ function createDirectBoxRenderer(state, dslState, base) {
510
+ return (options, callback) => {
511
+ if (options.zIndex !== undefined) {
512
+ const { zIndex, ...layeredOptions } = options;
513
+ base.layer(zIndex, (layerCtx) => {
514
+ layerCtx.box(layeredOptions, callback);
515
+ });
516
+ return '';
517
+ }
518
+ const boxOptions = options.anchor === 'top' || options.anchor === 'bottom'
519
+ ? { ...options, width: base.width }
520
+ : options;
521
+ const hasFixedSize = boxOptions.width !== undefined &&
522
+ boxOptions.width !== 'auto' &&
523
+ boxOptions.height !== undefined &&
524
+ boxOptions.height !== 'auto';
525
+ if (hasFixedSize) {
526
+ const preArea = resolveBoxArea(base.area, boxOptions, 0, 0);
527
+ const innerArea = resolveBoxInnerArea(preArea, boxOptions);
528
+ if (boxOptions.bgColor || boxOptions.color) {
529
+ rect(state, preArea.x, preArea.y, preArea.width, preArea.height, {
530
+ char: ' ',
531
+ bg: boxOptions.bgColor,
532
+ fg: boxOptions.color === 'blank' ? undefined : boxOptions.color,
533
+ });
534
+ }
535
+ const styledBox = layoutBox('', boxOptions, base.width, base.height);
536
+ layoutBlit(state, preArea.x, preArea.y, styledBox);
537
+ const subContents = [];
538
+ dslState.stack.push(dslState.activeContents);
539
+ dslState.activeContents = subContents;
540
+ const subCtx = createDSLContext(state, dslState, innerArea.width, innerArea.height, innerArea.x, innerArea.y);
541
+ callback(subCtx);
542
+ dslState.activeContents = dslState.stack.pop();
543
+ const contentStr = subContents.join('');
544
+ if (contentStr) {
545
+ const styledContent = layoutBox(contentStr, {
546
+ width: innerArea.width,
547
+ height: innerArea.height,
548
+ align: boxOptions.align,
549
+ valign: boxOptions.valign,
550
+ wrap: boxOptions.wrap,
551
+ border: 'none',
552
+ padding: [0, 0],
553
+ }, innerArea.width, innerArea.height);
554
+ layoutBlit(state, innerArea.x, innerArea.y, styledContent);
555
+ }
556
+ return styledBox;
557
+ }
558
+ const subContents = [];
559
+ dslState.stack.push(dslState.activeContents);
560
+ dslState.activeContents = subContents;
561
+ const preAreaTemp = resolveBoxArea(base.area, boxOptions, base.width, base.height);
562
+ const innerAreaTemp = resolveBoxInnerArea(preAreaTemp, boxOptions);
563
+ const subCtx = createDSLContext(state, dslState, innerAreaTemp.width, innerAreaTemp.height, innerAreaTemp.x, innerAreaTemp.y);
564
+ callback(subCtx);
565
+ dslState.activeContents = dslState.stack.pop();
566
+ const contentStr = subContents.join('');
567
+ const styledBox = layoutBox(contentStr, boxOptions, base.width, base.height);
568
+ const lines = styledBox.split('\n');
569
+ const lineWidths = lines.map(visibleWidth);
570
+ const boxArea = resolveBoxArea(base.area, boxOptions, lineWidths.length > 0 ? Math.max(...lineWidths) : 0, lines.length);
571
+ if (boxOptions.bgColor || boxOptions.color) {
572
+ rect(state, boxArea.x, boxArea.y, boxArea.width, boxArea.height, {
573
+ char: ' ',
574
+ bg: boxOptions.bgColor,
575
+ fg: boxOptions.color === 'blank' ? undefined : boxOptions.color,
576
+ });
577
+ }
578
+ layoutBlit(state, boxArea.x, boxArea.y, styledBox);
579
+ return styledBox;
580
+ };
581
+ }
582
+ /**
583
+ * Top-level Screen Context
584
+ */
585
+ export function createScreenContext(state) {
586
+ state.hookCounter = 0;
587
+ const previousFocusableIds = state.focusableIds;
588
+ if (state.lastKey === KEYS.TAB && previousFocusableIds.length > 0) {
589
+ const idx = previousFocusableIds.indexOf(state.focusedId || '');
590
+ const nextIdx = (idx + 1) % previousFocusableIds.length;
591
+ state.focusedId = previousFocusableIds[nextIdx];
592
+ }
593
+ state.focusableIds = []; // Rebuild from this frame's rendered focusables.
594
+ state.hitboxes = new Map(); // Rebuild from this frame's interactive geometry.
595
+ const dslState = {
596
+ activeContents: [],
597
+ stack: [],
598
+ layers: [],
599
+ layerOrder: 0,
600
+ };
601
+ const base = createDSLContext(state, dslState, state.width, state.height, 0, 0, true);
602
+ const flushFlow = () => {
603
+ const flow = dslState.activeContents.join('');
604
+ if (flow)
605
+ layoutBlit(state, 0, 0, flow);
606
+ dslState.layers
607
+ .sort((a, b) => a.zIndex - b.zIndex || a.order - b.order)
608
+ .forEach((layer) => {
609
+ compositeLayer(state, layer.buffer);
610
+ });
611
+ };
612
+ const boxOverride = createDirectBoxRenderer(state, dslState, base);
613
+ return {
614
+ ...base,
615
+ box: boxOverride,
616
+ flushFlow,
617
+ requestStop: () => {
618
+ state.requestStop?.();
619
+ },
620
+ };
621
+ }
622
+ /**
623
+ * Primary Entry Point
624
+ */
625
+ export async function render(callback, options = {}) {
626
+ // Sync apply forced options first
627
+ if (options.nerdFont !== undefined) {
628
+ await init({ nerdFont: options.nerdFont });
629
+ }
630
+ else {
631
+ // Start detection in background, don't await!
632
+ init();
633
+ }
634
+ const state = createScreenState(options);
635
+ const tick = () => {
636
+ clearBackBuffer(state);
637
+ const b = createScreenContext(state);
638
+ if (typeof callback === 'string') {
639
+ b.blit(0, 0, callback);
640
+ }
641
+ else {
642
+ callback(b);
643
+ }
644
+ if (state.isStopped)
645
+ return;
646
+ b.flushFlow();
647
+ if (state.isStopped)
648
+ return;
649
+ flush(state);
650
+ };
651
+ if (options.once) {
652
+ tick();
653
+ await new Promise((resolve) => {
654
+ setTimeout(() => {
655
+ resolve();
656
+ process.exit(0);
657
+ }, 50);
658
+ });
659
+ return;
660
+ }
661
+ await loop(state, (_s) => tick());
662
+ }
663
+ //# sourceMappingURL=dsl.js.map