linecraft 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/.cursor/plan.md +952 -0
- package/LICENSE +22 -0
- package/README.md +111 -0
- package/TESTING.md +102 -0
- package/build.zig +100 -0
- package/examples/basic-progress.ts +21 -0
- package/examples/multi-lane.ts +29 -0
- package/examples/spinner.ts +20 -0
- package/examples/test-basic.ts +23 -0
- package/lib/components/progress-bar.d.ts +19 -0
- package/lib/components/progress-bar.d.ts.map +1 -0
- package/lib/components/progress-bar.js +43 -0
- package/lib/components/progress-bar.js.map +1 -0
- package/lib/components/spinner.d.ts +18 -0
- package/lib/components/spinner.d.ts.map +1 -0
- package/lib/components/spinner.js +48 -0
- package/lib/components/spinner.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/native.d.ts +12 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +65 -0
- package/lib/native.js.map +1 -0
- package/lib/region.d.ts +17 -0
- package/lib/region.d.ts.map +1 -0
- package/lib/region.js +74 -0
- package/lib/region.js.map +1 -0
- package/lib/types.d.ts +32 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/utils/colors.d.ts +3 -0
- package/lib/utils/colors.d.ts.map +1 -0
- package/lib/utils/colors.js +61 -0
- package/lib/utils/colors.js.map +1 -0
- package/package.json +46 -0
- package/src/ts/components/progress-bar.ts +53 -0
- package/src/ts/components/spinner.ts +56 -0
- package/src/ts/index.ts +37 -0
- package/src/ts/native.ts +86 -0
- package/src/ts/region.ts +89 -0
- package/src/ts/types/ffi-napi.d.ts +11 -0
- package/src/ts/types/ref-napi.d.ts +5 -0
- package/src/ts/types.ts +53 -0
- package/src/ts/utils/colors.ts +72 -0
- package/src/zig/ansi.zig +21 -0
- package/src/zig/buffer.zig +37 -0
- package/src/zig/diff.zig +43 -0
- package/src/zig/region.zig +292 -0
- package/src/zig/renderer.zig +92 -0
- package/src/zig/test_ansi.zig +66 -0
- package/src/zig/test_buffer.zig +82 -0
- package/src/zig/test_diff.zig +220 -0
- package/src/zig/test_integration.zig +76 -0
- package/src/zig/test_region.zig +191 -0
- package/src/zig/test_runner.zig +27 -0
- package/src/zig/test_throttle.zig +59 -0
- package/src/zig/throttle.zig +38 -0
- package/tsconfig.json +21 -0
package/.cursor/plan.md
ADDED
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
# EchoKit - High-Performance Terminal UI Library
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
EchoKit is a high-performance terminal UI library for Node.js that uses Zig for the performance-critical terminal rendering operations. It provides a friendly TypeScript API while leveraging Zig's speed and efficiency for terminal operations.
|
|
6
|
+
|
|
7
|
+
## Core Philosophy
|
|
8
|
+
|
|
9
|
+
- **Zig handles performance**: Diffing, ANSI operations, buffering, flushing
|
|
10
|
+
- **Node.js handles logic**: UI components, state management, user API
|
|
11
|
+
- **Region-based rendering**: Manage a region of the terminal, not just single lines
|
|
12
|
+
- **Zero-copy where possible**: Minimize data movement between Node and Zig
|
|
13
|
+
- **Lock-free updates**: Support concurrent progress updates without blocking
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
### High-Level Flow
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Node.js (TypeScript)
|
|
21
|
+
↓ (calls renderer API)
|
|
22
|
+
Zig Native Addon (via N-API/FFI)
|
|
23
|
+
↓ (manages terminal region)
|
|
24
|
+
Terminal (via stdout/stderr)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Component Responsibilities
|
|
28
|
+
|
|
29
|
+
#### Zig Layer (`src/zig/`)
|
|
30
|
+
|
|
31
|
+
**Core Responsibilities:**
|
|
32
|
+
1. **Region Management**
|
|
33
|
+
- Track a rectangular region of the terminal
|
|
34
|
+
- Handle cursor positioning within the region
|
|
35
|
+
- Manage region boundaries and scrolling
|
|
36
|
+
|
|
37
|
+
2. **Double-Buffer Diffing**
|
|
38
|
+
- Maintain previous frame state
|
|
39
|
+
- Compare current frame vs previous frame
|
|
40
|
+
- Generate minimal ANSI operations for changes only
|
|
41
|
+
- Line-level diffing (detect which lines changed)
|
|
42
|
+
|
|
43
|
+
3. **ANSI Cursor Movement**
|
|
44
|
+
- Efficient cursor positioning (up/down/left/right)
|
|
45
|
+
- Save/restore cursor position
|
|
46
|
+
- Hide/show cursor during updates
|
|
47
|
+
- Clear operations (line, region, to end of line)
|
|
48
|
+
|
|
49
|
+
4. **Buffering & Flushing**
|
|
50
|
+
- Buffer ANSI operations
|
|
51
|
+
- Batch writes to stdout/stderr
|
|
52
|
+
- Flush at appropriate times (frame boundaries, explicit flush)
|
|
53
|
+
|
|
54
|
+
5. **Redraw Throttling**
|
|
55
|
+
- Limit redraw frequency (e.g., max 60 FPS)
|
|
56
|
+
- Queue updates and batch them
|
|
57
|
+
- Skip intermediate frames if updates come too fast
|
|
58
|
+
|
|
59
|
+
6. **Lock-Free Progress Updates**
|
|
60
|
+
- Atomic counters for progress values
|
|
61
|
+
- Thread-safe update operations
|
|
62
|
+
- Support parallel progress lanes
|
|
63
|
+
|
|
64
|
+
7. **Performance Optimizations**
|
|
65
|
+
- Zero-allocation hot paths where possible
|
|
66
|
+
- Efficient string handling
|
|
67
|
+
- Minimal system calls
|
|
68
|
+
- Fast ANSI code generation
|
|
69
|
+
|
|
70
|
+
**Zig API Surface (C ABI):**
|
|
71
|
+
|
|
72
|
+
```zig
|
|
73
|
+
// Region management
|
|
74
|
+
export fn create_region(x: u32, y: u32, width: u32, height: u32) RegionHandle;
|
|
75
|
+
export fn destroy_region(handle: RegionHandle) void;
|
|
76
|
+
export fn resize_region(handle: RegionHandle, width: u32, height: u32) void;
|
|
77
|
+
|
|
78
|
+
// Line updates (automatically batched and throttled by Zig)
|
|
79
|
+
// Note: line_number is 1-based (line 1 = first line)
|
|
80
|
+
export fn set_line(handle: RegionHandle, line_number: u32, content: [*]const u8, len: usize) void;
|
|
81
|
+
// Zig internally:
|
|
82
|
+
// - Convert to 0-based: line_index = line_number - 1
|
|
83
|
+
// - If line_index >= current height: expand region height
|
|
84
|
+
// - Buffers the update in pending_frame
|
|
85
|
+
// - Schedules a render (respecting throttle)
|
|
86
|
+
// - On render: diffs vs previous_frame, generates ANSI, writes to buffer
|
|
87
|
+
|
|
88
|
+
// Set entire content (multiple lines with \n separators)
|
|
89
|
+
export fn set(handle: RegionHandle, content: [*]const u8, len: usize) void;
|
|
90
|
+
// Zig internally:
|
|
91
|
+
// - Split by \n to get lines
|
|
92
|
+
// - Expand region height if needed
|
|
93
|
+
// - Update all lines in pending_frame
|
|
94
|
+
// - Schedule render
|
|
95
|
+
|
|
96
|
+
// Clear operations
|
|
97
|
+
export fn clear_line(handle: RegionHandle, line_number: u32) void; // 1-based
|
|
98
|
+
export fn clear_region(handle: RegionHandle) void;
|
|
99
|
+
|
|
100
|
+
// Note: Progress tracking is handled at the component level (ProgressBar)
|
|
101
|
+
// The atomic progress updates in Zig are an internal implementation detail
|
|
102
|
+
// for lock-free updates when multiple threads update progress concurrently
|
|
103
|
+
|
|
104
|
+
// Flushing and throttling
|
|
105
|
+
export fn flush(handle: RegionHandle) void; // Force immediate render of pending updates
|
|
106
|
+
export fn set_throttle_fps(handle: RegionHandle, fps: u32) void;
|
|
107
|
+
|
|
108
|
+
// ANSI utilities (internal, but exposed for testing/debugging)
|
|
109
|
+
export fn ansi_move_cursor(x: u32, y: u32) [*]const u8;
|
|
110
|
+
export fn ansi_clear_line() [*]const u8;
|
|
111
|
+
export fn ansi_hide_cursor() [*]const u8;
|
|
112
|
+
export fn ansi_show_cursor() [*]const u8;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Node.js Layer (`src/ts/`)
|
|
116
|
+
|
|
117
|
+
**Core Responsibilities:**
|
|
118
|
+
1. **User-Friendly API**
|
|
119
|
+
- TypeScript interfaces for all operations
|
|
120
|
+
- High-level abstractions (lines, colors, components)
|
|
121
|
+
- Event-driven updates
|
|
122
|
+
- Promise-based async operations
|
|
123
|
+
|
|
124
|
+
2. **UI Components**
|
|
125
|
+
- Progress bars
|
|
126
|
+
- Spinners
|
|
127
|
+
- Text lines with styling
|
|
128
|
+
- Multi-line layouts
|
|
129
|
+
- Status indicators
|
|
130
|
+
|
|
131
|
+
3. **State Management**
|
|
132
|
+
- Track component state
|
|
133
|
+
- Handle user updates
|
|
134
|
+
- Queue render operations
|
|
135
|
+
- Manage component lifecycle
|
|
136
|
+
|
|
137
|
+
4. **Zig Integration**
|
|
138
|
+
- Load Zig native addon
|
|
139
|
+
- Bridge TypeScript API to Zig C ABI
|
|
140
|
+
- Handle memory management
|
|
141
|
+
- Error handling and validation
|
|
142
|
+
|
|
143
|
+
**TypeScript API Design:**
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Core renderer
|
|
147
|
+
interface TerminalRegion {
|
|
148
|
+
width: number;
|
|
149
|
+
height: number; // Current height (may expand dynamically)
|
|
150
|
+
|
|
151
|
+
// Set individual line (1-based: line 1 is the first line)
|
|
152
|
+
// If line index > current height, region automatically expands
|
|
153
|
+
setLine(lineNumber: number, content: string | LineContent): void;
|
|
154
|
+
|
|
155
|
+
// Set entire contents (with line breaks) - replaces all lines
|
|
156
|
+
set(content: string | LineContent[]): void;
|
|
157
|
+
|
|
158
|
+
// Clear operations
|
|
159
|
+
clearLine(index: number): void;
|
|
160
|
+
clear(): void;
|
|
161
|
+
|
|
162
|
+
// Flushing (optional - Zig auto-flushes based on throttle)
|
|
163
|
+
flush(): void; // Force immediate render of pending updates
|
|
164
|
+
|
|
165
|
+
// Configuration
|
|
166
|
+
setThrottle(fps: number): void; // Set max render rate (default: 60)
|
|
167
|
+
|
|
168
|
+
// Cleanup
|
|
169
|
+
destroy(): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Region creation options
|
|
173
|
+
interface RegionOptions {
|
|
174
|
+
x?: number; // Default: 0
|
|
175
|
+
y?: number; // Default: 0
|
|
176
|
+
width?: number; // Default: terminal width
|
|
177
|
+
height?: number; // Default: 1 (expands as needed)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Line content with styling
|
|
181
|
+
interface LineContent {
|
|
182
|
+
text: string;
|
|
183
|
+
style?: TextStyle;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface TextStyle {
|
|
187
|
+
color?: Color;
|
|
188
|
+
backgroundColor?: Color;
|
|
189
|
+
bold?: boolean;
|
|
190
|
+
italic?: boolean;
|
|
191
|
+
underline?: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// High-level components
|
|
195
|
+
interface ProgressBar {
|
|
196
|
+
update(percentage: number): void;
|
|
197
|
+
setLabel(label: string): void;
|
|
198
|
+
finish(): void;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface Spinner {
|
|
202
|
+
start(): void;
|
|
203
|
+
stop(): void;
|
|
204
|
+
setText(text: string): void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Factory functions
|
|
208
|
+
function createRegion(options: RegionOptions): TerminalRegion;
|
|
209
|
+
function createProgressBar(region: TerminalRegion, options: ProgressBarOptions): ProgressBar;
|
|
210
|
+
function createSpinner(region: TerminalRegion, options: SpinnerOptions): Spinner;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Project Structure
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
echokit/ # Package name: "echokit" (lowercase)
|
|
217
|
+
├── package.json # PNPM package config, MIT license
|
|
218
|
+
├── pnpm-lock.yaml
|
|
219
|
+
├── tsconfig.json # TypeScript config
|
|
220
|
+
├── build.zig # Zig build script
|
|
221
|
+
├── build/ # Build output
|
|
222
|
+
│ ├── echokit.node # Compiled native addon
|
|
223
|
+
│ └── lib/ # Compiled TypeScript
|
|
224
|
+
├── src/
|
|
225
|
+
│ ├── zig/ # Zig source code
|
|
226
|
+
│ │ ├── renderer.zig # Core rendering engine
|
|
227
|
+
│ │ ├── diff.zig # Diffing algorithms
|
|
228
|
+
│ │ ├── ansi.zig # ANSI code generation
|
|
229
|
+
│ │ ├── buffer.zig # Buffering logic
|
|
230
|
+
│ │ ├── region.zig # Region management
|
|
231
|
+
│ │ ├── progress.zig # Progress tracking (atomic)
|
|
232
|
+
│ │ └── throttle.zig # Redraw throttling
|
|
233
|
+
│ └── ts/ # TypeScript source
|
|
234
|
+
│ ├── index.ts # Main entry point
|
|
235
|
+
│ ├── region.ts # TerminalRegion implementation
|
|
236
|
+
│ ├── native.ts # Zig addon bindings
|
|
237
|
+
│ ├── components/
|
|
238
|
+
│ │ ├── progress-bar.ts
|
|
239
|
+
│ │ ├── spinner.ts
|
|
240
|
+
│ │ └── text-line.ts
|
|
241
|
+
│ ├── utils/
|
|
242
|
+
│ │ ├── colors.ts
|
|
243
|
+
│ │ └── ansi.ts
|
|
244
|
+
│ └── types.ts # TypeScript type definitions
|
|
245
|
+
├── examples/
|
|
246
|
+
│ ├── basic-progress.ts
|
|
247
|
+
│ ├── multi-lane.ts
|
|
248
|
+
│ ├── spinner.ts
|
|
249
|
+
│ └── custom-layout.ts
|
|
250
|
+
├── tests/
|
|
251
|
+
│ ├── zig/ # Zig unit tests
|
|
252
|
+
│ └── ts/ # TypeScript tests
|
|
253
|
+
└── README.md
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Implementation Details
|
|
257
|
+
|
|
258
|
+
### Zig Implementation
|
|
259
|
+
|
|
260
|
+
#### 1. Region Management (`region.zig`)
|
|
261
|
+
|
|
262
|
+
```zig
|
|
263
|
+
const Region = struct {
|
|
264
|
+
x: u32,
|
|
265
|
+
y: u32,
|
|
266
|
+
width: u32,
|
|
267
|
+
height: u32,
|
|
268
|
+
pending_frame: [][]u8, // Current updates (being built)
|
|
269
|
+
previous_frame: [][]u8, // Last rendered frame (for diffing)
|
|
270
|
+
render_scheduled: bool, // Is a render scheduled?
|
|
271
|
+
last_render_time: i64, // Last render timestamp (for throttling)
|
|
272
|
+
throttle: Throttle, // Throttling state
|
|
273
|
+
buffer: RenderBuffer, // ANSI output buffer
|
|
274
|
+
|
|
275
|
+
pub fn init(allocator: Allocator, x: u32, y: u32, width: u32, height: u32) !Region {
|
|
276
|
+
// Allocate frame buffers
|
|
277
|
+
// Initialize throttle (default 60 FPS)
|
|
278
|
+
// Initialize render buffer
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pub fn deinit(self: *Region) void {
|
|
282
|
+
// Free frame buffers
|
|
283
|
+
// Flush any pending renders
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub fn set_line(self: *Region, line_number: u32, content: []const u8) !void {
|
|
287
|
+
// Convert 1-based to 0-based
|
|
288
|
+
if (line_number == 0) {
|
|
289
|
+
return error.InvalidLineNumber; // Line numbers start at 1
|
|
290
|
+
}
|
|
291
|
+
const line_index = line_number - 1;
|
|
292
|
+
|
|
293
|
+
// If line_index >= height, expand region
|
|
294
|
+
if (line_index >= self.height) {
|
|
295
|
+
try self.expand_to(line_index + 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Ensure pending_frame has enough lines
|
|
299
|
+
while (self.pending_frame.items.len <= line_index) {
|
|
300
|
+
try self.pending_frame.append(self.allocator, &[_]u8{});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Update pending_frame[line_index]
|
|
304
|
+
self.pending_frame.items[line_index] = try self.allocator.dupe(u8, content);
|
|
305
|
+
|
|
306
|
+
// Schedule render (respects throttle)
|
|
307
|
+
self.schedule_render();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
pub fn set(self: *Region, content: []const u8) !void {
|
|
311
|
+
// Split by \n to get lines
|
|
312
|
+
var lines = std.ArrayList([]const u8).init(self.allocator);
|
|
313
|
+
defer lines.deinit();
|
|
314
|
+
|
|
315
|
+
var it = std.mem.splitScalar(u8, content, '\n');
|
|
316
|
+
while (it.next()) |line| {
|
|
317
|
+
try lines.append(line);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Expand region if needed
|
|
321
|
+
if (lines.items.len > self.height) {
|
|
322
|
+
try self.expand_to(@intCast(lines.items.len));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Update all lines in pending_frame
|
|
326
|
+
try self.pending_frame.resize(self.allocator, lines.items.len);
|
|
327
|
+
for (lines.items, 0..) |line, i| {
|
|
328
|
+
self.pending_frame.items[i] = try self.allocator.dupe(u8, line);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Schedule render
|
|
332
|
+
self.schedule_render();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn expand_to(self: *Region, new_height: u32) !void {
|
|
336
|
+
// Expand previous_frame and pending_frame
|
|
337
|
+
// Update height
|
|
338
|
+
self.height = new_height;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
pub fn schedule_render(self: *Region) void {
|
|
342
|
+
// Check throttle - should we render now?
|
|
343
|
+
// If yes: call render_now()
|
|
344
|
+
// If no: mark render_scheduled = true, will render on next throttle tick
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
pub fn render_now(self: *Region) !void {
|
|
348
|
+
// Diff pending_frame vs previous_frame
|
|
349
|
+
// Generate ANSI operations
|
|
350
|
+
// Write to buffer
|
|
351
|
+
// Flush buffer
|
|
352
|
+
// Copy pending_frame to previous_frame
|
|
353
|
+
// Clear pending_frame
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
pub fn resize(self: *Region, new_width: u32, new_height: u32) !void {
|
|
357
|
+
// Reallocate frames
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### 2. Diffing (`diff.zig`)
|
|
363
|
+
|
|
364
|
+
```zig
|
|
365
|
+
const DiffOp = union(enum) {
|
|
366
|
+
no_change: void,
|
|
367
|
+
update_line: struct { line: u32, content: []const u8 },
|
|
368
|
+
insert_line: struct { line: u32, content: []const u8 },
|
|
369
|
+
delete_line: u32,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
pub fn diff_frames(
|
|
373
|
+
prev: [][]const u8,
|
|
374
|
+
curr: [][]const u8,
|
|
375
|
+
allocator: Allocator
|
|
376
|
+
) ![]DiffOp {
|
|
377
|
+
// Compare line by line
|
|
378
|
+
// Generate minimal diff operations
|
|
379
|
+
// Return array of operations
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### 3. ANSI Operations (`ansi.zig`)
|
|
384
|
+
|
|
385
|
+
```zig
|
|
386
|
+
pub fn move_cursor_to(x: u32, y: u32) []const u8 {
|
|
387
|
+
// Generate: \x1b[{y};{x}H
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pub fn move_cursor_up(n: u32) []const u8 {
|
|
391
|
+
// Generate: \x1b[{n}A
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
pub fn clear_line() []const u8 {
|
|
395
|
+
// Generate: \x1b[2K
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pub fn hide_cursor() []const u8 {
|
|
399
|
+
// Generate: \x1b[?25l
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pub fn show_cursor() []const u8 {
|
|
403
|
+
// Generate: \x1b[?25h
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### 4. Buffering (`buffer.zig`)
|
|
408
|
+
|
|
409
|
+
```zig
|
|
410
|
+
const RenderBuffer = struct {
|
|
411
|
+
data: std.ArrayList(u8),
|
|
412
|
+
stdout: std.fs.File,
|
|
413
|
+
|
|
414
|
+
pub fn init(allocator: Allocator, stdout: std.fs.File) RenderBuffer {
|
|
415
|
+
return .{
|
|
416
|
+
.data = std.ArrayList(u8).init(allocator),
|
|
417
|
+
.stdout = stdout,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
pub fn write(self: *RenderBuffer, bytes: []const u8) !void {
|
|
422
|
+
try self.data.appendSlice(bytes);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pub fn flush(self: *RenderBuffer) !void {
|
|
426
|
+
try self.stdout.writeAll(self.data.items);
|
|
427
|
+
self.data.clearRetainingCapacity();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### 5. Throttling (`throttle.zig`)
|
|
433
|
+
|
|
434
|
+
```zig
|
|
435
|
+
const Throttle = struct {
|
|
436
|
+
last_frame_time: i64,
|
|
437
|
+
min_frame_interval: i64, // nanoseconds (calculated from FPS)
|
|
438
|
+
fps: u32,
|
|
439
|
+
|
|
440
|
+
pub fn init(fps: u32) Throttle {
|
|
441
|
+
const interval_ns = 1_000_000_000 / @as(i64, fps);
|
|
442
|
+
return .{
|
|
443
|
+
.last_frame_time = 0,
|
|
444
|
+
.min_frame_interval = interval_ns,
|
|
445
|
+
.fps = fps,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
pub fn set_fps(self: *Throttle, fps: u32) void {
|
|
450
|
+
self.fps = fps;
|
|
451
|
+
self.min_frame_interval = 1_000_000_000 / @as(i64, fps);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
pub fn should_render(self: *Throttle) bool {
|
|
455
|
+
const now = std.time.nanoTimestamp();
|
|
456
|
+
if (now - self.last_frame_time >= self.min_frame_interval) {
|
|
457
|
+
self.last_frame_time = now;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
pub fn time_until_next_frame(self: *Throttle) i64 {
|
|
464
|
+
const now = std.time.nanoTimestamp();
|
|
465
|
+
const elapsed = now - self.last_frame_time;
|
|
466
|
+
const remaining = self.min_frame_interval - elapsed;
|
|
467
|
+
return if (remaining > 0) remaining else 0;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### 6. Progress Tracking (Internal - for lock-free updates)
|
|
473
|
+
|
|
474
|
+
Progress tracking uses atomic operations internally for lock-free updates when multiple threads/async operations update progress concurrently. This is an implementation detail - the user-facing API is `createProgressBar()` which handles everything.
|
|
475
|
+
|
|
476
|
+
```zig
|
|
477
|
+
// Internal: Atomic progress values (used by ProgressBar component)
|
|
478
|
+
const ProgressState = struct {
|
|
479
|
+
current: std.atomic.Value(u64),
|
|
480
|
+
total: std.atomic.Value(u64),
|
|
481
|
+
|
|
482
|
+
pub fn update(self: *ProgressState, value: u64, max: u64) void {
|
|
483
|
+
_ = self.current.store(value, .Release);
|
|
484
|
+
_ = self.total.store(max, .Release);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
pub fn get(self: *ProgressState) struct { current: u64, total: u64 } {
|
|
488
|
+
return .{
|
|
489
|
+
.current = self.current.load(.Acquire),
|
|
490
|
+
.total = self.total.load(.Acquire),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Node.js Implementation
|
|
497
|
+
|
|
498
|
+
#### 1. Native Addon Binding (`native.ts`)
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
import { createRequire } from 'module';
|
|
502
|
+
import { fileURLToPath } from 'url';
|
|
503
|
+
import { dirname, join } from 'path';
|
|
504
|
+
|
|
505
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
506
|
+
const __dirname = dirname(__filename);
|
|
507
|
+
|
|
508
|
+
// Load the compiled Zig addon
|
|
509
|
+
const addon = require(join(__dirname, '../build/echokit.node'));
|
|
510
|
+
|
|
511
|
+
export interface NativeRegion {
|
|
512
|
+
createRegion(x: number, y: number, width: number, height: number): number;
|
|
513
|
+
destroyRegion(handle: number): void;
|
|
514
|
+
setLine(handle: number, lineNumber: number, content: string): void; // 1-based
|
|
515
|
+
set(handle: number, content: string): void;
|
|
516
|
+
clearLine(handle: number, lineNumber: number): void; // 1-based
|
|
517
|
+
clearRegion(handle: number): void;
|
|
518
|
+
flush(handle: number): void;
|
|
519
|
+
setThrottleFps(handle: number, fps: number): void;
|
|
520
|
+
// ... other methods
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export const native: NativeRegion = addon;
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
#### 2. TerminalRegion (`region.ts`)
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { native } from './native.js';
|
|
530
|
+
|
|
531
|
+
export class TerminalRegion {
|
|
532
|
+
private handle: number;
|
|
533
|
+
private width: number;
|
|
534
|
+
private _height: number; // Current height (may expand)
|
|
535
|
+
|
|
536
|
+
constructor(options: RegionOptions = {}) {
|
|
537
|
+
const x = options.x ?? 0;
|
|
538
|
+
const y = options.y ?? 0;
|
|
539
|
+
this.width = options.width ?? process.stdout.columns ?? 80;
|
|
540
|
+
this._height = options.height ?? 1; // Default to 1 line, expands as needed
|
|
541
|
+
this.handle = native.createRegion(x, y, this.width, this._height);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
get width(): number {
|
|
545
|
+
return this.width;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
get height(): number {
|
|
549
|
+
return this._height;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
setLine(lineNumber: number, content: string | LineContent): void {
|
|
553
|
+
if (lineNumber < 1) {
|
|
554
|
+
throw new Error('Line numbers start at 1');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Zig handles batching and expansion automatically
|
|
558
|
+
const text = typeof content === 'string' ? content : content.text;
|
|
559
|
+
// Apply styling if provided
|
|
560
|
+
const styled = this.applyStyle(text, typeof content === 'object' ? content.style : undefined);
|
|
561
|
+
native.setLine(this.handle, lineNumber, styled);
|
|
562
|
+
// Zig will:
|
|
563
|
+
// - Convert to 0-based internally
|
|
564
|
+
// - Expand region if lineNumber > current height
|
|
565
|
+
// - Buffer this update in pending_frame
|
|
566
|
+
// - Check throttle
|
|
567
|
+
// - Schedule render if needed (or render immediately if throttle allows)
|
|
568
|
+
|
|
569
|
+
// Update our height tracking if Zig expanded
|
|
570
|
+
if (lineNumber > this._height) {
|
|
571
|
+
this._height = lineNumber;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
set(content: string | LineContent[]): void {
|
|
576
|
+
if (typeof content === 'string') {
|
|
577
|
+
// Single string with \n line breaks
|
|
578
|
+
native.set(this.handle, content);
|
|
579
|
+
// Update height based on line count
|
|
580
|
+
this._height = content.split('\n').length;
|
|
581
|
+
} else {
|
|
582
|
+
// Array of LineContent
|
|
583
|
+
const lines = content.map(c =>
|
|
584
|
+
this.applyStyle(c.text, c.style)
|
|
585
|
+
).join('\n');
|
|
586
|
+
native.set(this.handle, lines);
|
|
587
|
+
this._height = content.length;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
clearLine(lineNumber: number): void {
|
|
592
|
+
if (lineNumber < 1) {
|
|
593
|
+
throw new Error('Line numbers start at 1');
|
|
594
|
+
}
|
|
595
|
+
native.clearLine(this.handle, lineNumber);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
clear(): void {
|
|
599
|
+
native.clearRegion(this.handle);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
flush(): void {
|
|
603
|
+
// Force immediate render of any pending updates (bypasses throttle)
|
|
604
|
+
native.flush(this.handle);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
setThrottle(fps: number): void {
|
|
608
|
+
native.setThrottleFps(this.handle, fps);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
destroy(): void {
|
|
612
|
+
native.destroyRegion(this.handle);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private applyStyle(text: string, style?: TextStyle): string {
|
|
616
|
+
// Convert TextStyle to ANSI codes
|
|
617
|
+
// Return styled string
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
#### 3. Progress Bar Component (`components/progress-bar.ts`)
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
import { TerminalRegion } from '../region.js';
|
|
626
|
+
|
|
627
|
+
export interface ProgressBarOptions {
|
|
628
|
+
label?: string;
|
|
629
|
+
width?: number;
|
|
630
|
+
style?: {
|
|
631
|
+
complete?: string;
|
|
632
|
+
incomplete?: string;
|
|
633
|
+
brackets?: [string, string];
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export class ProgressBar {
|
|
638
|
+
private region: TerminalRegion;
|
|
639
|
+
private lineNumber: number; // 1-based
|
|
640
|
+
private current: number = 0;
|
|
641
|
+
private total: number = 100;
|
|
642
|
+
private label: string;
|
|
643
|
+
private width: number;
|
|
644
|
+
|
|
645
|
+
constructor(region: TerminalRegion, lineNumber: number, options: ProgressBarOptions = {}) {
|
|
646
|
+
this.region = region;
|
|
647
|
+
this.lineNumber = lineNumber;
|
|
648
|
+
this.label = options.label || '';
|
|
649
|
+
this.width = options.width || 40;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
update(current: number, total: number): void {
|
|
653
|
+
this.current = current;
|
|
654
|
+
this.total = total;
|
|
655
|
+
this.render();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
setLabel(label: string): void {
|
|
659
|
+
this.label = label;
|
|
660
|
+
this.render();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private render(): void {
|
|
664
|
+
const percentage = Math.min(100, Math.max(0, (this.current / this.total) * 100));
|
|
665
|
+
const filled = Math.floor((percentage / 100) * this.width);
|
|
666
|
+
const empty = this.width - filled;
|
|
667
|
+
|
|
668
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
669
|
+
const text = `${this.label} [${bar}] ${percentage.toFixed(1)}%`;
|
|
670
|
+
|
|
671
|
+
// Just update the line - Zig handles batching and rendering
|
|
672
|
+
this.region.setLine(this.lineNumber, text);
|
|
673
|
+
// Optional: call flush() if you need immediate rendering
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
finish(): void {
|
|
677
|
+
this.update(this.total, this.total);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### 4. Spinner Component (`components/spinner.ts`)
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import { TerminalRegion } from '../region.js';
|
|
686
|
+
|
|
687
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
688
|
+
|
|
689
|
+
export class Spinner {
|
|
690
|
+
private region: TerminalRegion;
|
|
691
|
+
private lineNumber: number; // 1-based
|
|
692
|
+
private frameIndex: number = 0;
|
|
693
|
+
private text: string = '';
|
|
694
|
+
private interval?: NodeJS.Timeout;
|
|
695
|
+
private isRunning: boolean = false;
|
|
696
|
+
|
|
697
|
+
constructor(region: TerminalRegion, lineNumber: number) {
|
|
698
|
+
this.region = region;
|
|
699
|
+
this.lineNumber = lineNumber;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
start(): void {
|
|
703
|
+
if (this.isRunning) return;
|
|
704
|
+
this.isRunning = true;
|
|
705
|
+
this.interval = setInterval(() => {
|
|
706
|
+
this.render();
|
|
707
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
708
|
+
}, 100);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
stop(): void {
|
|
712
|
+
if (!this.isRunning) return;
|
|
713
|
+
this.isRunning = false;
|
|
714
|
+
if (this.interval) {
|
|
715
|
+
clearInterval(this.interval);
|
|
716
|
+
}
|
|
717
|
+
// Clear the spinner line
|
|
718
|
+
this.region.setLine(this.lineNumber, '');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
setText(text: string): void {
|
|
722
|
+
this.text = text;
|
|
723
|
+
this.render();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private render(): void {
|
|
727
|
+
const frame = SPINNER_FRAMES[this.frameIndex];
|
|
728
|
+
const line = `${frame} ${this.text}`;
|
|
729
|
+
|
|
730
|
+
// Just update the line - Zig handles batching and rendering
|
|
731
|
+
this.region.setLine(this.lineNumber, line);
|
|
732
|
+
// Spinner updates frequently, so Zig's throttling will handle smooth animation
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## Build System
|
|
738
|
+
|
|
739
|
+
### Zig Build (`build.zig`)
|
|
740
|
+
|
|
741
|
+
```zig
|
|
742
|
+
const std = @import("std");
|
|
743
|
+
|
|
744
|
+
pub fn build(b: *std.Build) void {
|
|
745
|
+
const target = b.standardTargetOptions(.{});
|
|
746
|
+
const optimize = b.standardOptimizeOption(.{});
|
|
747
|
+
|
|
748
|
+
const lib = b.addSharedLibrary(.{
|
|
749
|
+
.name = "echokit",
|
|
750
|
+
.root_source_file = b.path("src/zig/renderer.zig"),
|
|
751
|
+
.target = target,
|
|
752
|
+
.optimize = optimize,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Link with Node.js N-API
|
|
756
|
+
lib.linkLibC();
|
|
757
|
+
// Add N-API headers path
|
|
758
|
+
|
|
759
|
+
b.installArtifact(lib);
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Package.json Scripts
|
|
764
|
+
|
|
765
|
+
```json
|
|
766
|
+
{
|
|
767
|
+
"scripts": {
|
|
768
|
+
"build:zig": "zig build",
|
|
769
|
+
"build:ts": "tsc",
|
|
770
|
+
"build": "pnpm build:zig && pnpm build:ts",
|
|
771
|
+
"dev": "pnpm build:zig && pnpm build:ts --watch",
|
|
772
|
+
"test": "vitest",
|
|
773
|
+
"example:progress": "tsx examples/basic-progress.ts"
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## Features Checklist
|
|
779
|
+
|
|
780
|
+
### Core Features (Zig)
|
|
781
|
+
- [x] Region management (rectangular terminal area)
|
|
782
|
+
- [x] Double-buffer diffing (line-level)
|
|
783
|
+
- [x] ANSI cursor movement
|
|
784
|
+
- [x] Buffering and flushing
|
|
785
|
+
- [x] Redraw throttling
|
|
786
|
+
- [x] Lock-free progress updates (atomic operations)
|
|
787
|
+
- [x] Parallel progress lanes support
|
|
788
|
+
|
|
789
|
+
### API Features (Node.js)
|
|
790
|
+
- [x] TypeScript API
|
|
791
|
+
- [x] Automatic batching (Zig handles it)
|
|
792
|
+
- [x] Simple setLine() API (no manual frame management)
|
|
793
|
+
- [x] Optional flush() for immediate rendering
|
|
794
|
+
- [x] Color/styling support
|
|
795
|
+
- [x] Progress bar component
|
|
796
|
+
- [x] Spinner component
|
|
797
|
+
- [x] Text line component
|
|
798
|
+
- [x] Multi-line layout support
|
|
799
|
+
|
|
800
|
+
### Performance Features
|
|
801
|
+
- [x] Zero-allocation hot paths (where possible)
|
|
802
|
+
- [x] Minimal system calls
|
|
803
|
+
- [x] Efficient string handling
|
|
804
|
+
- [x] Fast ANSI code generation
|
|
805
|
+
- [x] Batch operations
|
|
806
|
+
|
|
807
|
+
## Example Usage
|
|
808
|
+
|
|
809
|
+
### Basic Progress Bar
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
import { createRegion, createProgressBar } from 'echokit'; // Package: "echokit"
|
|
813
|
+
|
|
814
|
+
// Region defaults to 1 line, expands as needed
|
|
815
|
+
const region = createRegion({ width: 80 });
|
|
816
|
+
const progress = createProgressBar(region, 1, { // Line 1 (first line)
|
|
817
|
+
label: 'Installing packages',
|
|
818
|
+
width: 50,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
for (let i = 0; i <= 100; i++) {
|
|
822
|
+
progress.update(i, 100);
|
|
823
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
progress.finish();
|
|
827
|
+
region.destroy();
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Multi-Lane Progress
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
import { createRegion, createProgressBar } from 'echokit'; // Package: "echokit"
|
|
834
|
+
|
|
835
|
+
// Region starts at 1 line, expands to 3 as we add progress bars
|
|
836
|
+
const region = createRegion({ width: 80 });
|
|
837
|
+
|
|
838
|
+
const download = createProgressBar(region, 1, { label: 'Downloading' }); // Line 1
|
|
839
|
+
const extract = createProgressBar(region, 2, { label: 'Extracting' }); // Line 2 (expands region)
|
|
840
|
+
const install = createProgressBar(region, 3, { label: 'Installing' }); // Line 3 (expands region)
|
|
841
|
+
|
|
842
|
+
// Update lanes concurrently
|
|
843
|
+
Promise.all([
|
|
844
|
+
updateProgress(download, 100),
|
|
845
|
+
updateProgress(extract, 100),
|
|
846
|
+
updateProgress(install, 100),
|
|
847
|
+
]);
|
|
848
|
+
|
|
849
|
+
region.destroy();
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Spinner with Text
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
import { createRegion, createSpinner } from 'echokit'; // Package: "echokit"
|
|
856
|
+
|
|
857
|
+
// Region defaults to 1 line
|
|
858
|
+
const region = createRegion({ width: 80 });
|
|
859
|
+
const spinner = createSpinner(region, 1); // Line 1
|
|
860
|
+
|
|
861
|
+
spinner.setText('Processing...');
|
|
862
|
+
spinner.start();
|
|
863
|
+
|
|
864
|
+
// Do work...
|
|
865
|
+
await doWork();
|
|
866
|
+
|
|
867
|
+
spinner.stop();
|
|
868
|
+
region.destroy();
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### Custom Layout
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
import { createRegion } from 'echokit'; // Package: "echokit"
|
|
875
|
+
|
|
876
|
+
// Region starts at 1 line, expands as we add lines
|
|
877
|
+
const region = createRegion({ width: 80 });
|
|
878
|
+
|
|
879
|
+
// Just update lines - Zig automatically batches, expands, and renders efficiently
|
|
880
|
+
region.setLine(1, { text: 'Status:', style: { bold: true } }); // Line 1
|
|
881
|
+
region.setLine(2, ' ✓ Connected'); // Line 2 (expands to 2 lines)
|
|
882
|
+
region.setLine(3, ' ⏳ Processing...'); // Line 3 (expands to 3 lines)
|
|
883
|
+
region.setLine(4, { text: ' ✗ Error', style: { color: 'red' } }); // Line 4 (expands to 4 lines)
|
|
884
|
+
|
|
885
|
+
// Or set entire content at once:
|
|
886
|
+
region.set(`Status:
|
|
887
|
+
✓ Connected
|
|
888
|
+
⏳ Processing...
|
|
889
|
+
✗ Error`);
|
|
890
|
+
|
|
891
|
+
// Optional: force immediate render (otherwise Zig will render based on throttle)
|
|
892
|
+
// region.flush();
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
## Testing Strategy
|
|
896
|
+
|
|
897
|
+
### Zig Tests
|
|
898
|
+
- Unit tests for diffing algorithm
|
|
899
|
+
- Unit tests for ANSI code generation
|
|
900
|
+
- Integration tests for region management
|
|
901
|
+
- Performance benchmarks
|
|
902
|
+
|
|
903
|
+
### TypeScript Tests
|
|
904
|
+
- Unit tests for components
|
|
905
|
+
- Integration tests for region API
|
|
906
|
+
- E2E tests with actual terminal output
|
|
907
|
+
|
|
908
|
+
## Performance Targets
|
|
909
|
+
|
|
910
|
+
- **Frame rate**: 60+ FPS for smooth animations
|
|
911
|
+
- **Latency**: < 16ms per frame
|
|
912
|
+
- **Memory**: Minimal allocations in hot paths
|
|
913
|
+
- **CPU**: Efficient diffing (O(n) where n = number of lines)
|
|
914
|
+
|
|
915
|
+
## Future Enhancements
|
|
916
|
+
|
|
917
|
+
1. **More Components**
|
|
918
|
+
- Tables
|
|
919
|
+
- Trees
|
|
920
|
+
- Forms
|
|
921
|
+
- Charts (ASCII)
|
|
922
|
+
|
|
923
|
+
2. **Advanced Features**
|
|
924
|
+
- Mouse support
|
|
925
|
+
- Keyboard input handling
|
|
926
|
+
- Window resizing detection
|
|
927
|
+
- Terminal detection (capabilities)
|
|
928
|
+
|
|
929
|
+
3. **Optimizations**
|
|
930
|
+
- SIMD for diffing (if beneficial)
|
|
931
|
+
- More aggressive buffering
|
|
932
|
+
- Adaptive throttling
|
|
933
|
+
|
|
934
|
+
4. **Developer Experience**
|
|
935
|
+
- Better error messages
|
|
936
|
+
- Debug mode (show diff operations)
|
|
937
|
+
- Performance profiling tools
|
|
938
|
+
|
|
939
|
+
## License
|
|
940
|
+
|
|
941
|
+
MIT License - see LICENSE file
|
|
942
|
+
|
|
943
|
+
## Dependencies
|
|
944
|
+
|
|
945
|
+
### Zig
|
|
946
|
+
- Standard library only (no external deps)
|
|
947
|
+
|
|
948
|
+
### Node.js
|
|
949
|
+
- TypeScript (dev)
|
|
950
|
+
- @types/node (dev)
|
|
951
|
+
- Build tools (dev)
|
|
952
|
+
|