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/src/ts/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type Color =
|
|
2
|
+
| 'black'
|
|
3
|
+
| 'red'
|
|
4
|
+
| 'green'
|
|
5
|
+
| 'yellow'
|
|
6
|
+
| 'blue'
|
|
7
|
+
| 'magenta'
|
|
8
|
+
| 'cyan'
|
|
9
|
+
| 'white'
|
|
10
|
+
| 'brightBlack'
|
|
11
|
+
| 'brightRed'
|
|
12
|
+
| 'brightGreen'
|
|
13
|
+
| 'brightYellow'
|
|
14
|
+
| 'brightBlue'
|
|
15
|
+
| 'brightMagenta'
|
|
16
|
+
| 'brightCyan'
|
|
17
|
+
| 'brightWhite';
|
|
18
|
+
|
|
19
|
+
export interface TextStyle {
|
|
20
|
+
color?: Color;
|
|
21
|
+
backgroundColor?: Color;
|
|
22
|
+
bold?: boolean;
|
|
23
|
+
italic?: boolean;
|
|
24
|
+
underline?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LineContent {
|
|
28
|
+
text: string;
|
|
29
|
+
style?: TextStyle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RegionOptions {
|
|
33
|
+
x?: number; // Default: 0
|
|
34
|
+
y?: number; // Default: 0
|
|
35
|
+
width?: number; // Default: terminal width
|
|
36
|
+
height?: number; // Default: 1 (expands as needed)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProgressBarOptions {
|
|
40
|
+
label?: string;
|
|
41
|
+
width?: number;
|
|
42
|
+
style?: {
|
|
43
|
+
complete?: string;
|
|
44
|
+
incomplete?: string;
|
|
45
|
+
brackets?: [string, string];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SpinnerOptions {
|
|
50
|
+
frames?: string[];
|
|
51
|
+
interval?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Color, TextStyle } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const ANSI_COLORS: Record<Color, string> = {
|
|
4
|
+
black: '30',
|
|
5
|
+
red: '31',
|
|
6
|
+
green: '32',
|
|
7
|
+
yellow: '33',
|
|
8
|
+
blue: '34',
|
|
9
|
+
magenta: '35',
|
|
10
|
+
cyan: '36',
|
|
11
|
+
white: '37',
|
|
12
|
+
brightBlack: '90',
|
|
13
|
+
brightRed: '91',
|
|
14
|
+
brightGreen: '92',
|
|
15
|
+
brightYellow: '93',
|
|
16
|
+
brightBlue: '94',
|
|
17
|
+
brightMagenta: '95',
|
|
18
|
+
brightCyan: '96',
|
|
19
|
+
brightWhite: '97',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ANSI_BG_COLORS: Record<Color, string> = {
|
|
23
|
+
black: '40',
|
|
24
|
+
red: '41',
|
|
25
|
+
green: '42',
|
|
26
|
+
yellow: '43',
|
|
27
|
+
blue: '44',
|
|
28
|
+
magenta: '45',
|
|
29
|
+
cyan: '46',
|
|
30
|
+
white: '47',
|
|
31
|
+
brightBlack: '100',
|
|
32
|
+
brightRed: '101',
|
|
33
|
+
brightGreen: '102',
|
|
34
|
+
brightYellow: '103',
|
|
35
|
+
brightBlue: '104',
|
|
36
|
+
brightMagenta: '105',
|
|
37
|
+
brightCyan: '106',
|
|
38
|
+
brightWhite: '107',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function applyStyle(text: string, style?: TextStyle): string {
|
|
42
|
+
if (!style) return text;
|
|
43
|
+
|
|
44
|
+
const codes: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (style.color) {
|
|
47
|
+
codes.push(ANSI_COLORS[style.color]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (style.backgroundColor) {
|
|
51
|
+
codes.push(ANSI_BG_COLORS[style.backgroundColor]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (style.bold) {
|
|
55
|
+
codes.push('1');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (style.italic) {
|
|
59
|
+
codes.push('3');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (style.underline) {
|
|
63
|
+
codes.push('4');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (codes.length === 0) {
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `\x1b[${codes.join(';')}m${text}\x1b[0m`;
|
|
71
|
+
}
|
|
72
|
+
|
package/src/zig/ansi.zig
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ANSI escape code generation
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const Allocator = std.mem.Allocator;
|
|
4
|
+
|
|
5
|
+
pub fn move_cursor_to(allocator: Allocator, x: u32, y: u32) ![]u8 {
|
|
6
|
+
return try std.fmt.allocPrint(allocator, "\x1b[{d};{d}H", .{ y, x });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
pub fn move_cursor_up(allocator: Allocator, n: u32) ![]u8 {
|
|
10
|
+
return try std.fmt.allocPrint(allocator, "\x1b[{d}A", .{n});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn move_cursor_down(allocator: Allocator, n: u32) ![]u8 {
|
|
14
|
+
return try std.fmt.allocPrint(allocator, "\x1b[{d}B", .{n});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub const CLEAR_LINE = "\x1b[2K";
|
|
18
|
+
pub const HIDE_CURSOR = "\x1b[?25l";
|
|
19
|
+
pub const SHOW_CURSOR = "\x1b[?25h";
|
|
20
|
+
pub const SAVE_CURSOR = "\x1b[s";
|
|
21
|
+
pub const RESTORE_CURSOR = "\x1b[u";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Render buffer for batching ANSI operations
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const Allocator = std.mem.Allocator;
|
|
4
|
+
|
|
5
|
+
pub const RenderBuffer = struct {
|
|
6
|
+
data: std.ArrayList(u8),
|
|
7
|
+
allocator: Allocator,
|
|
8
|
+
stdout: std.fs.File,
|
|
9
|
+
|
|
10
|
+
pub fn init(allocator: Allocator, stdout: std.fs.File) RenderBuffer {
|
|
11
|
+
return .{
|
|
12
|
+
.data = std.ArrayList(u8){},
|
|
13
|
+
.allocator = allocator,
|
|
14
|
+
.stdout = stdout,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn deinit(self: *RenderBuffer) void {
|
|
19
|
+
self.data.deinit(self.allocator);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn write(self: *RenderBuffer, bytes: []const u8) !void {
|
|
23
|
+
try self.data.appendSlice(self.allocator, bytes);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn flush(self: *RenderBuffer) !void {
|
|
27
|
+
if (self.data.items.len > 0) {
|
|
28
|
+
// Write all data
|
|
29
|
+
try self.stdout.writeAll(self.data.items);
|
|
30
|
+
self.data.clearRetainingCapacity();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn clear(self: *RenderBuffer) void {
|
|
35
|
+
self.data.clearRetainingCapacity();
|
|
36
|
+
}
|
|
37
|
+
};
|
package/src/zig/diff.zig
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Diffing algorithm for efficient updates
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const Allocator = std.mem.Allocator;
|
|
4
|
+
|
|
5
|
+
pub const DiffOp = union(enum) {
|
|
6
|
+
no_change: void,
|
|
7
|
+
update_line: struct { line: u32, content: []u8 },
|
|
8
|
+
delete_line: u32,
|
|
9
|
+
insert_line: struct { line: u32, content: []u8 },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn diff_frames(
|
|
13
|
+
prev: [][]u8,
|
|
14
|
+
curr: [][]u8,
|
|
15
|
+
allocator: Allocator,
|
|
16
|
+
) ![]DiffOp {
|
|
17
|
+
var ops = std.ArrayList(DiffOp){};
|
|
18
|
+
errdefer ops.deinit(allocator);
|
|
19
|
+
|
|
20
|
+
const max_len = @max(prev.len, curr.len);
|
|
21
|
+
|
|
22
|
+
for (0..max_len) |i| {
|
|
23
|
+
const prev_line = if (i < prev.len) prev[i] else null;
|
|
24
|
+
const curr_line = if (i < curr.len) curr[i] else null;
|
|
25
|
+
|
|
26
|
+
if (prev_line == null and curr_line != null) {
|
|
27
|
+
// Line inserted
|
|
28
|
+
try ops.append(allocator, .{ .insert_line = .{ .line = @intCast(i), .content = curr_line.? } });
|
|
29
|
+
} else if (prev_line != null and curr_line == null) {
|
|
30
|
+
// Line deleted
|
|
31
|
+
try ops.append(allocator, .{ .delete_line = @intCast(i) });
|
|
32
|
+
} else if (prev_line != null and curr_line != null) {
|
|
33
|
+
// Check if line changed
|
|
34
|
+
if (!std.mem.eql(u8, prev_line.?, curr_line.?)) {
|
|
35
|
+
try ops.append(allocator, .{ .update_line = .{ .line = @intCast(i), .content = curr_line.? } });
|
|
36
|
+
} else {
|
|
37
|
+
try ops.append(allocator, .no_change);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return try ops.toOwnedSlice(allocator);
|
|
43
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Region management for terminal rendering
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const Allocator = std.mem.Allocator;
|
|
4
|
+
const ansi = @import("ansi.zig");
|
|
5
|
+
const diff = @import("diff.zig");
|
|
6
|
+
const buffer = @import("buffer.zig");
|
|
7
|
+
const throttle = @import("throttle.zig");
|
|
8
|
+
|
|
9
|
+
pub const Region = struct {
|
|
10
|
+
allocator: Allocator,
|
|
11
|
+
x: u32,
|
|
12
|
+
y: u32,
|
|
13
|
+
width: u32,
|
|
14
|
+
height: u32,
|
|
15
|
+
pending_frame: std.ArrayList([]u8),
|
|
16
|
+
previous_frame: std.ArrayList([]u8),
|
|
17
|
+
render_scheduled: bool,
|
|
18
|
+
throttle_state: throttle.Throttle,
|
|
19
|
+
render_buffer: buffer.RenderBuffer,
|
|
20
|
+
stdout: std.fs.File,
|
|
21
|
+
disable_rendering: bool = false, // For tests - skip actual rendering
|
|
22
|
+
|
|
23
|
+
pub fn init(
|
|
24
|
+
allocator: Allocator,
|
|
25
|
+
x: u32,
|
|
26
|
+
y: u32,
|
|
27
|
+
width: u32,
|
|
28
|
+
height: u32,
|
|
29
|
+
stdout: std.fs.File,
|
|
30
|
+
) !Region {
|
|
31
|
+
var pending = std.ArrayList([]u8){};
|
|
32
|
+
var previous = std.ArrayList([]u8){};
|
|
33
|
+
|
|
34
|
+
// Initialize with empty lines
|
|
35
|
+
try pending.ensureTotalCapacity(allocator, height);
|
|
36
|
+
try previous.ensureTotalCapacity(allocator, height);
|
|
37
|
+
for (0..height) |_| {
|
|
38
|
+
try pending.append(allocator, try allocator.dupe(u8, ""));
|
|
39
|
+
try previous.append(allocator, try allocator.dupe(u8, ""));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return .{
|
|
43
|
+
.allocator = allocator,
|
|
44
|
+
.x = x,
|
|
45
|
+
.y = y,
|
|
46
|
+
.width = width,
|
|
47
|
+
.height = height,
|
|
48
|
+
.pending_frame = pending,
|
|
49
|
+
.previous_frame = previous,
|
|
50
|
+
.render_scheduled = false,
|
|
51
|
+
.throttle_state = throttle.Throttle.init(60),
|
|
52
|
+
.render_buffer = buffer.RenderBuffer.init(allocator, stdout),
|
|
53
|
+
.stdout = stdout,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn deinit(self: *Region) void {
|
|
58
|
+
// Free frame buffers
|
|
59
|
+
for (self.pending_frame.items) |line| {
|
|
60
|
+
self.allocator.free(line);
|
|
61
|
+
}
|
|
62
|
+
self.pending_frame.deinit(self.allocator);
|
|
63
|
+
|
|
64
|
+
for (self.previous_frame.items) |line| {
|
|
65
|
+
self.allocator.free(line);
|
|
66
|
+
}
|
|
67
|
+
self.previous_frame.deinit(self.allocator);
|
|
68
|
+
|
|
69
|
+
self.render_buffer.deinit();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub fn expand_to(self: *Region, new_height: u32) !void {
|
|
73
|
+
const old_height = self.height;
|
|
74
|
+
_ = old_height; // autofix
|
|
75
|
+
self.height = new_height;
|
|
76
|
+
|
|
77
|
+
// Expand pending_frame
|
|
78
|
+
try self.pending_frame.ensureTotalCapacity(self.allocator, new_height);
|
|
79
|
+
while (self.pending_frame.items.len < new_height) {
|
|
80
|
+
try self.pending_frame.append(self.allocator, try self.allocator.dupe(u8, ""));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Expand previous_frame
|
|
84
|
+
try self.previous_frame.ensureTotalCapacity(self.allocator, new_height);
|
|
85
|
+
while (self.previous_frame.items.len < new_height) {
|
|
86
|
+
try self.previous_frame.append(self.allocator, try self.allocator.dupe(u8, ""));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn set_line(self: *Region, line_number: u32, content: []const u8) !void {
|
|
91
|
+
// Convert 1-based to 0-based
|
|
92
|
+
if (line_number == 0) {
|
|
93
|
+
return error.InvalidLineNumber;
|
|
94
|
+
}
|
|
95
|
+
const line_index = line_number - 1;
|
|
96
|
+
|
|
97
|
+
// Expand if needed
|
|
98
|
+
if (line_index >= self.height) {
|
|
99
|
+
try self.expand_to(line_index + 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Ensure pending_frame has enough lines
|
|
103
|
+
while (self.pending_frame.items.len <= line_index) {
|
|
104
|
+
try self.pending_frame.append(self.allocator, try self.allocator.dupe(u8, ""));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Free old line and set new one
|
|
108
|
+
self.allocator.free(self.pending_frame.items[line_index]);
|
|
109
|
+
self.pending_frame.items[line_index] = try self.allocator.dupe(u8, content);
|
|
110
|
+
|
|
111
|
+
// Schedule render
|
|
112
|
+
self.schedule_render();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub fn set(self: *Region, content: []const u8) !void {
|
|
116
|
+
// Split by \n to get lines
|
|
117
|
+
var lines = std.ArrayList([]const u8){};
|
|
118
|
+
defer lines.deinit(self.allocator);
|
|
119
|
+
|
|
120
|
+
var it = std.mem.splitScalar(u8, content, '\n');
|
|
121
|
+
while (it.next()) |line| {
|
|
122
|
+
try lines.append(self.allocator, line);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Expand region if needed
|
|
126
|
+
if (lines.items.len > self.height) {
|
|
127
|
+
try self.expand_to(@intCast(lines.items.len));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Free old pending frame
|
|
131
|
+
for (self.pending_frame.items) |line| {
|
|
132
|
+
self.allocator.free(line);
|
|
133
|
+
}
|
|
134
|
+
self.pending_frame.clearRetainingCapacity();
|
|
135
|
+
|
|
136
|
+
// Update all lines in pending_frame
|
|
137
|
+
try self.pending_frame.ensureTotalCapacity(self.allocator, lines.items.len);
|
|
138
|
+
for (lines.items) |line| {
|
|
139
|
+
try self.pending_frame.append(self.allocator, try self.allocator.dupe(u8, line));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Schedule render
|
|
143
|
+
self.schedule_render();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pub fn schedule_render(self: *Region) void {
|
|
147
|
+
// Skip rendering if disabled (for tests)
|
|
148
|
+
if (self.disable_rendering) {
|
|
149
|
+
// Just copy pending to previous without actually rendering
|
|
150
|
+
self.copy_pending_to_previous() catch {};
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (self.throttle_state.should_render()) {
|
|
155
|
+
self.render_now() catch |e| {
|
|
156
|
+
std.log.err("Render error: {}", .{e});
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
self.render_scheduled = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn copy_pending_to_previous(self: *Region) !void {
|
|
164
|
+
// Free old previous_frame
|
|
165
|
+
for (self.previous_frame.items) |line| {
|
|
166
|
+
self.allocator.free(line);
|
|
167
|
+
}
|
|
168
|
+
self.previous_frame.clearRetainingCapacity();
|
|
169
|
+
|
|
170
|
+
// Copy pending to previous
|
|
171
|
+
try self.previous_frame.ensureTotalCapacity(self.allocator, self.pending_frame.items.len);
|
|
172
|
+
for (self.pending_frame.items) |line| {
|
|
173
|
+
try self.previous_frame.append(self.allocator, try self.allocator.dupe(u8, line));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub fn render_now(self: *Region) !void {
|
|
178
|
+
// Hide cursor
|
|
179
|
+
try self.render_buffer.write(ansi.HIDE_CURSOR);
|
|
180
|
+
|
|
181
|
+
// Move to region start
|
|
182
|
+
const move_seq = try ansi.move_cursor_to(self.allocator, self.x, self.y);
|
|
183
|
+
defer self.allocator.free(move_seq);
|
|
184
|
+
try self.render_buffer.write(move_seq);
|
|
185
|
+
|
|
186
|
+
// Diff and render
|
|
187
|
+
const diff_ops = try diff.diff_frames(
|
|
188
|
+
self.previous_frame.items,
|
|
189
|
+
self.pending_frame.items,
|
|
190
|
+
self.allocator,
|
|
191
|
+
);
|
|
192
|
+
defer {
|
|
193
|
+
for (diff_ops) |_| {}
|
|
194
|
+
self.allocator.free(diff_ops);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var current_line: u32 = 0;
|
|
198
|
+
for (diff_ops) |op| {
|
|
199
|
+
switch (op) {
|
|
200
|
+
.update_line => |update| {
|
|
201
|
+
// Move to line if needed
|
|
202
|
+
if (update.line != current_line) {
|
|
203
|
+
const move = try ansi.move_cursor_to(
|
|
204
|
+
self.allocator,
|
|
205
|
+
self.x,
|
|
206
|
+
self.y + update.line,
|
|
207
|
+
);
|
|
208
|
+
defer self.allocator.free(move);
|
|
209
|
+
try self.render_buffer.write(move);
|
|
210
|
+
current_line = update.line;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Clear line and write new content
|
|
214
|
+
try self.render_buffer.write(ansi.CLEAR_LINE);
|
|
215
|
+
try self.render_buffer.write(update.content);
|
|
216
|
+
current_line += 1;
|
|
217
|
+
},
|
|
218
|
+
.insert_line => |insert| {
|
|
219
|
+
// Move to line
|
|
220
|
+
const move = try ansi.move_cursor_to(
|
|
221
|
+
self.allocator,
|
|
222
|
+
self.x,
|
|
223
|
+
self.y + insert.line,
|
|
224
|
+
);
|
|
225
|
+
defer self.allocator.free(move);
|
|
226
|
+
try self.render_buffer.write(move);
|
|
227
|
+
|
|
228
|
+
// Write content
|
|
229
|
+
try self.render_buffer.write(insert.content);
|
|
230
|
+
current_line = insert.line + 1;
|
|
231
|
+
},
|
|
232
|
+
.delete_line => |del| {
|
|
233
|
+
// Move to line and clear it
|
|
234
|
+
const move = try ansi.move_cursor_to(
|
|
235
|
+
self.allocator,
|
|
236
|
+
self.x,
|
|
237
|
+
self.y + del,
|
|
238
|
+
);
|
|
239
|
+
defer self.allocator.free(move);
|
|
240
|
+
try self.render_buffer.write(move);
|
|
241
|
+
try self.render_buffer.write(ansi.CLEAR_LINE);
|
|
242
|
+
},
|
|
243
|
+
.no_change => {
|
|
244
|
+
// Skip unchanged lines
|
|
245
|
+
current_line += 1;
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Show cursor
|
|
251
|
+
try self.render_buffer.write(ansi.SHOW_CURSOR);
|
|
252
|
+
|
|
253
|
+
// Flush buffer
|
|
254
|
+
try self.render_buffer.flush();
|
|
255
|
+
|
|
256
|
+
// Copy pending_frame to previous_frame
|
|
257
|
+
for (self.previous_frame.items) |line| {
|
|
258
|
+
self.allocator.free(line);
|
|
259
|
+
}
|
|
260
|
+
self.previous_frame.clearRetainingCapacity();
|
|
261
|
+
|
|
262
|
+
try self.previous_frame.ensureTotalCapacity(self.allocator, self.pending_frame.items.len);
|
|
263
|
+
for (self.pending_frame.items) |line| {
|
|
264
|
+
try self.previous_frame.append(self.allocator, try self.allocator.dupe(u8, line));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
self.render_scheduled = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pub fn flush(self: *Region) !void {
|
|
271
|
+
// Force immediate render
|
|
272
|
+
try self.render_now();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
pub fn set_throttle_fps(self: *Region, fps: u32) void {
|
|
276
|
+
self.throttle_state.set_fps(fps);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub fn clear_line(self: *Region, line_number: u32) !void {
|
|
280
|
+
if (line_number == 0) {
|
|
281
|
+
return error.InvalidLineNumber;
|
|
282
|
+
}
|
|
283
|
+
try self.set_line(line_number, "");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub fn clear(self: *Region) !void {
|
|
287
|
+
// Clear all lines
|
|
288
|
+
for (0..self.height) |i| {
|
|
289
|
+
try self.set_line(@intCast(i + 1), "");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Main renderer with N-API bindings
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const region = @import("region.zig");
|
|
4
|
+
|
|
5
|
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
6
|
+
const allocator = gpa.allocator();
|
|
7
|
+
|
|
8
|
+
var regions = std.ArrayList(*region.Region){};
|
|
9
|
+
var region_mutex = std.Thread.Mutex{};
|
|
10
|
+
|
|
11
|
+
// Get stdout file handle
|
|
12
|
+
fn get_stdout() std.fs.File {
|
|
13
|
+
return std.fs.File{ .handle = 1 }; // stdout handle
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export fn create_region(x: u32, y: u32, width: u32, height: u32) u64 {
|
|
17
|
+
region_mutex.lock();
|
|
18
|
+
defer region_mutex.unlock();
|
|
19
|
+
|
|
20
|
+
const r = allocator.create(region.Region) catch return 0;
|
|
21
|
+
r.* = region.Region.init(allocator, x, y, width, height, get_stdout()) catch {
|
|
22
|
+
allocator.destroy(r);
|
|
23
|
+
return 0;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
regions.append(allocator, r) catch {
|
|
27
|
+
r.deinit();
|
|
28
|
+
allocator.destroy(r);
|
|
29
|
+
return 0;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return @intFromPtr(r);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export fn destroy_region(handle: u64) void {
|
|
36
|
+
region_mutex.lock();
|
|
37
|
+
defer region_mutex.unlock();
|
|
38
|
+
|
|
39
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
40
|
+
r.deinit();
|
|
41
|
+
allocator.destroy(r);
|
|
42
|
+
|
|
43
|
+
// Remove from regions list
|
|
44
|
+
for (regions.items, 0..) |reg, i| {
|
|
45
|
+
if (reg == r) {
|
|
46
|
+
_ = regions.swapRemove(i);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export fn set_line(handle: u64, line_number: u32, content: [*]const u8, len: usize) void {
|
|
53
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
54
|
+
const content_slice = content[0..len];
|
|
55
|
+
r.set_line(line_number, content_slice) catch |e| {
|
|
56
|
+
std.log.err("set_line error: {}", .{e});
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export fn set(handle: u64, content: [*]const u8, len: usize) void {
|
|
61
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
62
|
+
const content_slice = content[0..len];
|
|
63
|
+
r.set(content_slice) catch |e| {
|
|
64
|
+
std.log.err("set error: {}", .{e});
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export fn clear_line(handle: u64, line_number: u32) void {
|
|
69
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
70
|
+
r.clear_line(line_number) catch |e| {
|
|
71
|
+
std.log.err("clear_line error: {}", .{e});
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export fn clear_region(handle: u64) void {
|
|
76
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
77
|
+
r.clear() catch |e| {
|
|
78
|
+
std.log.err("clear_region error: {}", .{e});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export fn flush(handle: u64) void {
|
|
83
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
84
|
+
r.flush() catch |e| {
|
|
85
|
+
std.log.err("flush error: {}", .{e});
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export fn set_throttle_fps(handle: u64, fps: u32) void {
|
|
90
|
+
const r = @as(*region.Region, @ptrFromInt(handle));
|
|
91
|
+
r.set_throttle_fps(fps);
|
|
92
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const ansi = @import("ansi.zig");
|
|
3
|
+
const testing = std.testing;
|
|
4
|
+
|
|
5
|
+
test "ANSI: move_cursor_to generates correct ANSI" {
|
|
6
|
+
const allocator = testing.allocator;
|
|
7
|
+
const seq = try ansi.move_cursor_to(allocator, 10, 5);
|
|
8
|
+
defer allocator.free(seq);
|
|
9
|
+
|
|
10
|
+
try testing.expectEqualStrings("\x1b[5;10H", seq);
|
|
11
|
+
std.debug.print(" ✓ move_cursor_to(10, 5): {s}\n", .{seq});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test "ANSI: move_cursor_up generates correct ANSI" {
|
|
15
|
+
const allocator = testing.allocator;
|
|
16
|
+
const seq = try ansi.move_cursor_up(allocator, 3);
|
|
17
|
+
defer allocator.free(seq);
|
|
18
|
+
|
|
19
|
+
try testing.expectEqualStrings("\x1b[3A", seq);
|
|
20
|
+
std.debug.print(" ✓ move_cursor_up(3): {s}\n", .{seq});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test "ANSI: move_cursor_down generates correct ANSI" {
|
|
24
|
+
const allocator = testing.allocator;
|
|
25
|
+
const seq = try ansi.move_cursor_down(allocator, 2);
|
|
26
|
+
defer allocator.free(seq);
|
|
27
|
+
|
|
28
|
+
try testing.expectEqualStrings("\x1b[2B", seq);
|
|
29
|
+
std.debug.print(" ✓ move_cursor_down(2): {s}\n", .{seq});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test "ANSI: constants are correct" {
|
|
33
|
+
try testing.expectEqualStrings("\x1b[2K", ansi.CLEAR_LINE);
|
|
34
|
+
try testing.expectEqualStrings("\x1b[?25l", ansi.HIDE_CURSOR);
|
|
35
|
+
try testing.expectEqualStrings("\x1b[?25h", ansi.SHOW_CURSOR);
|
|
36
|
+
try testing.expectEqualStrings("\x1b[s", ansi.SAVE_CURSOR);
|
|
37
|
+
try testing.expectEqualStrings("\x1b[u", ansi.RESTORE_CURSOR);
|
|
38
|
+
std.debug.print(" ✓ All ANSI constants verified\n", .{});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test "ANSI: move_cursor_to with zero coordinates" {
|
|
42
|
+
const allocator = testing.allocator;
|
|
43
|
+
const seq = try ansi.move_cursor_to(allocator, 0, 0);
|
|
44
|
+
defer allocator.free(seq);
|
|
45
|
+
|
|
46
|
+
try testing.expectEqualStrings("\x1b[0;0H", seq);
|
|
47
|
+
std.debug.print(" ✓ move_cursor_to(0, 0): {s}\n", .{seq});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test "ANSI: move_cursor_to with large coordinates" {
|
|
51
|
+
const allocator = testing.allocator;
|
|
52
|
+
const seq = try ansi.move_cursor_to(allocator, 200, 100);
|
|
53
|
+
defer allocator.free(seq);
|
|
54
|
+
|
|
55
|
+
try testing.expectEqualStrings("\x1b[100;200H", seq);
|
|
56
|
+
std.debug.print(" ✓ move_cursor_to(200, 100): {s}\n", .{seq});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test "ANSI: move_cursor_up with zero" {
|
|
60
|
+
const allocator = testing.allocator;
|
|
61
|
+
const seq = try ansi.move_cursor_up(allocator, 0);
|
|
62
|
+
defer allocator.free(seq);
|
|
63
|
+
|
|
64
|
+
try testing.expectEqualStrings("\x1b[0A", seq);
|
|
65
|
+
std.debug.print(" ✓ move_cursor_up(0): {s}\n", .{seq});
|
|
66
|
+
}
|