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.
Files changed (61) hide show
  1. package/.cursor/plan.md +952 -0
  2. package/LICENSE +22 -0
  3. package/README.md +111 -0
  4. package/TESTING.md +102 -0
  5. package/build.zig +100 -0
  6. package/examples/basic-progress.ts +21 -0
  7. package/examples/multi-lane.ts +29 -0
  8. package/examples/spinner.ts +20 -0
  9. package/examples/test-basic.ts +23 -0
  10. package/lib/components/progress-bar.d.ts +19 -0
  11. package/lib/components/progress-bar.d.ts.map +1 -0
  12. package/lib/components/progress-bar.js +43 -0
  13. package/lib/components/progress-bar.js.map +1 -0
  14. package/lib/components/spinner.d.ts +18 -0
  15. package/lib/components/spinner.d.ts.map +1 -0
  16. package/lib/components/spinner.js +48 -0
  17. package/lib/components/spinner.js.map +1 -0
  18. package/lib/index.d.ts +12 -0
  19. package/lib/index.d.ts.map +1 -0
  20. package/lib/index.js +16 -0
  21. package/lib/index.js.map +1 -0
  22. package/lib/native.d.ts +12 -0
  23. package/lib/native.d.ts.map +1 -0
  24. package/lib/native.js +65 -0
  25. package/lib/native.js.map +1 -0
  26. package/lib/region.d.ts +17 -0
  27. package/lib/region.d.ts.map +1 -0
  28. package/lib/region.js +74 -0
  29. package/lib/region.js.map +1 -0
  30. package/lib/types.d.ts +32 -0
  31. package/lib/types.d.ts.map +1 -0
  32. package/lib/types.js +2 -0
  33. package/lib/types.js.map +1 -0
  34. package/lib/utils/colors.d.ts +3 -0
  35. package/lib/utils/colors.d.ts.map +1 -0
  36. package/lib/utils/colors.js +61 -0
  37. package/lib/utils/colors.js.map +1 -0
  38. package/package.json +46 -0
  39. package/src/ts/components/progress-bar.ts +53 -0
  40. package/src/ts/components/spinner.ts +56 -0
  41. package/src/ts/index.ts +37 -0
  42. package/src/ts/native.ts +86 -0
  43. package/src/ts/region.ts +89 -0
  44. package/src/ts/types/ffi-napi.d.ts +11 -0
  45. package/src/ts/types/ref-napi.d.ts +5 -0
  46. package/src/ts/types.ts +53 -0
  47. package/src/ts/utils/colors.ts +72 -0
  48. package/src/zig/ansi.zig +21 -0
  49. package/src/zig/buffer.zig +37 -0
  50. package/src/zig/diff.zig +43 -0
  51. package/src/zig/region.zig +292 -0
  52. package/src/zig/renderer.zig +92 -0
  53. package/src/zig/test_ansi.zig +66 -0
  54. package/src/zig/test_buffer.zig +82 -0
  55. package/src/zig/test_diff.zig +220 -0
  56. package/src/zig/test_integration.zig +76 -0
  57. package/src/zig/test_region.zig +191 -0
  58. package/src/zig/test_runner.zig +27 -0
  59. package/src/zig/test_throttle.zig +59 -0
  60. package/src/zig/throttle.zig +38 -0
  61. package/tsconfig.json +21 -0
@@ -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
+