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,11 @@
1
+ declare module 'ffi-napi' {
2
+ export interface Library {
3
+ [key: string]: (...args: any[]) => any;
4
+ }
5
+
6
+ export function Library(
7
+ path: string,
8
+ functions: Record<string, [string, string[]]>
9
+ ): Library;
10
+ }
11
+
@@ -0,0 +1,5 @@
1
+ declare module 'ref-napi' {
2
+ export function alloc(size: number): Buffer;
3
+ export function alloc(type: string, size: number): Buffer;
4
+ }
5
+
@@ -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
+
@@ -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
+ };
@@ -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
+ }