summaryrefslogtreecommitdiff
path: root/src/web.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 10:41:33 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch)
tree91fce278abc85e4f7491de8b5f02184c8a2d5a56 /src/web.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'src/web.zig')
-rw-r--r--src/web.zig165
1 files changed, 122 insertions, 43 deletions
diff --git a/src/web.zig b/src/web.zig
index 43b6c96d..e17e147e 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -96,14 +96,45 @@ comptime {
const State = struct {
core: *pardes.Pardes,
arena: std.heap.ArenaAllocator,
- cells: std.ArrayList(WebCell) = .empty,
- effect_data: std.ArrayList(u8) = .empty,
+ cells: []WebCell = &.{},
+ frame_len: usize = 0,
+ frame_cols: u16 = 0,
+ frame_rows: u16 = 0,
+ effect_data: []u8 = &.{},
+ effect_len: usize = 0,
+
+ fn init(cols: u16, rows: u16) !State {
+ const allocs = pardes.allocators.init(gpa);
+ errdefer pardes.allocators.deinit();
+ pardes.image.start(std.Io.failing, allocs.image);
+ errdefer pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.start(allocs.pdf);
+ errdefer if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.start(allocs.tree_sitter);
+ errdefer pardes.syntax.stop();
+
+ const core = try pardes.Pardes.initFromDump(allocs.pardes, .{
+ .cols = cols,
+ .rows = rows,
+ .image_allocator = allocs.image,
+ .pdf_allocator = allocs.pdf,
+ .tree_sitter_allocator = allocs.tree_sitter,
+ }, embedded_dump);
+ return .{
+ .core = core,
+ .arena = .init(allocs.frame),
+ };
+ }
fn deinit(s: *State) void {
- s.core.deinit();
+ if (s.cells.len > 0) gpa.free(s.cells);
+ if (s.effect_data.len > 0) gpa.free(s.effect_data);
s.arena.deinit();
- s.cells.deinit(gpa);
- s.effect_data.deinit(gpa);
+ s.core.deinit();
+ pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.stop();
+ pardes.allocators.deinit();
}
};
@@ -120,12 +151,11 @@ export fn pardes_init(cols_arg: u32, rows_arg: u32) u32 {
const cols: u16 = @intCast(std.math.clamp(cols_arg, 1, std.math.maxInt(u16)));
const rows: u16 = @intCast(std.math.clamp(rows_arg, 1, std.math.maxInt(u16)));
- const core = pardes.Pardes.initFromDump(gpa, .{ .cols = cols, .rows = rows }, embedded_dump) catch |err| {
+ state = State.init(cols, rows) catch |err| {
last_error = @intFromError(err);
return last_error;
};
- state = .{ .core = core, .arena = .init(gpa) };
- core.update(.{ .resize = .{ .cols = cols, .rows = rows } });
+ state.?.core.update(.{ .resize = .{ .cols = cols, .rows = rows } });
return 0;
}
@@ -284,16 +314,33 @@ export fn pardes_tick() void {
export fn pardes_frame() u32 {
const s = &(state orelse return 0);
_ = s.arena.reset(.retain_capacity);
+ s.frame_len = 0;
+ s.frame_cols = 0;
+ s.frame_rows = 0;
const surface = s.core.render(s.arena.allocator()) catch |err| {
last_error = @intFromError(err);
return 0;
};
const count: usize = @as(usize, surface.cols) * surface.rows;
- s.cells.resize(gpa, count) catch |err| {
- last_error = @intFromError(err);
- return 0;
- };
- for (surface.cells, s.cells.items) |cell, *out| {
+ if (count != s.cells.len) {
+ if (count == 0) {
+ if (s.cells.len > 0) gpa.free(s.cells);
+ s.cells = &.{};
+ } else {
+ const resized = if (s.cells.len == 0)
+ gpa.alloc(WebCell, count)
+ else
+ gpa.realloc(s.cells, count);
+ s.cells = resized catch |err| {
+ last_error = @intFromError(err);
+ return 0;
+ };
+ }
+ }
+ s.frame_len = count;
+ s.frame_cols = surface.cols;
+ s.frame_rows = surface.rows;
+ for (surface.cells, s.cells[0..count]) |cell, *out| {
out.* = .{
.text = @splat(0),
.fg = encodeColor(cell.style.fg),
@@ -309,17 +356,17 @@ export fn pardes_frame() u32 {
export fn pardes_frame_ptr() u32 {
const s = &(state orelse return 0);
- return if (s.cells.items.len == 0) 0 else @intCast(@intFromPtr(s.cells.items.ptr));
+ return if (s.frame_len == 0) 0 else @intCast(@intFromPtr(s.cells.ptr));
}
export fn pardes_frame_cols() u32 {
const s = &(state orelse return 0);
- return s.core.screen_w;
+ return s.frame_cols;
}
export fn pardes_frame_rows() u32 {
const s = &(state orelse return 0);
- return s.core.screen_h;
+ return s.frame_rows;
}
export fn pardes_cursor_x() i32 {
@@ -353,7 +400,7 @@ export fn pardes_chrome_target(col_arg: u32, row_arg: u32) u32 {
// process-capable host does not require another wasm boundary.
export fn pardes_effect_next() u32 {
const s = &(state orelse return 0);
- s.effect_data.clearRetainingCapacity();
+ clearEffect(s);
effect_aux0 = 0;
effect_aux1 = 0;
const effect = s.core.nextEffect() orelse return 0;
@@ -379,11 +426,8 @@ export fn pardes_effect_next() u32 {
},
.save_file => |e| blk: {
effect_aux0 = e.pane;
- if (s.core.panes[e.pane]) |pane| if (pane.file) |file| {
- putU32(s, @intCast(file.path.len));
- appendEffect(s, file.path);
- appendEffect(s, file.content);
- };
+ if (s.core.panes[e.pane]) |pane| if (pane.file) |file|
+ putSaveEffect(s, file.path, file.content);
break :blk 5;
},
.write_dump => blk: {
@@ -402,18 +446,10 @@ export fn pardes_effect_next() u32 {
},
// Process-capable embeddings receive exact command/cwd/input bytes:
// [command_len][cwd_len][count][input lengths...][all payloads...].
- // The ordinary static browser surfaces this as `pardes-io` and stays
- // inert because it has no process authority.
.pipe => |e| blk: {
effect_aux0 = e.id;
const request = s.core.pipeRequest(e.id) orelse break :blk 12;
- putU32(s, @intCast(request.command.len));
- putU32(s, @intCast(request.cwd.len));
- putU32(s, @intCast(request.inputs.len));
- for (request.inputs) |selection| putU32(s, @intCast(selection.bytes.len));
- appendEffect(s, request.command);
- appendEffect(s, request.cwd);
- for (request.inputs) |selection| appendEffect(s, selection.bytes);
+ putPipeEffect(s, request);
break :blk 12;
},
.watch => |e| blk: {
@@ -422,8 +458,6 @@ export fn pardes_effect_next() u32 {
if (e.on) if (s.core.panes[e.pane]) |pane| if (pane.file) |file| putEffect(s, file.path);
break :blk 9;
},
- // Browser hosts cannot create this file, but surface the request like
- // the other host-specific IO effects instead of performing IO here.
.new_file => |e| blk: {
effect_aux0 = e.pane;
effect_aux1 = e.serial;
@@ -435,12 +469,12 @@ export fn pardes_effect_next() u32 {
export fn pardes_effect_ptr() u32 {
const s = &(state orelse return 0);
- return if (s.effect_data.items.len == 0) 0 else @intCast(@intFromPtr(s.effect_data.items.ptr));
+ return if (s.effect_len == 0) 0 else @intCast(@intFromPtr(s.effect_data.ptr));
}
export fn pardes_effect_len() u32 {
const s = &(state orelse return 0);
- return @intCast(s.effect_data.items.len);
+ return @intCast(s.effect_len);
}
export fn pardes_effect_aux0() u32 {
@@ -472,17 +506,62 @@ fn encodeAttrs(style: pardes.CellStyle) u16 {
return attrs;
}
+fn clearEffect(s: *State) void {
+ if (s.effect_data.len > 0) gpa.free(s.effect_data);
+ s.effect_data = &.{};
+ s.effect_len = 0;
+}
+
+fn allocEffect(s: *State, len: usize) ?[]u8 {
+ std.debug.assert(s.effect_data.len == 0 and s.effect_len == 0);
+ if (len == 0) return s.effect_data;
+ s.effect_data = gpa.alloc(u8, len) catch return null;
+ s.effect_len = len;
+ return s.effect_data;
+}
+
fn putEffect(s: *State, bytes: []const u8) void {
- s.effect_data.clearRetainingCapacity();
- appendEffect(s, bytes);
+ const out = allocEffect(s, bytes.len) orelse return;
+ @memcpy(out, bytes);
+}
+
+fn writeEffectU32(out: []u8, at: *usize, value: u32) void {
+ const word: *[4]u8 = @ptrCast(out[at.*..].ptr);
+ std.mem.writeInt(u32, word, value, .little);
+ at.* += 4;
}
-fn appendEffect(s: *State, bytes: []const u8) void {
- s.effect_data.appendSlice(gpa, bytes) catch {};
+fn putSaveEffect(s: *State, path: []const u8, content: []const u8) void {
+ var total = std.math.add(usize, 4, path.len) catch return;
+ total = std.math.add(usize, total, content.len) catch return;
+ const out = allocEffect(s, total) orelse return;
+ var at: usize = 0;
+ writeEffectU32(out, &at, @intCast(path.len));
+ @memcpy(out[at..][0..path.len], path);
+ at += path.len;
+ @memcpy(out[at..][0..content.len], content);
}
-fn putU32(s: *State, value: u32) void {
- var bytes: [4]u8 = undefined;
- std.mem.writeInt(u32, &bytes, value, .little);
- appendEffect(s, &bytes);
+fn putPipeEffect(s: *State, request: anytype) void {
+ var total = std.math.mul(usize, request.inputs.len, 4) catch return;
+ total = std.math.add(usize, total, 12) catch return;
+ total = std.math.add(usize, total, request.command.len) catch return;
+ total = std.math.add(usize, total, request.cwd.len) catch return;
+ for (request.inputs) |selection|
+ total = std.math.add(usize, total, selection.bytes.len) catch return;
+ const out = allocEffect(s, total) orelse return;
+ var at: usize = 0;
+ writeEffectU32(out, &at, @intCast(request.command.len));
+ writeEffectU32(out, &at, @intCast(request.cwd.len));
+ writeEffectU32(out, &at, @intCast(request.inputs.len));
+ for (request.inputs) |selection| writeEffectU32(out, &at, @intCast(selection.bytes.len));
+ @memcpy(out[at..][0..request.command.len], request.command);
+ at += request.command.len;
+ @memcpy(out[at..][0..request.cwd.len], request.cwd);
+ at += request.cwd.len;
+ for (request.inputs) |selection| {
+ @memcpy(out[at..][0..selection.bytes.len], selection.bytes);
+ at += selection.bytes.len;
+ }
+ std.debug.assert(at == out.len);
}