voonex 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/components/Box.d.ts +33 -0
  4. package/dist/components/Box.js +242 -0
  5. package/dist/components/Button.d.ts +23 -0
  6. package/dist/components/Button.js +76 -0
  7. package/dist/components/Checkbox.d.ts +20 -0
  8. package/dist/components/Checkbox.js +40 -0
  9. package/dist/components/Input.d.ts +23 -0
  10. package/dist/components/Input.js +79 -0
  11. package/dist/components/Menu.d.ts +27 -0
  12. package/dist/components/Menu.js +93 -0
  13. package/dist/components/Popup.d.ts +7 -0
  14. package/dist/components/Popup.js +50 -0
  15. package/dist/components/ProgressBar.d.ts +28 -0
  16. package/dist/components/ProgressBar.js +47 -0
  17. package/dist/components/Radio.d.ts +22 -0
  18. package/dist/components/Radio.js +64 -0
  19. package/dist/components/Select.d.ts +24 -0
  20. package/dist/components/Select.js +126 -0
  21. package/dist/components/Tab.d.ts +24 -0
  22. package/dist/components/Tab.js +72 -0
  23. package/dist/components/Table.d.ts +25 -0
  24. package/dist/components/Table.js +116 -0
  25. package/dist/components/Textarea.d.ts +26 -0
  26. package/dist/components/Textarea.js +176 -0
  27. package/dist/components/Tree.d.ts +33 -0
  28. package/dist/components/Tree.js +166 -0
  29. package/dist/core/Component.d.ts +6 -0
  30. package/dist/core/Component.js +5 -0
  31. package/dist/core/Cursor.d.ts +7 -0
  32. package/dist/core/Cursor.js +59 -0
  33. package/dist/core/Events.d.ts +11 -0
  34. package/dist/core/Events.js +37 -0
  35. package/dist/core/Focus.d.ts +16 -0
  36. package/dist/core/Focus.js +69 -0
  37. package/dist/core/Input.d.ts +13 -0
  38. package/dist/core/Input.js +88 -0
  39. package/dist/core/Layout.d.ts +18 -0
  40. package/dist/core/Layout.js +65 -0
  41. package/dist/core/Screen.d.ts +78 -0
  42. package/dist/core/Screen.js +373 -0
  43. package/dist/core/Styler.d.ts +71 -0
  44. package/dist/core/Styler.js +157 -0
  45. package/dist/index.d.ts +25 -0
  46. package/dist/index.js +41 -0
  47. package/package.json +29 -0
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ // ==========================================
3
+ // CORE: SCREEN MANAGER (The "Canvas")
4
+ // ==========================================
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Screen = void 0;
7
+ const Cursor_1 = require("./Cursor");
8
+ class Screen {
9
+ /**
10
+ * Enters the Alternate Screen Buffer (like Vim or Nano).
11
+ */
12
+ static enter() {
13
+ if (this.isAlternateBuffer)
14
+ return;
15
+ process.stdout.write('\x1b[?1049h'); // Enter alternate buffer
16
+ Cursor_1.Cursor.hide();
17
+ this.isAlternateBuffer = true;
18
+ this.resizeBuffers();
19
+ // Removed startLoop(), now using reactive rendering
20
+ this.onResize(() => {
21
+ this.resizeBuffers();
22
+ this.scheduleRender();
23
+ });
24
+ }
25
+ /**
26
+ * Leaves the Alternate Screen Buffer and restores cursor.
27
+ */
28
+ static leave() {
29
+ if (!this.isAlternateBuffer)
30
+ return;
31
+ // Removed stopLoop()
32
+ Cursor_1.Cursor.show();
33
+ process.stdout.write('\x1b[?1049l'); // Leave alternate buffer
34
+ this.isAlternateBuffer = false;
35
+ }
36
+ static resizeBuffers() {
37
+ this.width = process.stdout.columns || 80;
38
+ this.height = process.stdout.rows || 24;
39
+ // Initialize or Resize Buffers
40
+ this.currentBuffer = Array(this.height).fill(null).map(() => Array(this.width).fill(null).map(() => ({ char: ' ', style: '' })));
41
+ this.previousBuffer = Array(this.height).fill(null).map(() => Array(this.width).fill(null).map(() => ({ char: ' ', style: '' })));
42
+ // Mark all rows as dirty to ensure full initial render
43
+ this.dirtyRows = Array(this.height).fill(true);
44
+ }
45
+ /**
46
+ * Schedules a render flush in the next tick.
47
+ * Call this whenever a component changes state.
48
+ */
49
+ static scheduleRender() {
50
+ if (this.renderScheduled)
51
+ return;
52
+ this.renderScheduled = true;
53
+ // Use setImmediate to batch updates in the same tick
54
+ setImmediate(() => {
55
+ this.flush();
56
+ this.renderScheduled = false;
57
+ });
58
+ }
59
+ /**
60
+ * Writes text at a specific coordinate into the virtual buffer.
61
+ * Handles ANSI escape codes using a state machine parser.
62
+ * Supports clipping region and relative positioning.
63
+ */
64
+ static write(x, y, text, clipRect) {
65
+ if (!this.isAlternateBuffer)
66
+ return;
67
+ // Apply Clipping Translation: (Relative -> Absolute)
68
+ let absX = x;
69
+ let absY = y;
70
+ if (clipRect) {
71
+ absX += clipRect.x;
72
+ absY += clipRect.y;
73
+ }
74
+ // Apply Clipping Bounds
75
+ const clipX1 = clipRect ? clipRect.x : 0;
76
+ const clipY1 = clipRect ? clipRect.y : 0;
77
+ const clipX2 = clipRect ? clipRect.x + clipRect.width : this.width;
78
+ const clipY2 = clipRect ? clipRect.y + clipRect.height : this.height;
79
+ if (absY < clipY1 || absY >= clipY2 || absY >= this.height)
80
+ return;
81
+ // Mark row as dirty
82
+ this.dirtyRows[absY] = true;
83
+ this.scheduleRender(); // Trigger render
84
+ let currentX = absX;
85
+ let currentStyle = "";
86
+ // ANSI State Machine Parser
87
+ let i = 0;
88
+ const len = text.length;
89
+ while (i < len) {
90
+ const char = text[i];
91
+ if (char === '\x1b') {
92
+ // Potential Escape Sequence
93
+ if (i + 1 < len && text[i + 1] === '[') {
94
+ // ANSI Control Sequence Identifier (CSI)
95
+ let j = i + 2;
96
+ let code = "";
97
+ // Read until 'm' or other terminator
98
+ while (j < len) {
99
+ const c = text[j];
100
+ code += c;
101
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
102
+ break;
103
+ }
104
+ j++;
105
+ }
106
+ if (code.endsWith('m')) {
107
+ // Color/Style Code
108
+ const fullCode = `\x1b[${code}`;
109
+ if (fullCode === '\x1b[0m' || fullCode === '\x1b[m') {
110
+ currentStyle = "";
111
+ }
112
+ else {
113
+ currentStyle += fullCode;
114
+ }
115
+ }
116
+ i = j + 1;
117
+ continue;
118
+ }
119
+ }
120
+ // Normal Character
121
+ // Check Clipping Horizontally
122
+ if (currentX >= clipX1 && currentX < clipX2 && currentX < this.width) {
123
+ this.currentBuffer[absY][currentX] = {
124
+ char: char,
125
+ style: currentStyle
126
+ };
127
+ }
128
+ currentX++;
129
+ i++;
130
+ }
131
+ }
132
+ /**
133
+ * Pushes the current screen state to a stack.
134
+ */
135
+ static pushLayer() {
136
+ const snapshot = this.currentBuffer.map(row => row.map(cell => ({ ...cell })));
137
+ this.layerStack.push(snapshot);
138
+ }
139
+ /**
140
+ * Restores the screen state from the stack.
141
+ */
142
+ static popLayer() {
143
+ const snapshot = this.layerStack.pop();
144
+ if (snapshot) {
145
+ if (snapshot.length === this.height && snapshot[0].length === this.width) {
146
+ this.currentBuffer = snapshot;
147
+ this.dirtyRows.fill(true);
148
+ this.scheduleRender();
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Clears the virtual buffer.
154
+ */
155
+ static clear() {
156
+ if (!this.currentBuffer.length)
157
+ return;
158
+ for (let y = 0; y < this.height; y++) {
159
+ this.dirtyRows[y] = true;
160
+ for (let x = 0; x < this.width; x++) {
161
+ this.currentBuffer[y][x] = { char: ' ', style: '' };
162
+ }
163
+ }
164
+ this.scheduleRender();
165
+ }
166
+ /**
167
+ * Registers a root component for the rendering loop (Painter's Algorithm).
168
+ * @param renderFn The function that renders the component
169
+ * @param zIndex Priority (higher draws on top)
170
+ */
171
+ static mount(renderFn, zIndex = 0) {
172
+ this.renderRoots.push({ render: renderFn, zIndex });
173
+ this.renderRoots.sort((a, b) => a.zIndex - b.zIndex);
174
+ this.scheduleRender();
175
+ }
176
+ /**
177
+ * Unregisters a root component from the rendering loop.
178
+ * @param renderFn The function that renders the component
179
+ */
180
+ static unmount(renderFn) {
181
+ this.renderRoots = this.renderRoots.filter(r => r.render !== renderFn);
182
+ this.scheduleRender();
183
+ }
184
+ /**
185
+ * Diffs currentBuffer vs previousBuffer and writes changes to stdout.
186
+ * Optimized with String Batching and Relative Cursor Moves.
187
+ */
188
+ static flush() {
189
+ // Painter's Algorithm Phase: Re-run all render functions if any exist
190
+ // This clears the buffer and redraws everything from scratch (logically)
191
+ // But to keep performance, we might want to just let them draw over currentBuffer?
192
+ // User asked for: "1. Xóa Buffer ảo. 2. Duyệt qua danh sách...".
193
+ // If we have registered roots, we should follow this.
194
+ if (this.renderRoots.length > 0) {
195
+ // We need to clear currentBuffer effectively?
196
+ // Or maybe just let them overwrite. If transparency is involved, clearing is safer.
197
+ // But clearing everything is expensive if we just diff later.
198
+ // The Diff algorithm handles the "screen update" optimization.
199
+ // The "Virtual Buffer" update needs to be correct.
200
+ // If we have layers, and top layer moves, we need to redraw bottom layer to see what's behind.
201
+ // So YES, we must clear currentBuffer (or reset it to background state) and redraw all layers.
202
+ // Reset currentBuffer to empty/clean state WITHOUT affecting previousBuffer (screen state)
203
+ for (let y = 0; y < this.height; y++) {
204
+ for (let x = 0; x < this.width; x++) {
205
+ this.currentBuffer[y][x] = { char: ' ', style: '' };
206
+ }
207
+ // We don't mark dirtyRows here blindly, we mark them if they CHANGE in the diff phase.
208
+ // Wait, dirtyRows optimization relies on us knowing which rows MIGHT have changed.
209
+ // If we clear everything, we potentially change everything.
210
+ // So we should mark all as dirty?
211
+ // "Dirty" means "Virtual Buffer Row differs from Previous Buffer Row".
212
+ // Since we are rebuilding Virtual Buffer, we don't know yet.
213
+ // We should just run the diff loop on all rows?
214
+ // Or maybe we can track which rows are touched during render?
215
+ }
216
+ // Render all layers
217
+ for (const root of this.renderRoots) {
218
+ root.render();
219
+ }
220
+ // Mark all rows as dirty for the Diff phase to check them?
221
+ // Since we rebuilt the buffer, any row could be different from previousBuffer.
222
+ // Optimization: Maybe only rows that were touched?
223
+ // But 'write' marks dirtyRows.
224
+ // So if we clear buffer (resetting chars), we are effectively writing spaces.
225
+ // If we use 'clear()' method, it marks all dirty.
226
+ // Let's rely on 'write' marking dirtyRows.
227
+ // But we just did manual reset loop above.
228
+ // Let's check:
229
+ // If we reset manual loop, we are changing 'currentBuffer'.
230
+ // Previous buffer holds what is on screen.
231
+ // If we don't mark dirtyRows, flush() skips the row.
232
+ // If screen has text, and we cleared it to spaces, and didn't mark dirty, screen stays text. Bad.
233
+ // So yes, we should mark all dirty if we do a full clear-redraw cycle.
234
+ this.dirtyRows.fill(true);
235
+ }
236
+ if (!this.currentBuffer.length)
237
+ return;
238
+ let output = "";
239
+ let lastY = -1;
240
+ let lastX = -1;
241
+ let lastStyle = "";
242
+ for (let y = 0; y < this.height; y++) {
243
+ if (!this.dirtyRows[y])
244
+ continue;
245
+ this.dirtyRows[y] = false;
246
+ let x = 0;
247
+ while (x < this.width) {
248
+ const cell = this.currentBuffer[y][x];
249
+ const prev = this.previousBuffer[y][x];
250
+ // Check for change
251
+ if (cell.char !== prev.char || cell.style !== prev.style) {
252
+ // Update Previous Buffer
253
+ this.previousBuffer[y][x] = { ...cell };
254
+ // Move Cursor
255
+ // Relative Move Optimization
256
+ if (y === lastY && x === lastX + 1) {
257
+ // Already at correct position
258
+ }
259
+ else if (y === lastY && x > lastX && x < lastX + 5) {
260
+ // Close enough for right move
261
+ const diff = x - (lastX + 1);
262
+ if (diff > 0)
263
+ output += `\x1b[${diff}C`;
264
+ // If diff is 0 (next char), we do nothing.
265
+ // Actually if x = lastX + 2, diff is 1. We need \x1b[1C.
266
+ }
267
+ else {
268
+ // Absolute Move
269
+ output += `\x1b[${y + 1};${x + 1}H`;
270
+ }
271
+ // Update Style
272
+ if (cell.style !== lastStyle) {
273
+ output += `\x1b[0m${cell.style}`;
274
+ lastStyle = cell.style;
275
+ }
276
+ // String Batching: Look ahead for same style
277
+ let batch = cell.char;
278
+ let nextX = x + 1;
279
+ while (nextX < this.width) {
280
+ const nextCell = this.currentBuffer[y][nextX];
281
+ const nextPrev = this.previousBuffer[y][nextX];
282
+ // We only batch if the NEXT cell also NEEDS update AND has SAME style
283
+ // Actually, even if next cell DOES NOT need update,
284
+ // if we write over it with same content, it's fine (redundant write but saves cursor move).
285
+ // BUT, if we write over it, we might overwrite something valid with something else?
286
+ // No, currentBuffer is truth.
287
+ // Strategy:
288
+ // 1. Only batch consecutive CHANGED cells?
289
+ // 2. Or batch consecutive cells regardless, as long as style matches, to avoid cursor jumps?
290
+ // If we skip a cell (not changed), we have to move cursor.
291
+ // Moving cursor costs bytes (e.g. \x1b[C is 3 bytes).
292
+ // Writing a char is 1 byte.
293
+ // So overwriting valid char is cheaper than skipping 1-2 chars.
294
+ // But if we skip 10 chars, move is cheaper.
295
+ // Let's stick to simple logic: Only batch if next cell ALSO needs update OR we just overwrite it anyway to jump gap?
296
+ // The user asked for "Gộp chuỗi ký tự liên tiếp có cùng style".
297
+ // If we strictly follow diff, we only write changed cells.
298
+ // So let's look for consecutive CHANGED cells with same style.
299
+ if (nextCell.char !== nextPrev.char || nextCell.style !== nextPrev.style) {
300
+ if (nextCell.style === cell.style) {
301
+ batch += nextCell.char;
302
+ // Update previous buffer as we consume
303
+ this.previousBuffer[y][nextX] = { ...nextCell };
304
+ nextX++;
305
+ }
306
+ else {
307
+ break; // Style changed
308
+ }
309
+ }
310
+ else {
311
+ // Next cell is not changed.
312
+ // Should we continue batching to bridge the gap?
313
+ // If gap is small (1-2 chars), yes.
314
+ // But that complicates logic. Let's stop batching.
315
+ break;
316
+ }
317
+ }
318
+ output += batch;
319
+ lastY = y;
320
+ lastX = x + batch.length - 1; // last written position
321
+ x = nextX; // Continue loop
322
+ }
323
+ else {
324
+ x++;
325
+ }
326
+ }
327
+ }
328
+ if (output.length > 0) {
329
+ // Reset style
330
+ // output += '\x1b[0m';
331
+ process.stdout.write(output);
332
+ }
333
+ }
334
+ /**
335
+ * Gets current terminal dimensions.
336
+ */
337
+ static get size() {
338
+ return {
339
+ width: process.stdout.columns || 80,
340
+ height: process.stdout.rows || 24
341
+ };
342
+ }
343
+ /**
344
+ * Listens for resize events.
345
+ */
346
+ static onResize(callback) {
347
+ process.stdout.on('resize', () => {
348
+ if (this.resizeTimeout) {
349
+ clearTimeout(this.resizeTimeout);
350
+ }
351
+ this.resizeTimeout = setTimeout(() => {
352
+ callback();
353
+ }, 100);
354
+ });
355
+ }
356
+ }
357
+ exports.Screen = Screen;
358
+ Screen.isAlternateBuffer = false;
359
+ Screen.resizeTimeout = null;
360
+ // Double Buffering
361
+ Screen.currentBuffer = [];
362
+ Screen.previousBuffer = [];
363
+ Screen.dirtyRows = [];
364
+ // Layer Stack
365
+ // Stores snapshots of the buffer before a new layer is added.
366
+ Screen.layerStack = [];
367
+ // Reactive Rendering
368
+ Screen.isDirty = false;
369
+ Screen.renderScheduled = false;
370
+ Screen.width = 0;
371
+ Screen.height = 0;
372
+ // Component Registry for Layering (Painter's Algorithm)
373
+ Screen.renderRoots = [];
@@ -0,0 +1,71 @@
1
+ export declare const COLORS: {
2
+ readonly black: 30;
3
+ readonly red: 31;
4
+ readonly green: 32;
5
+ readonly yellow: 33;
6
+ readonly blue: 34;
7
+ readonly magenta: 35;
8
+ readonly cyan: 36;
9
+ readonly white: 37;
10
+ readonly gray: 90;
11
+ readonly brightRed: 91;
12
+ readonly brightGreen: 92;
13
+ readonly brightYellow: 93;
14
+ readonly brightBlue: 94;
15
+ readonly brightMagenta: 95;
16
+ readonly brightCyan: 96;
17
+ readonly brightWhite: 97;
18
+ readonly bgBlack: 40;
19
+ readonly bgRed: 41;
20
+ readonly bgGreen: 42;
21
+ readonly bgYellow: 43;
22
+ readonly bgBlue: 44;
23
+ readonly bgMagenta: 45;
24
+ readonly bgCyan: 46;
25
+ readonly bgWhite: 47;
26
+ };
27
+ export declare const MODIFIERS: {
28
+ readonly bold: 1;
29
+ readonly dim: 2;
30
+ readonly italic: 3;
31
+ readonly underline: 4;
32
+ readonly inverse: 7;
33
+ readonly hidden: 8;
34
+ readonly strikethrough: 9;
35
+ };
36
+ export type ColorName = keyof typeof COLORS;
37
+ export type ModifierName = keyof typeof MODIFIERS;
38
+ export declare class Styler {
39
+ /**
40
+ * Applies ANSI styles to a string.
41
+ */
42
+ static style(text: string, ...styles: (ColorName | ModifierName)[]): string;
43
+ /**
44
+ * Removes ANSI codes to calculate real string length.
45
+ */
46
+ static strip(text: string): string;
47
+ /**
48
+ * Checks if a character code point is 'wide' (takes 2 columns).
49
+ * Simplified lookup for CJK and Emoji.
50
+ */
51
+ private static isWide;
52
+ /**
53
+ * Returns the visual length of the string (ignoring ANSI codes).
54
+ * Handles CJK and Emojis as 2 columns.
55
+ */
56
+ static len(text: string): number;
57
+ /**
58
+ * Truncates a string to fit within a visual width.
59
+ * Returns the substring.
60
+ */
61
+ static truncate(text: string, maxWidth: number): string;
62
+ /**
63
+ * Advanced truncation that preserves ANSI state.
64
+ * Returns the truncated string and the active ANSI style at the cut point.
65
+ */
66
+ static truncateWithState(text: string, maxWidth: number): {
67
+ result: string;
68
+ remaining: string;
69
+ endStyle: string;
70
+ };
71
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ // ==========================================
3
+ // CORE: ANSI & STYLING
4
+ // ==========================================
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Styler = exports.MODIFIERS = exports.COLORS = void 0;
7
+ const ESC = '\x1b[';
8
+ const RESET = `${ESC}0m`;
9
+ exports.COLORS = {
10
+ // Foreground
11
+ black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37,
12
+ gray: 90, brightRed: 91, brightGreen: 92, brightYellow: 93, brightBlue: 94, brightMagenta: 95, brightCyan: 96, brightWhite: 97,
13
+ // Background
14
+ bgBlack: 40, bgRed: 41, bgGreen: 42, bgYellow: 43, bgBlue: 44, bgMagenta: 45, bgCyan: 46, bgWhite: 47
15
+ };
16
+ exports.MODIFIERS = {
17
+ bold: 1, dim: 2, italic: 3, underline: 4, inverse: 7, hidden: 8, strikethrough: 9
18
+ };
19
+ class Styler {
20
+ /**
21
+ * Applies ANSI styles to a string.
22
+ */
23
+ static style(text, ...styles) {
24
+ const codes = styles.map(s => {
25
+ if (s in exports.COLORS)
26
+ return exports.COLORS[s];
27
+ if (s in exports.MODIFIERS)
28
+ return exports.MODIFIERS[s];
29
+ return '';
30
+ }).filter(Boolean).join(';');
31
+ return `${ESC}${codes}m${text}${RESET}`;
32
+ }
33
+ /**
34
+ * Removes ANSI codes to calculate real string length.
35
+ */
36
+ static strip(text) {
37
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
38
+ }
39
+ /**
40
+ * Checks if a character code point is 'wide' (takes 2 columns).
41
+ * Simplified lookup for CJK and Emoji.
42
+ */
43
+ static isWide(code) {
44
+ if (!code)
45
+ return false;
46
+ return (code >= 0x1100 && ((code <= 0x115f) || // Hangul Jamo
47
+ (code === 0x2329) || // Left-pointing Angle Bracket
48
+ (code === 0x232a) || // Right-pointing Angle Bracket
49
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || // CJK ... Yi
50
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables
51
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
52
+ (code >= 0xfe10 && code <= 0xfe19) || // Vertical forms
53
+ (code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms
54
+ (code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms
55
+ (code >= 0xffe0 && code <= 0xffe6) ||
56
+ (code >= 0x1f300 && code <= 0x1f64f) || // Misc Symbols and Pictographs
57
+ (code >= 0x1f900 && code <= 0x1f9ff) // Supplemental Symbols and Pictographs
58
+ ));
59
+ }
60
+ /**
61
+ * Returns the visual length of the string (ignoring ANSI codes).
62
+ * Handles CJK and Emojis as 2 columns.
63
+ */
64
+ static len(text) {
65
+ const stripped = this.strip(text);
66
+ let len = 0;
67
+ for (const char of stripped) {
68
+ len += this.isWide(char.codePointAt(0)) ? 2 : 1;
69
+ }
70
+ return len;
71
+ }
72
+ /**
73
+ * Truncates a string to fit within a visual width.
74
+ * Returns the substring.
75
+ */
76
+ static truncate(text, maxWidth) {
77
+ const { result } = this.truncateWithState(text, maxWidth);
78
+ return result;
79
+ }
80
+ /**
81
+ * Advanced truncation that preserves ANSI state.
82
+ * Returns the truncated string and the active ANSI style at the cut point.
83
+ */
84
+ static truncateWithState(text, maxWidth) {
85
+ let width = 0;
86
+ let result = "";
87
+ let currentStyle = ""; // Tracks active style
88
+ let remaining = "";
89
+ const parts = text.split(/(\x1b\[[0-9;]*m)/);
90
+ let finished = false;
91
+ for (let i = 0; i < parts.length; i++) {
92
+ const part = parts[i];
93
+ if (finished) {
94
+ remaining += part;
95
+ continue;
96
+ }
97
+ if (part.startsWith('\x1b[')) {
98
+ result += part;
99
+ // Track style state
100
+ if (part === '\x1b[0m' || part === '\x1b[m') {
101
+ currentStyle = "";
102
+ }
103
+ else {
104
+ // This is a naive stack approximation.
105
+ // Real ANSI handling might be more complex (accumulating vs replacing).
106
+ // But usually in TUI we just append.
107
+ currentStyle += part;
108
+ }
109
+ }
110
+ else {
111
+ for (let j = 0; j < part.length; j++) {
112
+ // Iterate by code point to handle surrogate pairs correctly?
113
+ // JS strings are UTF-16. 'char' loop iterates code units.
114
+ // But we need code points for isWide.
115
+ // For simplicity, let's assume iterator handles surrogate pairs if we use `for...of` on string?
116
+ // Actually `for (const char of part)` iterates code points (unicode) in ES6.
117
+ // But we need index `j` to match `part`.
118
+ // We can't mix index loop and iterator easily.
119
+ // Let's use iterator and rebuild buffer.
120
+ // But wait, the outer loop IS an iterator.
121
+ // Previous implementation used `for (const char of part)`.
122
+ // Let's stick to that but we need to know when we stop to build `remaining`.
123
+ // Actually, let's restart the inner loop logic to be safer.
124
+ break;
125
+ }
126
+ // Better approach for inner text
127
+ let partIndex = 0;
128
+ for (const char of part) {
129
+ const charWidth = this.isWide(char.codePointAt(0)) ? 2 : 1;
130
+ if (width + charWidth > maxWidth) {
131
+ // We hit the limit.
132
+ // We need to stop here.
133
+ // `remaining` starts from here.
134
+ // `part` is the current text chunk.
135
+ remaining += part.substring(partIndex); // Rest of this part
136
+ finished = true;
137
+ break;
138
+ }
139
+ width += charWidth;
140
+ result += char;
141
+ partIndex += char.length; // char.length might be 2 for emojis
142
+ }
143
+ }
144
+ }
145
+ // If we finished with an active style, we should probably RESET it in result?
146
+ // Or leave it open? Screen.write handles it per cell.
147
+ // But if we print `result` then newline, the style might leak?
148
+ // Generally `truncate` returns the string to be printed.
149
+ // Ideally we append RESET to result if style is active, and PREPEND style to remaining.
150
+ return {
151
+ result: result + (currentStyle ? '\x1b[0m' : ''),
152
+ remaining: (currentStyle ? currentStyle : '') + remaining,
153
+ endStyle: currentStyle
154
+ };
155
+ }
156
+ }
157
+ exports.Styler = Styler;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Voonex - A zero-dependency Terminal UI Library.
3
+ * Copyright (c) CodeTease
4
+ */
5
+ export * from './core/Screen';
6
+ export * from './core/Input';
7
+ export * from './core/Styler';
8
+ export * from './core/Cursor';
9
+ export * from './core/Layout';
10
+ export * from './core/Focus';
11
+ export * from './core/Events';
12
+ export * from './core/Component';
13
+ export * from './components/Box';
14
+ export * from './components/Menu';
15
+ export * from './components/ProgressBar';
16
+ export * from './components/Table';
17
+ export * from './components/Tree';
18
+ export * from './components/Popup';
19
+ export * from './components/Button';
20
+ export * from './components/Input';
21
+ export * from './components/Textarea';
22
+ export * from './components/Select';
23
+ export * from './components/Checkbox';
24
+ export * from './components/Radio';
25
+ export * from './components/Tab';
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Voonex - A zero-dependency Terminal UI Library.
4
+ * Copyright (c) CodeTease
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ __exportStar(require("./core/Screen"), exports);
22
+ __exportStar(require("./core/Input"), exports);
23
+ __exportStar(require("./core/Styler"), exports);
24
+ __exportStar(require("./core/Cursor"), exports);
25
+ __exportStar(require("./core/Layout"), exports);
26
+ __exportStar(require("./core/Focus"), exports);
27
+ __exportStar(require("./core/Events"), exports);
28
+ __exportStar(require("./core/Component"), exports);
29
+ __exportStar(require("./components/Box"), exports);
30
+ __exportStar(require("./components/Menu"), exports);
31
+ __exportStar(require("./components/ProgressBar"), exports);
32
+ __exportStar(require("./components/Table"), exports);
33
+ __exportStar(require("./components/Tree"), exports);
34
+ __exportStar(require("./components/Popup"), exports);
35
+ __exportStar(require("./components/Button"), exports);
36
+ __exportStar(require("./components/Input"), exports);
37
+ __exportStar(require("./components/Textarea"), exports);
38
+ __exportStar(require("./components/Select"), exports);
39
+ __exportStar(require("./components/Checkbox"), exports);
40
+ __exportStar(require("./components/Radio"), exports);
41
+ __exportStar(require("./components/Tab"), exports);