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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/components/Box.d.ts +33 -0
- package/dist/components/Box.js +242 -0
- package/dist/components/Button.d.ts +23 -0
- package/dist/components/Button.js +76 -0
- package/dist/components/Checkbox.d.ts +20 -0
- package/dist/components/Checkbox.js +40 -0
- package/dist/components/Input.d.ts +23 -0
- package/dist/components/Input.js +79 -0
- package/dist/components/Menu.d.ts +27 -0
- package/dist/components/Menu.js +93 -0
- package/dist/components/Popup.d.ts +7 -0
- package/dist/components/Popup.js +50 -0
- package/dist/components/ProgressBar.d.ts +28 -0
- package/dist/components/ProgressBar.js +47 -0
- package/dist/components/Radio.d.ts +22 -0
- package/dist/components/Radio.js +64 -0
- package/dist/components/Select.d.ts +24 -0
- package/dist/components/Select.js +126 -0
- package/dist/components/Tab.d.ts +24 -0
- package/dist/components/Tab.js +72 -0
- package/dist/components/Table.d.ts +25 -0
- package/dist/components/Table.js +116 -0
- package/dist/components/Textarea.d.ts +26 -0
- package/dist/components/Textarea.js +176 -0
- package/dist/components/Tree.d.ts +33 -0
- package/dist/components/Tree.js +166 -0
- package/dist/core/Component.d.ts +6 -0
- package/dist/core/Component.js +5 -0
- package/dist/core/Cursor.d.ts +7 -0
- package/dist/core/Cursor.js +59 -0
- package/dist/core/Events.d.ts +11 -0
- package/dist/core/Events.js +37 -0
- package/dist/core/Focus.d.ts +16 -0
- package/dist/core/Focus.js +69 -0
- package/dist/core/Input.d.ts +13 -0
- package/dist/core/Input.js +88 -0
- package/dist/core/Layout.d.ts +18 -0
- package/dist/core/Layout.js +65 -0
- package/dist/core/Screen.d.ts +78 -0
- package/dist/core/Screen.js +373 -0
- package/dist/core/Styler.d.ts +71 -0
- package/dist/core/Styler.js +157 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +41 -0
- 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;
|
package/dist/index.d.ts
ADDED
|
@@ -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);
|