@termuijs/widgets 0.1.0

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.js ADDED
@@ -0,0 +1,968 @@
1
+ // src/base/Widget.ts
2
+ import {
3
+ defaultStyle,
4
+ mergeStyles,
5
+ createLayoutNode,
6
+ EventEmitter,
7
+ normalizeEdges,
8
+ getBorderChars,
9
+ styleToCellAttrs,
10
+ containsPoint
11
+ } from "@termuijs/core";
12
+ var _widgetIdCounter = 0;
13
+ var Widget = class {
14
+ /** Unique widget identifier */
15
+ id;
16
+ /** Widget's style */
17
+ _style;
18
+ /** Child widgets */
19
+ _children = [];
20
+ /** Parent widget (null for root) */
21
+ parent = null;
22
+ /** Computed layout rectangle */
23
+ _rect = { x: 0, y: 0, width: 0, height: 0 };
24
+ /** Reference to the layout node (set during getLayoutNode) */
25
+ _layoutNode = null;
26
+ /** Whether this widget can receive focus */
27
+ focusable = false;
28
+ /** Tab index for focus ordering */
29
+ tabIndex = 0;
30
+ /** Event emitter for this widget */
31
+ events = new EventEmitter();
32
+ /** Whether the widget is currently focused */
33
+ isFocused = false;
34
+ /**
35
+ * Dirty flag — true when this widget needs re-rendering.
36
+ * Newly created widgets start dirty.
37
+ */
38
+ _dirty = true;
39
+ constructor(style = {}) {
40
+ this.id = `widget_${++_widgetIdCounter}`;
41
+ this._style = mergeStyles(defaultStyle(), style);
42
+ }
43
+ /** Get the current style */
44
+ get style() {
45
+ return this._style;
46
+ }
47
+ /** Update the style (merge with existing) */
48
+ setStyle(style) {
49
+ this._style = mergeStyles(this._style, style);
50
+ this.markDirty();
51
+ }
52
+ /** Get the computed rect after layout */
53
+ get rect() {
54
+ return this._rect;
55
+ }
56
+ /** Add a child widget */
57
+ addChild(child) {
58
+ child.parent = this;
59
+ this._children.push(child);
60
+ }
61
+ /** Remove a child widget */
62
+ removeChild(child) {
63
+ const idx = this._children.indexOf(child);
64
+ if (idx >= 0) {
65
+ this._children.splice(idx, 1);
66
+ child.parent = null;
67
+ }
68
+ }
69
+ /** Remove all children */
70
+ clearChildren() {
71
+ for (const child of this._children) {
72
+ child.parent = null;
73
+ }
74
+ this._children = [];
75
+ }
76
+ /** Get all children */
77
+ get children() {
78
+ return this._children;
79
+ }
80
+ /**
81
+ * Build the LayoutNode tree for this widget.
82
+ * Stores a reference so we can sync computed rects back via syncLayout().
83
+ */
84
+ getLayoutNode() {
85
+ const childNodes = this._children.filter((c) => c.style.visible !== false).map((c) => c.getLayoutNode());
86
+ this._layoutNode = createLayoutNode(this.id, this._style, childNodes);
87
+ return this._layoutNode;
88
+ }
89
+ /**
90
+ * After computeLayout() has been called, sync the computed rects
91
+ * from the layout tree back into widget `_rect` fields.
92
+ * This MUST be called after computeLayout() and before render().
93
+ */
94
+ syncLayout() {
95
+ if (this._layoutNode) {
96
+ this._rect = { ...this._layoutNode.computed };
97
+ }
98
+ const visibleChildren = this._children.filter((c) => c.style.visible !== false);
99
+ for (let i = 0; i < visibleChildren.length; i++) {
100
+ visibleChildren[i].syncLayout();
101
+ }
102
+ }
103
+ /**
104
+ * Render this widget (and children) into the screen buffer.
105
+ * Automatically pushes a clip region if overflow is hidden (default).
106
+ */
107
+ render(screen) {
108
+ if (this._style.visible === false) return;
109
+ const shouldClip = this._style.overflow !== "visible";
110
+ if (shouldClip) {
111
+ screen.pushClip(this._rect);
112
+ }
113
+ this._renderSelf(screen);
114
+ this._renderBorder(screen);
115
+ for (const child of this._children) {
116
+ child.render(screen);
117
+ }
118
+ if (shouldClip) {
119
+ screen.popClip();
120
+ }
121
+ }
122
+ /**
123
+ * Update the computed rect from layout results.
124
+ */
125
+ updateRect(rect) {
126
+ this._rect = rect;
127
+ }
128
+ /**
129
+ * Mark this widget as needing re-render.
130
+ * Propagates up to parent so the render loop can detect changes.
131
+ */
132
+ markDirty() {
133
+ if (this._dirty) return;
134
+ this._dirty = true;
135
+ this.parent?.markDirty();
136
+ }
137
+ /**
138
+ * Clear the dirty flag after rendering.
139
+ */
140
+ clearDirty() {
141
+ this._dirty = false;
142
+ for (const child of this._children) {
143
+ child.clearDirty();
144
+ }
145
+ }
146
+ /** Check if this widget (or any child) needs re-rendering */
147
+ get isDirty() {
148
+ return this._dirty;
149
+ }
150
+ /**
151
+ * Render the border around this widget, including focus ring if focused.
152
+ */
153
+ _renderBorder(screen) {
154
+ const border = this._style.border;
155
+ const hasBorder = border && border !== "none";
156
+ const showFocusRing = this.isFocused && this.focusable && this._style.focusRingStyle !== "none";
157
+ if (!hasBorder && !showFocusRing) return;
158
+ const { x, y, width, height } = this._rect;
159
+ if (width < 2 || height < 2) return;
160
+ if (hasBorder) {
161
+ const chars = getBorderChars(border);
162
+ if (!chars) return;
163
+ const attrs = styleToCellAttrs(this._style);
164
+ const borderFg = this._style.borderColor ?? attrs.fg;
165
+ const fg = showFocusRing ? this._style.focusRingColor ?? { type: "named", name: "cyan" } : borderFg;
166
+ const cellStyle = { fg };
167
+ screen.setCell(x, y, { char: chars.topLeft, ...cellStyle });
168
+ for (let c = 1; c < width - 1; c++) {
169
+ screen.setCell(x + c, y, { char: chars.top, ...cellStyle });
170
+ }
171
+ screen.setCell(x + width - 1, y, { char: chars.topRight, ...cellStyle });
172
+ screen.setCell(x, y + height - 1, { char: chars.bottomLeft, ...cellStyle });
173
+ for (let c = 1; c < width - 1; c++) {
174
+ screen.setCell(x + c, y + height - 1, { char: chars.bottom, ...cellStyle });
175
+ }
176
+ screen.setCell(x + width - 1, y + height - 1, { char: chars.bottomRight, ...cellStyle });
177
+ for (let r = 1; r < height - 1; r++) {
178
+ screen.setCell(x, y + r, { char: chars.left, ...cellStyle });
179
+ screen.setCell(x + width - 1, y + r, { char: chars.right, ...cellStyle });
180
+ }
181
+ } else if (showFocusRing) {
182
+ const fg = this._style.focusRingColor ?? { type: "named", name: "cyan" };
183
+ const cellStyle = { fg, bold: true };
184
+ screen.setCell(x, y, { char: "\u250C", ...cellStyle });
185
+ if (width > 2) screen.setCell(x + 1, y, { char: "\u2500", ...cellStyle });
186
+ screen.setCell(x + width - 1, y, { char: "\u2510", ...cellStyle });
187
+ if (width > 2) screen.setCell(x + width - 2, y, { char: "\u2500", ...cellStyle });
188
+ screen.setCell(x, y + height - 1, { char: "\u2514", ...cellStyle });
189
+ if (width > 2) screen.setCell(x + 1, y + height - 1, { char: "\u2500", ...cellStyle });
190
+ screen.setCell(x + width - 1, y + height - 1, { char: "\u2518", ...cellStyle });
191
+ if (width > 2) screen.setCell(x + width - 2, y + height - 1, { char: "\u2500", ...cellStyle });
192
+ if (height > 2) {
193
+ screen.setCell(x, y + 1, { char: "\u2502", ...cellStyle });
194
+ screen.setCell(x + width - 1, y + 1, { char: "\u2502", ...cellStyle });
195
+ screen.setCell(x, y + height - 2, { char: "\u2502", ...cellStyle });
196
+ screen.setCell(x + width - 1, y + height - 2, { char: "\u2502", ...cellStyle });
197
+ }
198
+ }
199
+ }
200
+ /**
201
+ * Get the inner content area (after border + padding).
202
+ */
203
+ _getContentRect() {
204
+ const padding = normalizeEdges(this._style.padding);
205
+ const border = this._style.border && this._style.border !== "none" ? 1 : 0;
206
+ return {
207
+ x: this._rect.x + padding.left + border,
208
+ y: this._rect.y + padding.top + border,
209
+ width: Math.max(0, this._rect.width - padding.left - padding.right - border * 2),
210
+ height: Math.max(0, this._rect.height - padding.top - padding.bottom - border * 2)
211
+ };
212
+ }
213
+ /**
214
+ * Check if a point hits this widget.
215
+ */
216
+ hitTest(x, y) {
217
+ return containsPoint(this._rect, x, y);
218
+ }
219
+ /** Lifecycle: called when the widget is mounted */
220
+ mount() {
221
+ this.events.emit("mount", void 0);
222
+ for (const child of this._children) {
223
+ child.mount();
224
+ }
225
+ }
226
+ /** Lifecycle: called when the widget is unmounted */
227
+ unmount() {
228
+ for (const child of this._children) {
229
+ child.unmount();
230
+ }
231
+ this.events.emit("unmount", void 0);
232
+ }
233
+ };
234
+
235
+ // src/display/Box.ts
236
+ import { styleToCellAttrs as styleToCellAttrs2 } from "@termuijs/core";
237
+ var Box = class extends Widget {
238
+ constructor(style = {}) {
239
+ super(style);
240
+ }
241
+ _renderSelf(screen) {
242
+ const { bg } = styleToCellAttrs2(this._style);
243
+ if (bg.type === "none") return;
244
+ const { x, y, width, height } = this._rect;
245
+ const border = this._style.border && this._style.border !== "none" ? 1 : 0;
246
+ for (let r = border; r < height - border; r++) {
247
+ for (let c = border; c < width - border; c++) {
248
+ screen.setCell(x + c, y + r, { char: " ", bg });
249
+ }
250
+ }
251
+ }
252
+ };
253
+
254
+ // src/display/Text.ts
255
+ import { styleToCellAttrs as styleToCellAttrs3, wordWrap, stringWidth } from "@termuijs/core";
256
+ var Text = class extends Widget {
257
+ _content;
258
+ _wrap;
259
+ _align;
260
+ constructor(content, style = {}, props = {}) {
261
+ super(style);
262
+ this._content = content;
263
+ this._wrap = props.wrap ?? true;
264
+ this._align = props.align ?? "left";
265
+ }
266
+ /** Update the text content */
267
+ setContent(content) {
268
+ this._content = content;
269
+ }
270
+ /** Get current text content */
271
+ getContent() {
272
+ return this._content;
273
+ }
274
+ _renderSelf(screen) {
275
+ const contentRect = this._getContentRect();
276
+ const { x, y, width, height } = contentRect;
277
+ if (width <= 0 || height <= 0) return;
278
+ const attrs = styleToCellAttrs3(this._style);
279
+ let text = this._wrap ? wordWrap(this._content, width) : this._content;
280
+ const lines = text.split("\n");
281
+ for (let i = 0; i < Math.min(lines.length, height); i++) {
282
+ let line = lines[i];
283
+ const lineWidth = stringWidth(line);
284
+ let offsetX = 0;
285
+ if (this._align === "center") {
286
+ offsetX = Math.floor((width - lineWidth) / 2);
287
+ } else if (this._align === "right") {
288
+ offsetX = width - lineWidth;
289
+ }
290
+ screen.writeString(x + Math.max(0, offsetX), y + i, line, attrs);
291
+ }
292
+ }
293
+ };
294
+
295
+ // src/display/LogView.ts
296
+ import { styleToCellAttrs as styleToCellAttrs4, truncate } from "@termuijs/core";
297
+ var LogView = class extends Widget {
298
+ _lines = [];
299
+ _scrollOffset = 0;
300
+ _highlight;
301
+ _autoScroll;
302
+ constructor(style = {}, opts = {}) {
303
+ super(style);
304
+ this._highlight = opts.highlight ?? {
305
+ ERROR: { type: "named", name: "red" },
306
+ WARN: { type: "named", name: "yellow" },
307
+ INFO: { type: "named", name: "green" },
308
+ DEBUG: { type: "named", name: "brightBlack" }
309
+ };
310
+ this._autoScroll = opts.autoScroll ?? true;
311
+ }
312
+ setLines(lines) {
313
+ this._lines = lines;
314
+ if (this._autoScroll) {
315
+ this._scrollToBottom();
316
+ }
317
+ }
318
+ appendLine(line) {
319
+ this._lines.push(line);
320
+ if (this._autoScroll) {
321
+ this._scrollToBottom();
322
+ }
323
+ }
324
+ scrollUp(n = 1) {
325
+ this._scrollOffset = Math.max(0, this._scrollOffset - n);
326
+ }
327
+ scrollDown(n = 1) {
328
+ this._scrollOffset = Math.min(
329
+ Math.max(0, this._lines.length - 1),
330
+ this._scrollOffset + n
331
+ );
332
+ }
333
+ _scrollToBottom() {
334
+ const rect = this._getContentRect();
335
+ const visibleLines = Math.max(1, rect.height);
336
+ this._scrollOffset = Math.max(0, this._lines.length - visibleLines);
337
+ }
338
+ _renderSelf(screen) {
339
+ const rect = this._getContentRect();
340
+ const { x, y, width, height } = rect;
341
+ if (width <= 0 || height <= 0) return;
342
+ const attrs = styleToCellAttrs4(this._style);
343
+ const visibleLines = this._lines.slice(this._scrollOffset, this._scrollOffset + height);
344
+ for (let i = 0; i < Math.min(visibleLines.length, height); i++) {
345
+ const line = truncate(visibleLines[i], width);
346
+ const lineColor = this._getLineColor(line);
347
+ screen.writeString(x, y + i, line, {
348
+ ...attrs,
349
+ ...lineColor ? { fg: lineColor } : {}
350
+ });
351
+ }
352
+ }
353
+ _getLineColor(line) {
354
+ for (const [keyword, color] of Object.entries(this._highlight)) {
355
+ if (line.includes(keyword)) return color;
356
+ }
357
+ return null;
358
+ }
359
+ };
360
+
361
+ // src/input/List.ts
362
+ import { styleToCellAttrs as styleToCellAttrs5, stringWidth as stringWidth2, truncate as truncate2 } from "@termuijs/core";
363
+ var List = class extends Widget {
364
+ _items;
365
+ _selectedIndex = 0;
366
+ _scrollOffset = 0;
367
+ _onSelect;
368
+ constructor(items, style = {}, onSelect) {
369
+ super({ border: "single", ...style });
370
+ this._items = items;
371
+ this._onSelect = onSelect;
372
+ this.focusable = true;
373
+ }
374
+ get selectedIndex() {
375
+ return this._selectedIndex;
376
+ }
377
+ get selectedItem() {
378
+ return this._items[this._selectedIndex];
379
+ }
380
+ setItems(items) {
381
+ this._items = items;
382
+ this._selectedIndex = Math.min(this._selectedIndex, items.length - 1);
383
+ this._clampScroll();
384
+ }
385
+ /** Move selection up */
386
+ selectPrev() {
387
+ let next = this._selectedIndex - 1;
388
+ while (next >= 0 && this._items[next].disabled) next--;
389
+ if (next >= 0) {
390
+ this._selectedIndex = next;
391
+ this._clampScroll();
392
+ }
393
+ }
394
+ /** Move selection down */
395
+ selectNext() {
396
+ let next = this._selectedIndex + 1;
397
+ while (next < this._items.length && this._items[next].disabled) next++;
398
+ if (next < this._items.length) {
399
+ this._selectedIndex = next;
400
+ this._clampScroll();
401
+ }
402
+ }
403
+ /** Confirm the current selection */
404
+ confirm() {
405
+ const item = this._items[this._selectedIndex];
406
+ if (item && !item.disabled) {
407
+ this._onSelect?.(item, this._selectedIndex);
408
+ }
409
+ }
410
+ _renderSelf(screen) {
411
+ const rect = this._getContentRect();
412
+ const { x, y, width, height } = rect;
413
+ if (width <= 0 || height <= 0) return;
414
+ const attrs = styleToCellAttrs5(this._style);
415
+ const visibleCount = Math.min(this._items.length - this._scrollOffset, height);
416
+ for (let i = 0; i < visibleCount; i++) {
417
+ const itemIdx = this._scrollOffset + i;
418
+ const item = this._items[itemIdx];
419
+ const isSelected = itemIdx === this._selectedIndex;
420
+ const prefix = isSelected ? "\u25B8 " : " ";
421
+ let line = prefix + item.label;
422
+ line = truncate2(line, width);
423
+ const cellStyle = {
424
+ ...attrs,
425
+ bold: isSelected,
426
+ dim: item.disabled ?? false,
427
+ inverse: isSelected && this.isFocused
428
+ };
429
+ screen.writeString(x, y + i, line, cellStyle);
430
+ if (isSelected && this.isFocused) {
431
+ const remaining = width - stringWidth2(line);
432
+ for (let c = 0; c < remaining; c++) {
433
+ screen.setCell(x + stringWidth2(line) + c, y + i, { char: " ", ...cellStyle });
434
+ }
435
+ }
436
+ }
437
+ if (this._items.length > height) {
438
+ const scrollRatio = this._scrollOffset / (this._items.length - height);
439
+ const scrollPos = Math.floor(scrollRatio * (height - 1));
440
+ for (let r = 0; r < height; r++) {
441
+ const scrollChar = r === scrollPos ? "\u2588" : "\u2591";
442
+ screen.setCell(x + width - 1, y + r, { char: scrollChar, ...attrs, dim: true });
443
+ }
444
+ }
445
+ }
446
+ _clampScroll() {
447
+ const rect = this._getContentRect();
448
+ const visibleHeight = rect.height;
449
+ if (visibleHeight <= 0) return;
450
+ if (this._selectedIndex < this._scrollOffset) {
451
+ this._scrollOffset = this._selectedIndex;
452
+ }
453
+ if (this._selectedIndex >= this._scrollOffset + visibleHeight) {
454
+ this._scrollOffset = this._selectedIndex - visibleHeight + 1;
455
+ }
456
+ }
457
+ };
458
+
459
+ // src/input/TextInput.ts
460
+ import { styleToCellAttrs as styleToCellAttrs6, truncate as truncate3 } from "@termuijs/core";
461
+ var TextInput = class extends Widget {
462
+ _value = "";
463
+ _cursorPos = 0;
464
+ _placeholder;
465
+ _mask;
466
+ _maxLength;
467
+ _onChange;
468
+ _onSubmit;
469
+ constructor(style = {}, options = {}) {
470
+ super({ border: "single", height: 3, ...style });
471
+ this._placeholder = options.placeholder ?? "";
472
+ this._mask = options.mask ?? null;
473
+ this._maxLength = options.maxLength ?? Infinity;
474
+ this._onChange = options.onChange;
475
+ this._onSubmit = options.onSubmit;
476
+ this.focusable = true;
477
+ }
478
+ get value() {
479
+ return this._value;
480
+ }
481
+ set value(v) {
482
+ this._value = v.slice(0, this._maxLength);
483
+ this._cursorPos = Math.min(this._cursorPos, this._value.length);
484
+ }
485
+ /**
486
+ * Handle a typed character.
487
+ */
488
+ insertChar(char) {
489
+ if (this._value.length >= this._maxLength) return;
490
+ this._value = this._value.slice(0, this._cursorPos) + char + this._value.slice(this._cursorPos);
491
+ this._cursorPos++;
492
+ this._onChange?.(this._value);
493
+ }
494
+ /**
495
+ * Delete the character before the cursor.
496
+ */
497
+ deleteBack() {
498
+ if (this._cursorPos > 0) {
499
+ this._value = this._value.slice(0, this._cursorPos - 1) + this._value.slice(this._cursorPos);
500
+ this._cursorPos--;
501
+ this._onChange?.(this._value);
502
+ }
503
+ }
504
+ /**
505
+ * Delete the character after the cursor.
506
+ */
507
+ deleteForward() {
508
+ if (this._cursorPos < this._value.length) {
509
+ this._value = this._value.slice(0, this._cursorPos) + this._value.slice(this._cursorPos + 1);
510
+ this._onChange?.(this._value);
511
+ }
512
+ }
513
+ moveCursorLeft() {
514
+ this._cursorPos = Math.max(0, this._cursorPos - 1);
515
+ }
516
+ moveCursorRight() {
517
+ this._cursorPos = Math.min(this._value.length, this._cursorPos + 1);
518
+ }
519
+ moveCursorHome() {
520
+ this._cursorPos = 0;
521
+ }
522
+ moveCursorEnd() {
523
+ this._cursorPos = this._value.length;
524
+ }
525
+ submit() {
526
+ this._onSubmit?.(this._value);
527
+ }
528
+ clear() {
529
+ this._value = "";
530
+ this._cursorPos = 0;
531
+ this._onChange?.("");
532
+ }
533
+ _renderSelf(screen) {
534
+ const rect = this._getContentRect();
535
+ const { x, y, width, height } = rect;
536
+ if (width <= 0 || height <= 0) return;
537
+ const attrs = styleToCellAttrs6(this._style);
538
+ if (this._value.length === 0 && !this.isFocused) {
539
+ screen.writeString(x, y, truncate3(this._placeholder, width), { ...attrs, dim: true });
540
+ return;
541
+ }
542
+ const displayValue = this._mask ? this._mask.repeat(this._value.length) : this._value;
543
+ const visibleWidth = width - 1;
544
+ let scrollX = 0;
545
+ if (this._cursorPos > visibleWidth) {
546
+ scrollX = this._cursorPos - visibleWidth;
547
+ }
548
+ const visibleText = displayValue.slice(scrollX, scrollX + visibleWidth);
549
+ screen.writeString(x, y, visibleText, attrs);
550
+ if (this.isFocused) {
551
+ const cursorScreenPos = x + this._cursorPos - scrollX;
552
+ if (cursorScreenPos >= x && cursorScreenPos < x + width) {
553
+ const cursorChar = this._cursorPos < displayValue.length ? displayValue[this._cursorPos] : " ";
554
+ screen.setCell(cursorScreenPos, y, {
555
+ char: cursorChar,
556
+ ...attrs,
557
+ inverse: true
558
+ });
559
+ }
560
+ }
561
+ }
562
+ };
563
+
564
+ // src/data/Table.ts
565
+ import { styleToCellAttrs as styleToCellAttrs7, stringWidth as stringWidth4, truncate as truncate4 } from "@termuijs/core";
566
+ var Table = class extends Widget {
567
+ _columns;
568
+ _rows;
569
+ _showHeader;
570
+ _headerColor;
571
+ _stripe;
572
+ _stripeColor;
573
+ _separator;
574
+ constructor(columns, rows, style = {}, options = {}) {
575
+ super(style);
576
+ this._columns = columns;
577
+ this._rows = rows;
578
+ this._showHeader = options.showHeader ?? true;
579
+ this._headerColor = options.headerColor ?? { type: "named", name: "cyan" };
580
+ this._stripe = options.stripe ?? true;
581
+ this._stripeColor = options.stripeColor ?? { type: "named", name: "brightBlack" };
582
+ this._separator = options.separator ?? " \u2502 ";
583
+ }
584
+ setRows(rows) {
585
+ this._rows = rows;
586
+ }
587
+ _renderSelf(screen) {
588
+ const rect = this._getContentRect();
589
+ const { x, y, width, height } = rect;
590
+ if (width <= 0 || height <= 0) return;
591
+ const attrs = styleToCellAttrs7(this._style);
592
+ const sepWidth = stringWidth4(this._separator);
593
+ const colWidths = this._computeColumnWidths(
594
+ width - (this._columns.length - 1) * sepWidth
595
+ );
596
+ let row = 0;
597
+ if (this._showHeader && row < height) {
598
+ let cx = x;
599
+ for (let c = 0; c < this._columns.length; c++) {
600
+ const col = this._columns[c];
601
+ const cellText = this._alignText(col.header, colWidths[c], col.align ?? "left");
602
+ screen.writeString(cx, y + row, cellText, {
603
+ ...attrs,
604
+ fg: this._headerColor,
605
+ bold: true
606
+ });
607
+ cx += colWidths[c];
608
+ if (c < this._columns.length - 1) {
609
+ screen.writeString(cx, y + row, this._separator, { ...attrs, dim: true });
610
+ cx += sepWidth;
611
+ }
612
+ }
613
+ row++;
614
+ if (row < height) {
615
+ const sepLine = "\u2500".repeat(width);
616
+ screen.writeString(x, y + row, sepLine, { ...attrs, dim: true });
617
+ row++;
618
+ }
619
+ }
620
+ for (let r = 0; r < this._rows.length && row < height; r++) {
621
+ const dataRow = this._rows[r];
622
+ const isStripe = this._stripe && r % 2 === 1;
623
+ let cx = x;
624
+ for (let c = 0; c < this._columns.length; c++) {
625
+ const col = this._columns[c];
626
+ const rawValue = String(dataRow[col.key] ?? "");
627
+ const cellText = this._alignText(rawValue, colWidths[c], col.align ?? "left");
628
+ screen.writeString(cx, y + row, cellText, {
629
+ ...attrs,
630
+ bg: isStripe ? this._stripeColor : attrs.bg
631
+ });
632
+ cx += colWidths[c];
633
+ if (c < this._columns.length - 1) {
634
+ screen.writeString(cx, y + row, this._separator, {
635
+ ...attrs,
636
+ dim: true,
637
+ bg: isStripe ? this._stripeColor : attrs.bg
638
+ });
639
+ cx += sepWidth;
640
+ }
641
+ }
642
+ if (isStripe) {
643
+ for (let fx = cx; fx < x + width; fx++) {
644
+ screen.setCell(fx, y + row, { char: " ", bg: this._stripeColor });
645
+ }
646
+ }
647
+ row++;
648
+ }
649
+ }
650
+ _computeColumnWidths(totalWidth) {
651
+ const fixedCols = this._columns.filter((c) => c.width !== void 0);
652
+ const flexCols = this._columns.filter((c) => c.width === void 0);
653
+ let usedWidth = fixedCols.reduce((sum, c) => sum + (c.width ?? 0), 0);
654
+ const remainingWidth = Math.max(0, totalWidth - usedWidth);
655
+ const flexWidth = flexCols.length > 0 ? Math.floor(remainingWidth / flexCols.length) : 0;
656
+ return this._columns.map((c) => c.width ?? flexWidth);
657
+ }
658
+ _alignText(text, width, align) {
659
+ const truncated = truncate4(text, width);
660
+ const textWidth = stringWidth4(truncated);
661
+ const pad = Math.max(0, width - textWidth);
662
+ switch (align) {
663
+ case "right":
664
+ return " ".repeat(pad) + truncated;
665
+ case "center": {
666
+ const left = Math.floor(pad / 2);
667
+ const right = pad - left;
668
+ return " ".repeat(left) + truncated + " ".repeat(right);
669
+ }
670
+ case "left":
671
+ default:
672
+ return truncated + " ".repeat(pad);
673
+ }
674
+ }
675
+ };
676
+
677
+ // src/data/Gauge.ts
678
+ import { styleToCellAttrs as styleToCellAttrs8, stringWidth as stringWidth5 } from "@termuijs/core";
679
+ var Gauge = class extends Widget {
680
+ _label;
681
+ _value = 0;
682
+ _color;
683
+ _showLabel;
684
+ constructor(label, style = {}, opts = {}) {
685
+ super(style);
686
+ this._label = label;
687
+ this._color = opts.color ?? { type: "named", name: "green" };
688
+ this._showLabel = opts.showLabel ?? true;
689
+ }
690
+ setValue(value) {
691
+ this._value = Math.max(0, Math.min(1, value));
692
+ }
693
+ getValue() {
694
+ return this._value;
695
+ }
696
+ setLabel(label) {
697
+ this._label = label;
698
+ }
699
+ _renderSelf(screen) {
700
+ const rect = this._getContentRect();
701
+ const { x, y, width, height } = rect;
702
+ if (width <= 0 || height <= 0) return;
703
+ const attrs = styleToCellAttrs8(this._style);
704
+ const labelStr = this._label + " ";
705
+ const percentStr = this._showLabel ? ` ${Math.round(this._value * 100)}%` : "";
706
+ const labelWidth = stringWidth5(labelStr);
707
+ const percentWidth = stringWidth5(percentStr);
708
+ const barWidth = Math.max(0, width - labelWidth - percentWidth);
709
+ screen.writeString(x, y, labelStr, { ...attrs, bold: true });
710
+ const filled = Math.round(barWidth * this._value);
711
+ const barX = x + labelWidth;
712
+ for (let i = 0; i < barWidth; i++) {
713
+ const char = i < filled ? "\u2588" : "\u2591";
714
+ screen.setCell(barX + i, y, {
715
+ char,
716
+ fg: i < filled ? this._color : { type: "named", name: "brightBlack" }
717
+ });
718
+ }
719
+ if (this._showLabel) {
720
+ screen.writeString(barX + barWidth, y, percentStr, {
721
+ ...attrs,
722
+ bold: true
723
+ });
724
+ }
725
+ }
726
+ };
727
+
728
+ // src/data/Sparkline.ts
729
+ import { styleToCellAttrs as styleToCellAttrs9 } from "@termuijs/core";
730
+ var SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
731
+ var Sparkline = class extends Widget {
732
+ _label;
733
+ _data = [];
734
+ _color;
735
+ _showRange;
736
+ constructor(label, style = {}, opts = {}) {
737
+ super(style);
738
+ this._label = label;
739
+ this._color = opts.color ?? { type: "named", name: "cyan" };
740
+ this._showRange = opts.showRange ?? false;
741
+ }
742
+ setData(data) {
743
+ this._data = data;
744
+ }
745
+ pushValue(value) {
746
+ this._data.push(value);
747
+ }
748
+ _renderSelf(screen) {
749
+ const rect = this._getContentRect();
750
+ const { x, y, width, height } = rect;
751
+ if (width <= 0 || height <= 0) return;
752
+ const attrs = styleToCellAttrs9(this._style);
753
+ const labelStr = this._label + " ";
754
+ const labelWidth = labelStr.length;
755
+ screen.writeString(x, y, labelStr, { ...attrs, bold: true });
756
+ const sparkWidth = width - labelWidth;
757
+ if (sparkWidth <= 0 || this._data.length === 0) return;
758
+ const data = this._data.slice(-sparkWidth);
759
+ const min = Math.min(...data);
760
+ const max = Math.max(...data);
761
+ const range = max - min || 1;
762
+ for (let i = 0; i < data.length; i++) {
763
+ const normalized = (data[i] - min) / range;
764
+ const charIdx = Math.min(7, Math.floor(normalized * 8));
765
+ screen.setCell(x + labelWidth + i, y, {
766
+ char: SPARK_CHARS[charIdx],
767
+ fg: this._color
768
+ });
769
+ }
770
+ if (this._showRange && height > 1) {
771
+ const rangeStr = `${min.toFixed(0)}\u2013${max.toFixed(0)}`;
772
+ screen.writeString(x + labelWidth, y + 1, rangeStr, {
773
+ ...attrs,
774
+ dim: true
775
+ });
776
+ }
777
+ }
778
+ };
779
+
780
+ // src/data/StatusIndicator.ts
781
+ import { styleToCellAttrs as styleToCellAttrs10 } from "@termuijs/core";
782
+ var StatusIndicator = class extends Widget {
783
+ _label;
784
+ _isUp;
785
+ _upColor;
786
+ _downColor;
787
+ constructor(label, isUp, style = {}, opts = {}) {
788
+ super(style);
789
+ this._label = label;
790
+ this._isUp = isUp;
791
+ this._upColor = opts.upColor ?? { type: "named", name: "green" };
792
+ this._downColor = opts.downColor ?? { type: "named", name: "red" };
793
+ }
794
+ setStatus(isUp) {
795
+ this._isUp = isUp;
796
+ }
797
+ getStatus() {
798
+ return this._isUp;
799
+ }
800
+ setLabel(label) {
801
+ this._label = label;
802
+ }
803
+ _renderSelf(screen) {
804
+ const rect = this._getContentRect();
805
+ const { x, y, width, height } = rect;
806
+ if (width <= 0 || height <= 0) return;
807
+ const attrs = styleToCellAttrs10(this._style);
808
+ const dot = this._isUp ? "\u25CF" : "\u25CB";
809
+ const statusText = this._isUp ? "Online" : "Offline";
810
+ const color = this._isUp ? this._upColor : this._downColor;
811
+ screen.setCell(x, y, { char: dot, fg: color });
812
+ screen.writeString(x + 2, y, `${this._label} \u2014 ${statusText}`, {
813
+ ...attrs,
814
+ fg: color
815
+ });
816
+ }
817
+ };
818
+
819
+ // src/feedback/ProgressBar.ts
820
+ import { styleToCellAttrs as styleToCellAttrs11 } from "@termuijs/core";
821
+ var ProgressBar = class extends Widget {
822
+ _value;
823
+ _fillChar;
824
+ _emptyChar;
825
+ _fillColor;
826
+ _showLabel;
827
+ _labelFormat;
828
+ _total;
829
+ constructor(style = {}, options = {}) {
830
+ super({ height: 1, ...style });
831
+ this._value = Math.max(0, Math.min(1, options.value ?? 0));
832
+ this._fillChar = options.fillChar ?? "\u2588";
833
+ this._emptyChar = options.emptyChar ?? "\u2591";
834
+ this._fillColor = options.fillColor ?? { type: "named", name: "green" };
835
+ this._showLabel = options.showLabel ?? true;
836
+ this._labelFormat = options.labelFormat ?? "percent";
837
+ this._total = options.total ?? 100;
838
+ }
839
+ /** Set progress value (0–1) */
840
+ setValue(value) {
841
+ this._value = Math.max(0, Math.min(1, value));
842
+ }
843
+ get value() {
844
+ return this._value;
845
+ }
846
+ _renderSelf(screen) {
847
+ const rect = this._getContentRect();
848
+ const { x, y, width } = rect;
849
+ if (width <= 0) return;
850
+ const attrs = styleToCellAttrs11(this._style);
851
+ let label = "";
852
+ if (this._showLabel) {
853
+ if (this._labelFormat === "percent") {
854
+ label = ` ${Math.round(this._value * 100)}%`;
855
+ } else {
856
+ label = ` ${Math.round(this._value * this._total)}/${this._total}`;
857
+ }
858
+ }
859
+ const barWidth = Math.max(0, width - label.length);
860
+ const filled = Math.round(barWidth * this._value);
861
+ const empty = barWidth - filled;
862
+ for (let i = 0; i < filled; i++) {
863
+ screen.setCell(x + i, y, { char: this._fillChar, ...attrs, fg: this._fillColor });
864
+ }
865
+ for (let i = 0; i < empty; i++) {
866
+ screen.setCell(x + filled + i, y, { char: this._emptyChar, ...attrs, dim: true });
867
+ }
868
+ if (label) {
869
+ screen.writeString(x + barWidth, y, label, { ...attrs, bold: true });
870
+ }
871
+ }
872
+ };
873
+
874
+ // src/feedback/Spinner.ts
875
+ import { styleToCellAttrs as styleToCellAttrs12 } from "@termuijs/core";
876
+ var SPINNER_FRAMES = {
877
+ dots: {
878
+ frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
879
+ interval: 80
880
+ },
881
+ line: {
882
+ frames: ["-", "\\", "|", "/"],
883
+ interval: 130
884
+ },
885
+ star: {
886
+ frames: ["\u2736", "\u2738", "\u2739", "\u273A", "\u2739", "\u2737"],
887
+ interval: 70
888
+ },
889
+ arc: {
890
+ frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"],
891
+ interval: 100
892
+ },
893
+ circle: {
894
+ frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"],
895
+ interval: 120
896
+ },
897
+ bounce: {
898
+ frames: ["\u2801", "\u2802", "\u2804", "\u2802"],
899
+ interval: 120
900
+ },
901
+ arrow: {
902
+ frames: ["\u2190", "\u2196", "\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199"],
903
+ interval: 100
904
+ },
905
+ clock: {
906
+ frames: ["\u{1F550}", "\u{1F551}", "\u{1F552}", "\u{1F553}", "\u{1F554}", "\u{1F555}", "\u{1F556}", "\u{1F557}", "\u{1F558}", "\u{1F559}", "\u{1F55A}", "\u{1F55B}"],
907
+ interval: 100
908
+ }
909
+ };
910
+ var Spinner = class extends Widget {
911
+ _frames;
912
+ _interval;
913
+ _frameIndex = 0;
914
+ _label;
915
+ _color;
916
+ _lastTick = 0;
917
+ _elapsed = 0;
918
+ constructor(style = {}, options = {}) {
919
+ super({ height: 1, ...style });
920
+ const spinnerDef = typeof options.spinner === "string" ? SPINNER_FRAMES[options.spinner] ?? SPINNER_FRAMES.dots : options.spinner ?? SPINNER_FRAMES.dots;
921
+ this._frames = spinnerDef.frames;
922
+ this._interval = spinnerDef.interval;
923
+ this._label = options.label ?? "";
924
+ this._color = options.color ?? { type: "named", name: "cyan" };
925
+ }
926
+ /** Update the spinner label */
927
+ setLabel(label) {
928
+ this._label = label;
929
+ }
930
+ /**
931
+ * Advance the spinner frame based on elapsed time.
932
+ * Call this with a delta (ms) from the render loop.
933
+ */
934
+ tick(deltaMs) {
935
+ this._elapsed += deltaMs;
936
+ if (this._elapsed >= this._interval) {
937
+ this._frameIndex = (this._frameIndex + 1) % this._frames.length;
938
+ this._elapsed = 0;
939
+ }
940
+ }
941
+ _renderSelf(screen) {
942
+ const rect = this._getContentRect();
943
+ const { x, y, width } = rect;
944
+ if (width <= 0) return;
945
+ const attrs = styleToCellAttrs12(this._style);
946
+ const frame = this._frames[this._frameIndex];
947
+ screen.writeString(x, y, frame, { ...attrs, fg: this._color });
948
+ if (this._label) {
949
+ screen.writeString(x + 2, y, this._label, attrs);
950
+ }
951
+ }
952
+ };
953
+ export {
954
+ Box,
955
+ Gauge,
956
+ List,
957
+ LogView,
958
+ ProgressBar,
959
+ SPINNER_FRAMES,
960
+ Sparkline,
961
+ Spinner,
962
+ StatusIndicator,
963
+ Table,
964
+ Text,
965
+ TextInput,
966
+ Widget
967
+ };
968
+ //# sourceMappingURL=index.js.map