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,82 @@
1
+ const std = @import("std");
2
+ const buffer = @import("buffer.zig");
3
+ const testing = std.testing;
4
+
5
+ test "Buffer: initialization" {
6
+ const stdout = std.fs.File{ .handle = 1 }; // stdout handle
7
+ var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
8
+ defer buf.deinit();
9
+
10
+ try testing.expect(buf.data.items.len == 0);
11
+ std.debug.print(" ✓ Buffer initialized\n", .{});
12
+ }
13
+
14
+ test "Buffer: write" {
15
+ const stdout = std.fs.File{ .handle = 1 }; // stdout handle
16
+ var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
17
+ defer buf.deinit();
18
+
19
+ try buf.write("hello");
20
+ try buf.write(" world");
21
+
22
+ try testing.expectEqualStrings("hello world", buf.data.items);
23
+ std.debug.print(" ✓ Buffer write: {s}\n", .{buf.data.items});
24
+ }
25
+
26
+ test "Buffer: clear" {
27
+ const stdout = std.fs.File{ .handle = 1 }; // stdout handle
28
+ var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
29
+ defer buf.deinit();
30
+
31
+ try buf.write("test");
32
+ buf.clear();
33
+
34
+ try testing.expect(buf.data.items.len == 0);
35
+ std.debug.print(" ✓ Buffer cleared\n", .{});
36
+ }
37
+
38
+ test "Buffer: flush" {
39
+ // Use stderr for test output to avoid conflicts with test runner
40
+ // In production, this would be stdout, but for tests stderr is safer
41
+ const stderr = std.fs.File{ .handle = 2 };
42
+ var buf = buffer.RenderBuffer.init(testing.allocator, stderr);
43
+ defer buf.deinit();
44
+
45
+ try buf.write("flush test\n");
46
+ // Flush to stderr - this should work and be visible
47
+ try buf.flush();
48
+
49
+ try testing.expect(buf.data.items.len == 0);
50
+ std.debug.print(" ✓ Buffer flushed (wrote to stderr)\n", .{});
51
+ }
52
+
53
+ test "Buffer: write large content" {
54
+ const stdout = std.fs.File{ .handle = 1 }; // stdout handle
55
+ var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
56
+ defer buf.deinit();
57
+
58
+ const large_content = "x" ** 1000;
59
+ try buf.write(large_content);
60
+
61
+ try testing.expect(buf.data.items.len == 1000);
62
+ std.debug.print(" ✓ Buffer handles large content: {} bytes\n", .{buf.data.items.len});
63
+ }
64
+
65
+ test "Buffer: multiple writes and flush" {
66
+ // Use stderr for test output
67
+ const stderr = std.fs.File{ .handle = 2 };
68
+ var buf = buffer.RenderBuffer.init(testing.allocator, stderr);
69
+ defer buf.deinit();
70
+
71
+ try buf.write("part1");
72
+ try buf.write("part2");
73
+ try buf.write("part3\n");
74
+
75
+ // Verify all parts are in buffer
76
+ try testing.expectEqualStrings("part1part2part3\n", buf.data.items);
77
+
78
+ // Flush to stderr - should be visible
79
+ try buf.flush();
80
+ try testing.expect(buf.data.items.len == 0);
81
+ std.debug.print(" ✓ Buffer multiple writes and flush works\n", .{});
82
+ }
@@ -0,0 +1,220 @@
1
+ const std = @import("std");
2
+ const diff = @import("diff.zig");
3
+ const testing = std.testing;
4
+
5
+ test "Diff: identical frames" {
6
+ const allocator = testing.allocator;
7
+
8
+ var prev = std.ArrayList([]u8){};
9
+ defer {
10
+ for (prev.items) |line| allocator.free(line);
11
+ prev.deinit(allocator);
12
+ }
13
+
14
+ var curr = std.ArrayList([]u8){};
15
+ defer {
16
+ for (curr.items) |line| allocator.free(line);
17
+ curr.deinit(allocator);
18
+ }
19
+
20
+ try prev.append(allocator, try allocator.dupe(u8, "line1"));
21
+ try prev.append(allocator, try allocator.dupe(u8, "line2"));
22
+
23
+ try curr.append(allocator, try allocator.dupe(u8, "line1"));
24
+ try curr.append(allocator, try allocator.dupe(u8, "line2"));
25
+
26
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
27
+ defer allocator.free(ops);
28
+
29
+ try testing.expect(ops.len == 2);
30
+ for (ops) |op| {
31
+ try testing.expect(op == .no_change);
32
+ }
33
+ std.debug.print(" ✓ Diff identical frames: {} no_change ops\n", .{ops.len});
34
+ }
35
+
36
+ test "Diff: changed line" {
37
+ const allocator = testing.allocator;
38
+
39
+ var prev = std.ArrayList([]u8){};
40
+ defer {
41
+ for (prev.items) |line| allocator.free(line);
42
+ prev.deinit(allocator);
43
+ }
44
+
45
+ var curr = std.ArrayList([]u8){};
46
+ defer {
47
+ for (curr.items) |line| allocator.free(line);
48
+ curr.deinit(allocator);
49
+ }
50
+
51
+ try prev.append(allocator, try allocator.dupe(u8, "line1"));
52
+ try prev.append(allocator, try allocator.dupe(u8, "line2"));
53
+
54
+ try curr.append(allocator, try allocator.dupe(u8, "line1"));
55
+ try curr.append(allocator, try allocator.dupe(u8, "line2_changed"));
56
+
57
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
58
+ defer allocator.free(ops);
59
+
60
+ try testing.expect(ops.len == 2);
61
+ try testing.expect(ops[0] == .no_change);
62
+ try testing.expect(ops[1] == .update_line);
63
+ if (ops[1] == .update_line) {
64
+ try testing.expectEqual(@as(u32, 1), ops[1].update_line.line);
65
+ try testing.expectEqualStrings("line2_changed", ops[1].update_line.content);
66
+ }
67
+ std.debug.print(" ✓ Diff changed line detected\n", .{});
68
+ }
69
+
70
+ test "Diff: inserted line" {
71
+ const allocator = testing.allocator;
72
+
73
+ var prev = std.ArrayList([]u8){};
74
+ defer {
75
+ for (prev.items) |line| allocator.free(line);
76
+ prev.deinit(allocator);
77
+ }
78
+
79
+ var curr = std.ArrayList([]u8){};
80
+ defer {
81
+ for (curr.items) |line| allocator.free(line);
82
+ curr.deinit(allocator);
83
+ }
84
+
85
+ try prev.append(allocator, try allocator.dupe(u8, "line1"));
86
+
87
+ try curr.append(allocator, try allocator.dupe(u8, "line1"));
88
+ try curr.append(allocator, try allocator.dupe(u8, "line2"));
89
+
90
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
91
+ defer allocator.free(ops);
92
+
93
+ try testing.expect(ops.len == 2);
94
+ try testing.expect(ops[0] == .no_change);
95
+ try testing.expect(ops[1] == .insert_line);
96
+ if (ops[1] == .insert_line) {
97
+ try testing.expectEqual(@as(u32, 1), ops[1].insert_line.line);
98
+ try testing.expectEqualStrings("line2", ops[1].insert_line.content);
99
+ }
100
+ std.debug.print(" ✓ Diff inserted line detected\n", .{});
101
+ }
102
+
103
+ test "Diff: deleted line" {
104
+ const allocator = testing.allocator;
105
+
106
+ var prev = std.ArrayList([]u8){};
107
+ defer {
108
+ for (prev.items) |line| allocator.free(line);
109
+ prev.deinit(allocator);
110
+ }
111
+
112
+ var curr = std.ArrayList([]u8){};
113
+ defer {
114
+ for (curr.items) |line| allocator.free(line);
115
+ curr.deinit(allocator);
116
+ }
117
+
118
+ try prev.append(allocator, try allocator.dupe(u8, "line1"));
119
+ try prev.append(allocator, try allocator.dupe(u8, "line2"));
120
+
121
+ try curr.append(allocator, try allocator.dupe(u8, "line1"));
122
+
123
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
124
+ defer allocator.free(ops);
125
+
126
+ try testing.expect(ops.len == 2);
127
+ try testing.expect(ops[0] == .no_change);
128
+ try testing.expect(ops[1] == .delete_line);
129
+ if (ops[1] == .delete_line) {
130
+ try testing.expectEqual(@as(u32, 1), ops[1].delete_line);
131
+ }
132
+ std.debug.print(" ✓ Diff deleted line detected\n", .{});
133
+ }
134
+
135
+ test "Diff: multiple changes" {
136
+ const allocator = testing.allocator;
137
+
138
+ var prev = std.ArrayList([]u8){};
139
+ defer {
140
+ for (prev.items) |line| allocator.free(line);
141
+ prev.deinit(allocator);
142
+ }
143
+
144
+ var curr = std.ArrayList([]u8){};
145
+ defer {
146
+ for (curr.items) |line| allocator.free(line);
147
+ curr.deinit(allocator);
148
+ }
149
+
150
+ try prev.append(allocator, try allocator.dupe(u8, "line1"));
151
+ try prev.append(allocator, try allocator.dupe(u8, "line2"));
152
+ try prev.append(allocator, try allocator.dupe(u8, "line3"));
153
+
154
+ try curr.append(allocator, try allocator.dupe(u8, "line1_changed"));
155
+ try curr.append(allocator, try allocator.dupe(u8, "line2"));
156
+ try curr.append(allocator, try allocator.dupe(u8, "line3"));
157
+ try curr.append(allocator, try allocator.dupe(u8, "line4"));
158
+
159
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
160
+ defer allocator.free(ops);
161
+
162
+ try testing.expect(ops.len == 4);
163
+ try testing.expect(ops[0] == .update_line);
164
+ try testing.expect(ops[1] == .no_change);
165
+ try testing.expect(ops[2] == .no_change);
166
+ try testing.expect(ops[3] == .insert_line);
167
+ std.debug.print(" ✓ Diff multiple changes: {} ops\n", .{ops.len});
168
+ }
169
+
170
+ test "Diff: empty frames" {
171
+ const allocator = testing.allocator;
172
+
173
+ var prev = std.ArrayList([]u8){};
174
+ defer {
175
+ for (prev.items) |line| allocator.free(line);
176
+ prev.deinit(allocator);
177
+ }
178
+
179
+ var curr = std.ArrayList([]u8){};
180
+ defer {
181
+ for (curr.items) |line| allocator.free(line);
182
+ curr.deinit(allocator);
183
+ }
184
+
185
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
186
+ defer allocator.free(ops);
187
+
188
+ try testing.expect(ops.len == 0);
189
+ std.debug.print(" ✓ Diff empty frames: {} ops\n", .{ops.len});
190
+ }
191
+
192
+ test "Diff: completely different frames" {
193
+ const allocator = testing.allocator;
194
+
195
+ var prev = std.ArrayList([]u8){};
196
+ defer {
197
+ for (prev.items) |line| allocator.free(line);
198
+ prev.deinit(allocator);
199
+ }
200
+
201
+ var curr = std.ArrayList([]u8){};
202
+ defer {
203
+ for (curr.items) |line| allocator.free(line);
204
+ curr.deinit(allocator);
205
+ }
206
+
207
+ try prev.append(allocator, try allocator.dupe(u8, "old1"));
208
+ try prev.append(allocator, try allocator.dupe(u8, "old2"));
209
+
210
+ try curr.append(allocator, try allocator.dupe(u8, "new1"));
211
+ try curr.append(allocator, try allocator.dupe(u8, "new2"));
212
+
213
+ const ops = try diff.diff_frames(prev.items, curr.items, allocator);
214
+ defer allocator.free(ops);
215
+
216
+ try testing.expect(ops.len == 2);
217
+ try testing.expect(ops[0] == .update_line);
218
+ try testing.expect(ops[1] == .update_line);
219
+ std.debug.print(" ✓ Diff completely different frames: {} update ops\n", .{ops.len});
220
+ }
@@ -0,0 +1,76 @@
1
+ // Integration tests that test multiple modules together
2
+ const std = @import("std");
3
+ const region = @import("region.zig");
4
+ const testing = std.testing;
5
+
6
+ test "Integration: region with diff and throttle" {
7
+ const allocator = testing.allocator;
8
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
9
+
10
+ var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
11
+ r.disable_rendering = true; // Disable actual rendering in tests
12
+ defer r.deinit();
13
+
14
+ // Set initial lines
15
+ try r.set_line(1, "initial line 1");
16
+ try r.set_line(2, "initial line 2");
17
+
18
+ // Change one line
19
+ try r.set_line(1, "updated line 1");
20
+
21
+ // Verify pending frame has updates
22
+ try testing.expectEqualStrings("updated line 1", r.pending_frame.items[0]);
23
+ try testing.expectEqualStrings("initial line 2", r.pending_frame.items[1]);
24
+
25
+ std.debug.print(" ✓ Integration: region diff and throttle work together\n", .{});
26
+ }
27
+
28
+ test "Integration: region expansion with multiple operations" {
29
+ const allocator = testing.allocator;
30
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
31
+
32
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
33
+ r.disable_rendering = true; // Disable actual rendering in tests
34
+ defer r.deinit();
35
+
36
+ // Start with 1 line, expand to 5
37
+ try r.set_line(1, "line1");
38
+ try r.set_line(2, "line2");
39
+ try r.set_line(3, "line3");
40
+ try r.set_line(4, "line4");
41
+ try r.set_line(5, "line5");
42
+
43
+ try testing.expect(r.height >= 5);
44
+ try testing.expect(r.pending_frame.items.len >= 5);
45
+
46
+ // Verify all lines are correct
47
+ try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
48
+ try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
49
+ try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
50
+ try testing.expectEqualStrings("line4", r.pending_frame.items[3]);
51
+ try testing.expectEqualStrings("line5", r.pending_frame.items[4]);
52
+
53
+ std.debug.print(" ✓ Integration: region expansion with multiple operations\n", .{});
54
+ }
55
+
56
+ test "Integration: set then set_line" {
57
+ const allocator = testing.allocator;
58
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
59
+
60
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
61
+ r.disable_rendering = true; // Disable actual rendering in tests
62
+ defer r.deinit();
63
+
64
+ // Set entire content
65
+ try r.set("line1\nline2\nline3");
66
+
67
+ // Then update individual line
68
+ try r.set_line(2, "line2_updated");
69
+
70
+ try testing.expect(r.pending_frame.items.len >= 3);
71
+ try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
72
+ try testing.expectEqualStrings("line2_updated", r.pending_frame.items[1]);
73
+ try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
74
+
75
+ std.debug.print(" ✓ Integration: set then set_line works correctly\n", .{});
76
+ }
@@ -0,0 +1,191 @@
1
+ const std = @import("std");
2
+ const region = @import("region.zig");
3
+ const testing = std.testing;
4
+
5
+ test "Region: initialization" {
6
+ const allocator = testing.allocator;
7
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
8
+
9
+ var r = try region.Region.init(allocator, 0, 0, 80, 5, stdout);
10
+ r.disable_rendering = true; // Disable actual rendering in tests
11
+ defer r.deinit();
12
+
13
+ try testing.expectEqual(@as(u32, 0), r.x);
14
+ try testing.expectEqual(@as(u32, 0), r.y);
15
+ try testing.expectEqual(@as(u32, 80), r.width);
16
+ try testing.expectEqual(@as(u32, 5), r.height);
17
+ try testing.expect(r.pending_frame.items.len == 5);
18
+ try testing.expect(r.previous_frame.items.len == 5);
19
+ std.debug.print(" ✓ Region initialized: {}x{} at ({}, {})\n", .{ r.width, r.height, r.x, r.y });
20
+ }
21
+
22
+ test "Region: set_line expands automatically" {
23
+ const allocator = testing.allocator;
24
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
25
+
26
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
27
+ r.disable_rendering = true; // Disable actual rendering in tests
28
+ defer r.deinit();
29
+
30
+ // Set line 1 (should work)
31
+ try r.set_line(1, "line1");
32
+ try testing.expectEqual(@as(u32, 1), r.height);
33
+
34
+ // Set line 5 (should expand)
35
+ try r.set_line(5, "line5");
36
+ try testing.expect(r.height >= 5);
37
+ try testing.expect(r.pending_frame.items.len >= 5);
38
+ std.debug.print(" ✓ Region expanded from 1 to {} lines\n", .{r.height});
39
+ }
40
+
41
+ test "Region: set_line rejects line 0" {
42
+ const allocator = testing.allocator;
43
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
44
+
45
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
46
+ r.disable_rendering = true; // Disable actual rendering in tests
47
+ defer r.deinit();
48
+
49
+ const result = r.set_line(0, "invalid");
50
+ try testing.expectError(error.InvalidLineNumber, result);
51
+ std.debug.print(" ✓ Region correctly rejects line 0\n", .{});
52
+ }
53
+
54
+ test "Region: set with newlines" {
55
+ const allocator = testing.allocator;
56
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
57
+
58
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
59
+ r.disable_rendering = true; // Disable actual rendering in tests
60
+ defer r.deinit();
61
+
62
+ try r.set("line1\nline2\nline3");
63
+ try testing.expect(r.height >= 3);
64
+ try testing.expect(r.pending_frame.items.len >= 3);
65
+ try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
66
+ try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
67
+ try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
68
+ std.debug.print(" ✓ Region set with {} lines\n", .{r.pending_frame.items.len});
69
+ }
70
+
71
+ test "Region: expand_to" {
72
+ const allocator = testing.allocator;
73
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
74
+
75
+ var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
76
+ r.disable_rendering = true; // Disable actual rendering in tests
77
+ defer r.deinit();
78
+
79
+ try r.expand_to(10);
80
+ try testing.expectEqual(@as(u32, 10), r.height);
81
+ try testing.expect(r.pending_frame.items.len == 10);
82
+ try testing.expect(r.previous_frame.items.len == 10);
83
+ std.debug.print(" ✓ Region expanded to {} lines\n", .{r.height});
84
+ }
85
+
86
+ test "Region: clear_line" {
87
+ const allocator = testing.allocator;
88
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
89
+
90
+ var r = try region.Region.init(allocator, 0, 0, 80, 3, stdout);
91
+ r.disable_rendering = true; // Disable actual rendering in tests
92
+ defer r.deinit();
93
+
94
+ try r.set_line(2, "test");
95
+ try r.clear_line(2);
96
+ try testing.expectEqualStrings("", r.pending_frame.items[1]);
97
+ std.debug.print(" ✓ Region clear_line works\n", .{});
98
+ }
99
+
100
+ test "Region: throttle configuration" {
101
+ const allocator = testing.allocator;
102
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
103
+
104
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
105
+ r.disable_rendering = true; // Disable actual rendering in tests
106
+ defer r.deinit();
107
+
108
+ r.set_throttle_fps(30);
109
+ try testing.expectEqual(@as(u32, 30), r.throttle_state.fps);
110
+ std.debug.print(" ✓ Region throttle set to {} FPS\n", .{r.throttle_state.fps});
111
+ }
112
+
113
+ test "Region: set_line with empty string" {
114
+ const allocator = testing.allocator;
115
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
116
+
117
+ var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
118
+ r.disable_rendering = true; // Disable actual rendering in tests
119
+ defer r.deinit();
120
+
121
+ try r.set_line(1, "test");
122
+ try r.set_line(1, "");
123
+ try testing.expectEqualStrings("", r.pending_frame.items[0]);
124
+ std.debug.print(" ✓ Region set_line with empty string works\n", .{});
125
+ }
126
+
127
+ test "Region: set with empty content" {
128
+ const allocator = testing.allocator;
129
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
130
+
131
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
132
+ r.disable_rendering = true; // Disable actual rendering in tests
133
+ defer r.deinit();
134
+
135
+ try r.set("");
136
+ try testing.expect(r.pending_frame.items.len >= 1);
137
+ std.debug.print(" ✓ Region set with empty content works\n", .{});
138
+ }
139
+
140
+ test "Region: set with single line (no newline)" {
141
+ const allocator = testing.allocator;
142
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
143
+
144
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
145
+ r.disable_rendering = true; // Disable actual rendering in tests
146
+ defer r.deinit();
147
+
148
+ try r.set("single line");
149
+ try testing.expect(r.pending_frame.items.len >= 1);
150
+ try testing.expectEqualStrings("single line", r.pending_frame.items[0]);
151
+ std.debug.print(" ✓ Region set with single line works\n", .{});
152
+ }
153
+
154
+ test "Region: multiple set_line calls" {
155
+ const allocator = testing.allocator;
156
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
157
+
158
+ var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
159
+ r.disable_rendering = true; // Disable actual rendering in tests
160
+ defer r.deinit();
161
+
162
+ try r.set_line(1, "line1");
163
+ try r.set_line(2, "line2");
164
+ try r.set_line(3, "line3");
165
+
166
+ try testing.expect(r.height >= 3);
167
+ try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
168
+ try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
169
+ try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
170
+ std.debug.print(" ✓ Multiple set_line calls work correctly\n", .{});
171
+ }
172
+
173
+ test "Region: clear all lines" {
174
+ const allocator = testing.allocator;
175
+ const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
176
+
177
+ var r = try region.Region.init(allocator, 0, 0, 80, 3, stdout);
178
+ r.disable_rendering = true; // Disable actual rendering in tests
179
+ defer r.deinit();
180
+
181
+ try r.set_line(1, "line1");
182
+ try r.set_line(2, "line2");
183
+ try r.set_line(3, "line3");
184
+ try r.clear();
185
+
186
+ // All lines should be empty
187
+ for (r.pending_frame.items) |line| {
188
+ try testing.expectEqualStrings("", line);
189
+ }
190
+ std.debug.print(" ✓ Region clear works for all lines\n", .{});
191
+ }
@@ -0,0 +1,27 @@
1
+ // Test runner that imports and runs all test modules
2
+ // Zig's built-in test runner will discover all test functions
3
+ const std = @import("std");
4
+
5
+ // Import all test modules so their tests are discovered
6
+ // Zig's test runner automatically discovers all test functions in imported modules
7
+ // We use unique names to avoid duplicate declaration errors
8
+ const _test_ansi = @import("test_ansi.zig");
9
+ const _test_throttle = @import("test_throttle.zig");
10
+ const _test_buffer = @import("test_buffer.zig");
11
+ const _test_diff = @import("test_diff.zig");
12
+ const _test_region = @import("test_region.zig");
13
+ const _test_integration = @import("test_integration.zig");
14
+
15
+ // Suppress unused variable warnings
16
+ comptime {
17
+ _ = _test_ansi;
18
+ _ = _test_throttle;
19
+ _ = _test_buffer;
20
+ _ = _test_diff;
21
+ _ = _test_region;
22
+ _ = _test_integration;
23
+ }
24
+
25
+ // This file serves as the root for test discovery
26
+ // All test functions in imported modules will be run
27
+ // Each test prints its own output using std.debug.print() for visibility
@@ -0,0 +1,59 @@
1
+ const std = @import("std");
2
+ const throttle = @import("throttle.zig");
3
+ const testing = std.testing;
4
+
5
+ test "Throttle: initialization" {
6
+ const t = throttle.Throttle.init(60);
7
+ try testing.expectEqual(@as(u32, 60), t.fps);
8
+ try testing.expect(t.min_frame_interval > 0);
9
+ std.debug.print(" ✓ Throttle initialized: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
10
+ }
11
+
12
+ test "Throttle: set_fps" {
13
+ var t = throttle.Throttle.init(30);
14
+ t.set_fps(120);
15
+ try testing.expectEqual(@as(u32, 120), t.fps);
16
+ std.debug.print(" ✓ Throttle FPS changed to: {}\n", .{t.fps});
17
+ }
18
+
19
+ test "Throttle: should_render allows first frame" {
20
+ var t = throttle.Throttle.init(60);
21
+ // First call should always allow render
22
+ const should = t.should_render();
23
+ try testing.expect(should);
24
+ std.debug.print(" ✓ First frame allowed\n", .{});
25
+ }
26
+
27
+ test "Throttle: should_render respects interval" {
28
+ var t = throttle.Throttle.init(1000); // 1 FPS = 1 second interval
29
+ _ = t.should_render(); // First frame
30
+
31
+ // Immediately after, should not render
32
+ const should = t.should_render();
33
+ try testing.expect(!should);
34
+ std.debug.print(" ✓ Throttle correctly blocks immediate second frame\n", .{});
35
+ }
36
+
37
+ test "Throttle: time_until_next_frame" {
38
+ var t = throttle.Throttle.init(10); // 10 FPS = 100ms interval
39
+ _ = t.should_render();
40
+
41
+ const remaining = t.time_until_next_frame();
42
+ try testing.expect(remaining > 0);
43
+ try testing.expect(remaining <= 100_000_000); // Should be <= 100ms in nanoseconds
44
+ std.debug.print(" ✓ Time until next frame: {}ns\n", .{remaining});
45
+ }
46
+
47
+ test "Throttle: very high FPS" {
48
+ const t = throttle.Throttle.init(1000); // 1000 FPS
49
+ try testing.expectEqual(@as(u32, 1000), t.fps);
50
+ try testing.expect(t.min_frame_interval > 0);
51
+ std.debug.print(" ✓ Throttle handles high FPS: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
52
+ }
53
+
54
+ test "Throttle: very low FPS" {
55
+ const t = throttle.Throttle.init(1); // 1 FPS
56
+ try testing.expectEqual(@as(u32, 1), t.fps);
57
+ try testing.expect(t.min_frame_interval == 1_000_000_000); // 1 second
58
+ std.debug.print(" ✓ Throttle handles low FPS: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
59
+ }
@@ -0,0 +1,38 @@
1
+ // Throttling for frame rate limiting
2
+ const std = @import("std");
3
+
4
+ pub const Throttle = struct {
5
+ last_frame_time: i64,
6
+ min_frame_interval: i64, // nanoseconds
7
+ fps: u32,
8
+
9
+ pub fn init(fps: u32) Throttle {
10
+ const interval_ns = @divTrunc(1_000_000_000, @as(i64, fps));
11
+ return .{
12
+ .last_frame_time = 0,
13
+ .min_frame_interval = interval_ns,
14
+ .fps = fps,
15
+ };
16
+ }
17
+
18
+ pub fn set_fps(self: *Throttle, fps: u32) void {
19
+ self.fps = fps;
20
+ self.min_frame_interval = @divTrunc(1_000_000_000, @as(i64, fps));
21
+ }
22
+
23
+ pub fn should_render(self: *Throttle) bool {
24
+ const now = @as(i64, @intCast(std.time.nanoTimestamp()));
25
+ if (now - self.last_frame_time >= self.min_frame_interval) {
26
+ self.last_frame_time = now;
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ pub fn time_until_next_frame(self: *Throttle) i64 {
33
+ const now = @as(i64, @intCast(std.time.nanoTimestamp()));
34
+ const elapsed = now - self.last_frame_time;
35
+ const remaining = self.min_frame_interval - elapsed;
36
+ return if (remaining > 0) @as(i64, @intCast(remaining)) else 0;
37
+ }
38
+ };