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/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":"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"}
|
package/lib/region.d.ts
ADDED
|
@@ -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
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/ts/types.ts"],"names":[],"mappings":""}
|
|
@@ -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
|
+
|
package/src/ts/index.ts
ADDED
|
@@ -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
|
+
|
package/src/ts/native.ts
ADDED
|
@@ -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
|
+
|
package/src/ts/region.ts
ADDED
|
@@ -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
|
+
|