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
package/lib/native.js ADDED
@@ -0,0 +1,65 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { Library } from 'ffi-napi';
4
+ import ref from 'ref-napi';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ // Determine the library path based on platform
8
+ function getLibraryPath() {
9
+ const platform = process.platform;
10
+ if (platform === 'darwin') {
11
+ return join(__dirname, '../../zig-out/lib/libechokit.dylib');
12
+ }
13
+ else if (platform === 'win32') {
14
+ return join(__dirname, '../../zig-out/lib/echokit.dll');
15
+ }
16
+ else {
17
+ return join(__dirname, '../../zig-out/lib/libechokit.so');
18
+ }
19
+ }
20
+ // Load the Zig library using FFI
21
+ const lib = Library(getLibraryPath(), {
22
+ create_region: ['uint64', ['uint32', 'uint32', 'uint32', 'uint32']],
23
+ destroy_region: ['void', ['uint64']],
24
+ set_line: ['void', ['uint64', 'uint32', 'pointer', 'size_t']],
25
+ set: ['void', ['uint64', 'pointer', 'size_t']],
26
+ clear_line: ['void', ['uint64', 'uint32']],
27
+ clear_region: ['void', ['uint64']],
28
+ flush: ['void', ['uint64']],
29
+ set_throttle_fps: ['void', ['uint64', 'uint32']],
30
+ });
31
+ export const native = {
32
+ createRegion: (x, y, width, height) => {
33
+ return Number(lib.create_region(x, y, width, height));
34
+ },
35
+ destroyRegion: (handle) => {
36
+ lib.destroy_region(BigInt(handle));
37
+ },
38
+ setLine: (handle, lineNumber, content) => {
39
+ const buf = Buffer.from(content, 'utf8');
40
+ // Allocate a buffer and copy the string data
41
+ const ptr = ref.alloc(buf.length);
42
+ buf.copy(ptr);
43
+ lib.set_line(BigInt(handle), lineNumber, ptr, buf.length);
44
+ },
45
+ set: (handle, content) => {
46
+ const buf = Buffer.from(content, 'utf8');
47
+ // Allocate a buffer and copy the string data
48
+ const ptr = ref.alloc(buf.length);
49
+ buf.copy(ptr);
50
+ lib.set(BigInt(handle), ptr, buf.length);
51
+ },
52
+ clearLine: (handle, lineNumber) => {
53
+ lib.clear_line(BigInt(handle), lineNumber);
54
+ },
55
+ clearRegion: (handle) => {
56
+ lib.clear_region(BigInt(handle));
57
+ },
58
+ flush: (handle) => {
59
+ lib.flush(BigInt(handle));
60
+ },
61
+ setThrottleFps: (handle, fps) => {
62
+ lib.set_throttle_fps(BigInt(handle), fps);
63
+ },
64
+ };
65
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/ts/native.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,+CAA+C;AAC/C,SAAS,cAAc;IACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAElC,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,EAAE,oCAAoC,CAAC,CAAC;IAC/D,CAAC;SAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC,SAAS,EAAE,+BAA+B,CAAC,CAAC;IAC1D,CAAC;SAAM,CAAC;QACN,OAAO,IAAI,CAAC,SAAS,EAAE,iCAAiC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,GAAG,GAAG,OAAO,CAAC,cAAc,EAAE,EAAE;IACpC,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACnE,cAAc,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7D,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC1C,YAAY,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;IAClC,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;IAC3B,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;CACjD,CAAC,CAAC;AAaH,MAAM,CAAC,MAAM,MAAM,GAAiB;IAClC,YAAY,EAAE,CAAC,CAAS,EAAE,CAAS,EAAE,KAAa,EAAE,MAAc,EAAU,EAAE;QAC5E,OAAO,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,aAAa,EAAE,CAAC,MAAc,EAAQ,EAAE;QACtC,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,EAAE,CAAC,MAAc,EAAE,UAAkB,EAAE,OAAe,EAAQ,EAAE;QACrE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACzC,6CAA6C;QAC7C,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IAED,GAAG,EAAE,CAAC,MAAc,EAAE,OAAe,EAAQ,EAAE;QAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACzC,6CAA6C;QAC7C,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IAED,SAAS,EAAE,CAAC,MAAc,EAAE,UAAkB,EAAQ,EAAE;QACtD,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW,EAAE,CAAC,MAAc,EAAQ,EAAE;QACpC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,EAAE,CAAC,MAAc,EAAQ,EAAE;QAC9B,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,cAAc,EAAE,CAAC,MAAc,EAAE,GAAW,EAAQ,EAAE;QACpD,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { RegionOptions, LineContent } from './types.js';
2
+ export declare class TerminalRegion {
3
+ private handle;
4
+ private _width;
5
+ private _height;
6
+ constructor(options?: RegionOptions);
7
+ get width(): number;
8
+ get height(): number;
9
+ setLine(lineNumber: number, content: string | LineContent): void;
10
+ set(content: string | LineContent[]): void;
11
+ clearLine(lineNumber: number): void;
12
+ clear(): void;
13
+ flush(): void;
14
+ setThrottle(fps: number): void;
15
+ destroy(): void;
16
+ }
17
+ //# sourceMappingURL=region.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.d.ts","sourceRoot":"","sources":["../src/ts/region.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAExE,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,GAAE,aAAkB;IAQvC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAuBhE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,IAAI;IAgB1C,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOnC,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;IAKb,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI9B,OAAO,IAAI,IAAI;CAGhB"}
package/lib/region.js ADDED
@@ -0,0 +1,74 @@
1
+ import { native } from './native.js';
2
+ import { applyStyle } from './utils/colors.js';
3
+ export class TerminalRegion {
4
+ handle;
5
+ _width;
6
+ _height; // Current height (may expand)
7
+ constructor(options = {}) {
8
+ const x = options.x ?? 0;
9
+ const y = options.y ?? 0;
10
+ this._width = options.width ?? (process.stdout.columns ?? 80);
11
+ this._height = options.height ?? 1; // Default to 1 line, expands as needed
12
+ this.handle = native.createRegion(x, y, this._width, this._height);
13
+ }
14
+ get width() {
15
+ return this._width;
16
+ }
17
+ get height() {
18
+ return this._height;
19
+ }
20
+ setLine(lineNumber, content) {
21
+ if (lineNumber < 1) {
22
+ throw new Error('Line numbers start at 1');
23
+ }
24
+ // Zig handles batching and expansion automatically
25
+ const text = typeof content === 'string' ? content : content.text;
26
+ // Apply styling if provided
27
+ const styled = applyStyle(text, typeof content === 'object' ? content.style : undefined);
28
+ native.setLine(this.handle, lineNumber, styled);
29
+ // Zig will:
30
+ // - Convert to 0-based internally
31
+ // - Expand region if lineNumber > current height
32
+ // - Buffer this update in pending_frame
33
+ // - Check throttle
34
+ // - Schedule render if needed (or render immediately if throttle allows)
35
+ // Update our height tracking if Zig expanded
36
+ if (lineNumber > this._height) {
37
+ this._height = lineNumber;
38
+ }
39
+ }
40
+ set(content) {
41
+ if (typeof content === 'string') {
42
+ // Single string with \n line breaks
43
+ native.set(this.handle, content);
44
+ // Update height based on line count
45
+ this._height = content.split('\n').length;
46
+ }
47
+ else {
48
+ // Array of LineContent
49
+ const lines = content.map(c => applyStyle(c.text, c.style)).join('\n');
50
+ native.set(this.handle, lines);
51
+ this._height = content.length;
52
+ }
53
+ }
54
+ clearLine(lineNumber) {
55
+ if (lineNumber < 1) {
56
+ throw new Error('Line numbers start at 1');
57
+ }
58
+ native.clearLine(this.handle, lineNumber);
59
+ }
60
+ clear() {
61
+ native.clearRegion(this.handle);
62
+ }
63
+ flush() {
64
+ // Force immediate render of any pending updates (bypasses throttle)
65
+ native.flush(this.handle);
66
+ }
67
+ setThrottle(fps) {
68
+ native.setThrottleFps(this.handle, fps);
69
+ }
70
+ destroy() {
71
+ native.destroyRegion(this.handle);
72
+ }
73
+ }
74
+ //# sourceMappingURL=region.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.js","sourceRoot":"","sources":["../src/ts/region.ts"],"names":[],"mappings":"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"}
package/lib/types.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ export type Color = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'brightBlack' | 'brightRed' | 'brightGreen' | 'brightYellow' | 'brightBlue' | 'brightMagenta' | 'brightCyan' | 'brightWhite';
2
+ export interface TextStyle {
3
+ color?: Color;
4
+ backgroundColor?: Color;
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ underline?: boolean;
8
+ }
9
+ export interface LineContent {
10
+ text: string;
11
+ style?: TextStyle;
12
+ }
13
+ export interface RegionOptions {
14
+ x?: number;
15
+ y?: number;
16
+ width?: number;
17
+ height?: number;
18
+ }
19
+ export interface ProgressBarOptions {
20
+ label?: string;
21
+ width?: number;
22
+ style?: {
23
+ complete?: string;
24
+ incomplete?: string;
25
+ brackets?: [string, string];
26
+ };
27
+ }
28
+ export interface SpinnerOptions {
29
+ frames?: string[];
30
+ interval?: number;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/ts/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GACb,OAAO,GACP,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,OAAO,GACP,aAAa,GACb,WAAW,GACX,aAAa,GACb,cAAc,GACd,YAAY,GACZ,eAAe,GACf,YAAY,GACZ,aAAa,CAAC;AAElB,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,eAAe,CAAC,EAAE,KAAK,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE;QACN,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/ts/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import { TextStyle } from '../types.js';
2
+ export declare function applyStyle(text: string, style?: TextStyle): string;
3
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../src/ts/utils/colors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,SAAS,EAAE,MAAM,aAAa,CAAC;AAwC/C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CA8BlE"}
@@ -0,0 +1,61 @@
1
+ const ANSI_COLORS = {
2
+ black: '30',
3
+ red: '31',
4
+ green: '32',
5
+ yellow: '33',
6
+ blue: '34',
7
+ magenta: '35',
8
+ cyan: '36',
9
+ white: '37',
10
+ brightBlack: '90',
11
+ brightRed: '91',
12
+ brightGreen: '92',
13
+ brightYellow: '93',
14
+ brightBlue: '94',
15
+ brightMagenta: '95',
16
+ brightCyan: '96',
17
+ brightWhite: '97',
18
+ };
19
+ const ANSI_BG_COLORS = {
20
+ black: '40',
21
+ red: '41',
22
+ green: '42',
23
+ yellow: '43',
24
+ blue: '44',
25
+ magenta: '45',
26
+ cyan: '46',
27
+ white: '47',
28
+ brightBlack: '100',
29
+ brightRed: '101',
30
+ brightGreen: '102',
31
+ brightYellow: '103',
32
+ brightBlue: '104',
33
+ brightMagenta: '105',
34
+ brightCyan: '106',
35
+ brightWhite: '107',
36
+ };
37
+ export function applyStyle(text, style) {
38
+ if (!style)
39
+ return text;
40
+ const codes = [];
41
+ if (style.color) {
42
+ codes.push(ANSI_COLORS[style.color]);
43
+ }
44
+ if (style.backgroundColor) {
45
+ codes.push(ANSI_BG_COLORS[style.backgroundColor]);
46
+ }
47
+ if (style.bold) {
48
+ codes.push('1');
49
+ }
50
+ if (style.italic) {
51
+ codes.push('3');
52
+ }
53
+ if (style.underline) {
54
+ codes.push('4');
55
+ }
56
+ if (codes.length === 0) {
57
+ return text;
58
+ }
59
+ return `\x1b[${codes.join(';')}m${text}\x1b[0m`;
60
+ }
61
+ //# sourceMappingURL=colors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.js","sourceRoot":"","sources":["../../src/ts/utils/colors.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAA0B;IACzC,KAAK,EAAE,IAAI;IACX,GAAG,EAAE,IAAI;IACT,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,IAAI;IACjB,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;IACjB,YAAY,EAAE,IAAI;IAClB,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,IAAI;IACnB,UAAU,EAAE,IAAI;IAChB,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,MAAM,cAAc,GAA0B;IAC5C,KAAK,EAAE,IAAI;IACX,GAAG,EAAE,IAAI;IACT,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,KAAK;IAClB,SAAS,EAAE,KAAK;IAChB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,KAAK;IACjB,aAAa,EAAE,KAAK;IACpB,UAAU,EAAE,KAAK;IACjB,WAAW,EAAE,KAAK;CACnB,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAiB;IACxD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,QAAQ,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,SAAS,CAAC;AAClD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "linecraft",
3
+ "version": "0.1.0",
4
+ "description": "High-performance terminal UI library for Node.js with Zig backend",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "build:zig": "zig build",
10
+ "build:ts": "tsc",
11
+ "build": "pnpm build:zig && pnpm build:ts",
12
+ "dev": "pnpm build:zig && pnpm build:ts --watch",
13
+ "test": "zig build test",
14
+ "test:ansi": "zig build test:ansi",
15
+ "test:throttle": "zig build test:throttle",
16
+ "test:buffer": "zig build test:buffer",
17
+ "test:diff": "zig build test:diff",
18
+ "test:region": "zig build test:region",
19
+ "test:ts": "vitest",
20
+ "example:progress": "tsx examples/basic-progress.ts",
21
+ "example:spinner": "tsx examples/spinner.ts",
22
+ "example:multi": "tsx examples/multi-lane.ts"
23
+ },
24
+ "keywords": [
25
+ "terminal",
26
+ "tui",
27
+ "progress",
28
+ "spinner",
29
+ "ansi",
30
+ "zig"
31
+ ],
32
+ "dependencies": {
33
+ "ffi-napi": "^4.0.3",
34
+ "ref-napi": "^3.0.3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "tsx": "^4.7.0",
39
+ "typescript": "^5.3.0",
40
+ "vitest": "^1.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }
46
+
@@ -0,0 +1,53 @@
1
+ import { TerminalRegion } from '../region.js';
2
+ import type { ProgressBarOptions } from '../types.js';
3
+
4
+ export class ProgressBar {
5
+ private region: TerminalRegion;
6
+ private lineNumber: number; // 1-based
7
+ private current: number = 0;
8
+ private total: number = 100;
9
+ private label: string;
10
+ private width: number;
11
+ private completeChar: string;
12
+ private incompleteChar: string;
13
+ private brackets: [string, string];
14
+
15
+ constructor(region: TerminalRegion, lineNumber: number, options: ProgressBarOptions = {}) {
16
+ this.region = region;
17
+ this.lineNumber = lineNumber;
18
+ this.label = options.label || '';
19
+ this.width = options.width ?? 40;
20
+ this.completeChar = options.style?.complete ?? '█';
21
+ this.incompleteChar = options.style?.incomplete ?? '░';
22
+ this.brackets = options.style?.brackets ?? ['[', ']'];
23
+ }
24
+
25
+ update(current: number, total: number): void {
26
+ this.current = current;
27
+ this.total = total;
28
+ this.render();
29
+ }
30
+
31
+ setLabel(label: string): void {
32
+ this.label = label;
33
+ this.render();
34
+ }
35
+
36
+ private render(): void {
37
+ const percentage = Math.min(100, Math.max(0, (this.current / this.total) * 100));
38
+ const filled = Math.floor((percentage / 100) * this.width);
39
+ const empty = this.width - filled;
40
+
41
+ const bar = this.completeChar.repeat(filled) + this.incompleteChar.repeat(empty);
42
+ const text = `${this.label} ${this.brackets[0]}${bar}${this.brackets[1]} ${percentage.toFixed(1)}%`;
43
+
44
+ // Just update the line - Zig handles batching and rendering
45
+ this.region.setLine(this.lineNumber, text);
46
+ // Optional: call flush() if you need immediate rendering
47
+ }
48
+
49
+ finish(): void {
50
+ this.update(this.total, this.total);
51
+ }
52
+ }
53
+
@@ -0,0 +1,56 @@
1
+ import { TerminalRegion } from '../region.js';
2
+ import type { SpinnerOptions } from '../types.js';
3
+
4
+ const DEFAULT_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+
6
+ export class Spinner {
7
+ private region: TerminalRegion;
8
+ private lineNumber: number; // 1-based
9
+ private frameIndex: number = 0;
10
+ private text: string = '';
11
+ private interval?: NodeJS.Timeout;
12
+ private isRunning: boolean = false;
13
+ private frames: string[];
14
+ private intervalMs: number;
15
+
16
+ constructor(region: TerminalRegion, lineNumber: number, options: SpinnerOptions = {}) {
17
+ this.region = region;
18
+ this.lineNumber = lineNumber;
19
+ this.frames = options.frames ?? DEFAULT_SPINNER_FRAMES;
20
+ this.intervalMs = options.interval ?? 100;
21
+ }
22
+
23
+ start(): void {
24
+ if (this.isRunning) return;
25
+ this.isRunning = true;
26
+ this.interval = setInterval(() => {
27
+ this.render();
28
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
29
+ }, this.intervalMs);
30
+ }
31
+
32
+ stop(): void {
33
+ if (!this.isRunning) return;
34
+ this.isRunning = false;
35
+ if (this.interval) {
36
+ clearInterval(this.interval);
37
+ }
38
+ // Clear the spinner line
39
+ this.region.setLine(this.lineNumber, '');
40
+ }
41
+
42
+ setText(text: string): void {
43
+ this.text = text;
44
+ this.render();
45
+ }
46
+
47
+ private render(): void {
48
+ const frame = this.frames[this.frameIndex];
49
+ const line = `${frame} ${this.text}`;
50
+
51
+ // Just update the line - Zig handles batching and rendering
52
+ this.region.setLine(this.lineNumber, line);
53
+ // Spinner updates frequently, so Zig's throttling will handle smooth animation
54
+ }
55
+ }
56
+
@@ -0,0 +1,37 @@
1
+ export { TerminalRegion } from './region.js';
2
+ export { ProgressBar } from './components/progress-bar.js';
3
+ export { Spinner } from './components/spinner.js';
4
+ export type {
5
+ RegionOptions,
6
+ LineContent,
7
+ TextStyle,
8
+ Color,
9
+ ProgressBarOptions,
10
+ SpinnerOptions,
11
+ } from './types.js';
12
+
13
+ import { TerminalRegion } from './region.js';
14
+ import { ProgressBar } from './components/progress-bar.js';
15
+ import { Spinner } from './components/spinner.js';
16
+ import type { RegionOptions, ProgressBarOptions, SpinnerOptions } from './types.js';
17
+
18
+ export function createRegion(options?: RegionOptions): TerminalRegion {
19
+ return new TerminalRegion(options);
20
+ }
21
+
22
+ export function createProgressBar(
23
+ region: TerminalRegion,
24
+ lineNumber: number,
25
+ options?: ProgressBarOptions
26
+ ): ProgressBar {
27
+ return new ProgressBar(region, lineNumber, options);
28
+ }
29
+
30
+ export function createSpinner(
31
+ region: TerminalRegion,
32
+ lineNumber: number,
33
+ options?: SpinnerOptions
34
+ ): Spinner {
35
+ return new Spinner(region, lineNumber, options);
36
+ }
37
+
@@ -0,0 +1,86 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { Library } from 'ffi-napi';
4
+ import ref from 'ref-napi';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Determine the library path based on platform
10
+ function getLibraryPath(): string {
11
+ const platform = process.platform;
12
+
13
+ if (platform === 'darwin') {
14
+ return join(__dirname, '../../zig-out/lib/libechokit.dylib');
15
+ } else if (platform === 'win32') {
16
+ return join(__dirname, '../../zig-out/lib/echokit.dll');
17
+ } else {
18
+ return join(__dirname, '../../zig-out/lib/libechokit.so');
19
+ }
20
+ }
21
+
22
+ // Load the Zig library using FFI
23
+ const lib = Library(getLibraryPath(), {
24
+ create_region: ['uint64', ['uint32', 'uint32', 'uint32', 'uint32']],
25
+ destroy_region: ['void', ['uint64']],
26
+ set_line: ['void', ['uint64', 'uint32', 'pointer', 'size_t']],
27
+ set: ['void', ['uint64', 'pointer', 'size_t']],
28
+ clear_line: ['void', ['uint64', 'uint32']],
29
+ clear_region: ['void', ['uint64']],
30
+ flush: ['void', ['uint64']],
31
+ set_throttle_fps: ['void', ['uint64', 'uint32']],
32
+ });
33
+
34
+ export interface NativeRegion {
35
+ createRegion(x: number, y: number, width: number, height: number): number;
36
+ destroyRegion(handle: number): void;
37
+ setLine(handle: number, lineNumber: number, content: string): void; // 1-based
38
+ set(handle: number, content: string): void;
39
+ clearLine(handle: number, lineNumber: number): void; // 1-based
40
+ clearRegion(handle: number): void;
41
+ flush(handle: number): void;
42
+ setThrottleFps(handle: number, fps: number): void;
43
+ }
44
+
45
+ export const native: NativeRegion = {
46
+ createRegion: (x: number, y: number, width: number, height: number): number => {
47
+ return Number(lib.create_region(x, y, width, height));
48
+ },
49
+
50
+ destroyRegion: (handle: number): void => {
51
+ lib.destroy_region(BigInt(handle));
52
+ },
53
+
54
+ setLine: (handle: number, lineNumber: number, content: string): void => {
55
+ const buf = Buffer.from(content, 'utf8');
56
+ // Allocate a buffer and copy the string data
57
+ const ptr = ref.alloc(buf.length);
58
+ buf.copy(ptr);
59
+ lib.set_line(BigInt(handle), lineNumber, ptr, buf.length);
60
+ },
61
+
62
+ set: (handle: number, content: string): void => {
63
+ const buf = Buffer.from(content, 'utf8');
64
+ // Allocate a buffer and copy the string data
65
+ const ptr = ref.alloc(buf.length);
66
+ buf.copy(ptr);
67
+ lib.set(BigInt(handle), ptr, buf.length);
68
+ },
69
+
70
+ clearLine: (handle: number, lineNumber: number): void => {
71
+ lib.clear_line(BigInt(handle), lineNumber);
72
+ },
73
+
74
+ clearRegion: (handle: number): void => {
75
+ lib.clear_region(BigInt(handle));
76
+ },
77
+
78
+ flush: (handle: number): void => {
79
+ lib.flush(BigInt(handle));
80
+ },
81
+
82
+ setThrottleFps: (handle: number, fps: number): void => {
83
+ lib.set_throttle_fps(BigInt(handle), fps);
84
+ },
85
+ };
86
+
@@ -0,0 +1,89 @@
1
+ import { native } from './native.js';
2
+ import { applyStyle } from './utils/colors.js';
3
+ import type { RegionOptions, LineContent, TextStyle } from './types.js';
4
+
5
+ export class TerminalRegion {
6
+ private handle: number;
7
+ private _width: number;
8
+ private _height: number; // Current height (may expand)
9
+
10
+ constructor(options: RegionOptions = {}) {
11
+ const x = options.x ?? 0;
12
+ const y = options.y ?? 0;
13
+ this._width = options.width ?? (process.stdout.columns ?? 80);
14
+ this._height = options.height ?? 1; // Default to 1 line, expands as needed
15
+ this.handle = native.createRegion(x, y, this._width, this._height);
16
+ }
17
+
18
+ get width(): number {
19
+ return this._width;
20
+ }
21
+
22
+ get height(): number {
23
+ return this._height;
24
+ }
25
+
26
+ setLine(lineNumber: number, content: string | LineContent): void {
27
+ if (lineNumber < 1) {
28
+ throw new Error('Line numbers start at 1');
29
+ }
30
+
31
+ // Zig handles batching and expansion automatically
32
+ const text = typeof content === 'string' ? content : content.text;
33
+ // Apply styling if provided
34
+ const styled = applyStyle(text, typeof content === 'object' ? content.style : undefined);
35
+ native.setLine(this.handle, lineNumber, styled);
36
+ // Zig will:
37
+ // - Convert to 0-based internally
38
+ // - Expand region if lineNumber > current height
39
+ // - Buffer this update in pending_frame
40
+ // - Check throttle
41
+ // - Schedule render if needed (or render immediately if throttle allows)
42
+
43
+ // Update our height tracking if Zig expanded
44
+ if (lineNumber > this._height) {
45
+ this._height = lineNumber;
46
+ }
47
+ }
48
+
49
+ set(content: string | LineContent[]): void {
50
+ if (typeof content === 'string') {
51
+ // Single string with \n line breaks
52
+ native.set(this.handle, content);
53
+ // Update height based on line count
54
+ this._height = content.split('\n').length;
55
+ } else {
56
+ // Array of LineContent
57
+ const lines = content.map(c =>
58
+ applyStyle(c.text, c.style)
59
+ ).join('\n');
60
+ native.set(this.handle, lines);
61
+ this._height = content.length;
62
+ }
63
+ }
64
+
65
+ clearLine(lineNumber: number): void {
66
+ if (lineNumber < 1) {
67
+ throw new Error('Line numbers start at 1');
68
+ }
69
+ native.clearLine(this.handle, lineNumber);
70
+ }
71
+
72
+ clear(): void {
73
+ native.clearRegion(this.handle);
74
+ }
75
+
76
+ flush(): void {
77
+ // Force immediate render of any pending updates (bypasses throttle)
78
+ native.flush(this.handle);
79
+ }
80
+
81
+ setThrottle(fps: number): void {
82
+ native.setThrottleFps(this.handle, fps);
83
+ }
84
+
85
+ destroy(): void {
86
+ native.destroyRegion(this.handle);
87
+ }
88
+ }
89
+