//! Freestanding WebAssembly boundary for the DOM backend. //! //! This file deliberately contains no browser imports. JavaScript owns the //! clock and every IO operation, feeds events through this ABI, and reads one //! packed cell buffer after `pardes_frame`. Keeping that boundary flat makes //! the same module directly instantiable by the dependency-free Node harness. const std = @import("std"); const pardes = @import("pardes.zig"); const gpa = std.heap.wasm_allocator; const embedded_dump = @embedFile("embedded.dump.zon"); const input_capacity = 64 * 1024; const c_alloc_header_size = 16; comptime { if (c_alloc_header_size < @sizeOf(usize)) @compileError("C allocation header is too small"); } /// Tree-sitter's C runtime is linked into this module. Its conventional /// allocation symbols share the same wasm allocator as the Zig core and keep /// the original allocation size in a 16-byte, max-aligned prefix. export fn malloc(size_arg: usize) callconv(.c) ?*anyopaque { const size = @max(size_arg, 1); const total = std.math.add(usize, c_alloc_header_size, size) catch return null; const bytes = gpa.alignedAlloc(u8, .@"16", total) catch return null; const header: *align(16) usize = @ptrCast(bytes.ptr); header.* = total; return @ptrCast(bytes.ptr + c_alloc_header_size); } export fn calloc(count: usize, size: usize) callconv(.c) ?*anyopaque { const len = std.math.mul(usize, count, size) catch return null; const ptr = malloc(len) orelse return null; if (len != 0) { const bytes: [*]u8 = @ptrCast(ptr); @memset(bytes[0..len], 0); } return ptr; } export fn free(ptr: ?*anyopaque) callconv(.c) void { const data = ptr orelse return; const data_bytes: [*]u8 = @ptrCast(data); const base: [*]align(16) u8 = @ptrCast(@alignCast(data_bytes - c_alloc_header_size)); const header: *align(16) usize = @ptrCast(base); gpa.free(base[0..header.*]); } export fn realloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { if (ptr == null) return malloc(size); if (size == 0) { free(ptr); return null; } const data_bytes: [*]u8 = @ptrCast(ptr.?); const base: [*]align(16) u8 = @ptrCast(@alignCast(data_bytes - c_alloc_header_size)); const header: *align(16) usize = @ptrCast(base); const old_size = header.* - c_alloc_header_size; const replacement = malloc(size) orelse return null; const replacement_bytes: [*]u8 = @ptrCast(replacement); @memcpy(replacement_bytes[0..@min(old_size, size)], data_bytes[0..@min(old_size, size)]); free(ptr); return replacement; } pub const std_options: std.Options = .{ .logFn = logFn }; pub const panic = std.debug.FullPanic(panicImpl); fn logFn( comptime _: std.log.Level, comptime _: @EnumLiteral(), comptime _: []const u8, _: anytype, ) void {} fn panicImpl(_: []const u8, _: ?usize) noreturn { @trap(); } const WebCell = extern struct { // UTF-8 graphemes in the core are at most seven bytes. The eighth byte is // always zero, which also makes a debugger's memory view pleasant. text: [8]u8, fg: u32, bg: u32, attrs: u16, len: u8, flags: u8, }; comptime { if (@sizeOf(WebCell) != 20) @compileError("the JS frame ABI changed"); } const State = struct { core: *pardes.Pardes, arena: std.heap.ArenaAllocator, cells: std.ArrayList(WebCell) = .empty, effect_data: std.ArrayList(u8) = .empty, fn deinit(s: *State) void { s.core.deinit(); s.arena.deinit(); s.cells.deinit(gpa); s.effect_data.deinit(gpa); } }; var state: ?State = null; var input: [input_capacity]u8 = undefined; var last_error: u32 = 0; var effect_aux0: u32 = 0; var effect_aux1: u32 = 0; export fn pardes_init(cols_arg: u32, rows_arg: u32) u32 { if (state) |*old| old.deinit(); state = null; last_error = 0; 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| { last_error = @intFromError(err); return last_error; }; state = .{ .core = core, .arena = .init(gpa) }; core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); return 0; } export fn pardes_deinit() void { if (state) |*s| s.deinit(); state = null; } export fn pardes_last_error() u32 { return last_error; } export fn pardes_input_ptr() u32 { return @intCast(@intFromPtr(&input)); } export fn pardes_input_capacity() u32 { return input_capacity; } export fn pardes_resize(cols_arg: u32, rows_arg: u32) void { const s = &(state orelse return); 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))); if (s.core.screen_w == cols and s.core.screen_h == rows) return; s.core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); } /// `mods`: bit 0 ctrl, bit 1 alt, bit 2 shift. `len` bytes at input are the /// browser's composed text for printable keys and zero for functional keys. export fn pardes_key(cp_arg: u32, len_arg: u32, mods: u32) void { const s = &(state orelse return); if (cp_arg > std.math.maxInt(u21)) return; const len: usize = @min(len_arg, input.len); s.core.update(.{ .key = .{ .cp = @intCast(cp_arg), .text = input[0..len], .ctrl = mods & 1 != 0, .alt = mods & 2 != 0, .shift = mods & 4 != 0, } }); } export fn pardes_paste(len_arg: u32) void { const s = &(state orelse return); const len: usize = @min(len_arg, input.len); s.core.update(.{ .paste = input[0..len] }); } export fn pardes_output(pane: u32, len_arg: u32) void { const s = &(state orelse return); if (pane >= pardes.MAX_PANES) return; const len: usize = @min(len_arg, input.len); s.core.update(.{ .output = .{ .pane = @intCast(pane), .bytes = input[0..len] } }); } export fn pardes_lsp_response(id: u32, len_arg: u32) void { const s = &(state orelse return); const len: usize = @min(len_arg, input.len); s.core.update(.{ .lsp_resp = .{ .id = id, .rows = input[0..len] } }); } export fn pardes_file_changed(pane: u32, len_arg: u32) void { const s = &(state orelse return); if (pane >= pardes.MAX_PANES) return; const len: usize = @min(len_arg, input.len); s.core.update(.{ .file_changed = .{ .pane = @intCast(pane), .bytes = input[0..len] } }); } export fn pardes_set_cwd(pane: u32, len_arg: u32) void { const s = &(state orelse return); if (pane >= pardes.MAX_PANES) return; const len: usize = @min(len_arg, input.len); s.core.setCwd(@intCast(pane), input[0..len]); } export fn pardes_eof(pane: u32) void { const s = &(state orelse return); if (pane < pardes.MAX_PANES) s.core.update(.{ .eof = .{ .pane = @intCast(pane) } }); } /// Buttons and kinds are their boundary enum ordinals. Invalid values are /// ignored rather than reaching an unchecked enum cast inside wasm. export fn pardes_mouse(button_arg: u32, kind_arg: u32, col_arg: u32, row_arg: u32, mods: u32) void { const s = &(state orelse return); const button: pardes.Mouse.Button = switch (button_arg) { 0 => .left, 1 => .middle, 2 => .right, 3 => .wheel_up, 4 => .wheel_down, 5 => .wheel_left, 6 => .wheel_right, 7 => .none, else => return, }; const kind: pardes.Mouse.Kind = switch (kind_arg) { 0 => .press, 1 => .release, 2 => .motion, 3 => .drag, else => return, }; s.core.update(.{ .mouse = .{ .button = button, .kind = kind, .col = @intCast(@min(col_arg, std.math.maxInt(u16))), .row = @intCast(@min(row_arg, std.math.maxInt(u16))), .ctrl = mods & 1 != 0, } }); } export fn pardes_touch_scroll(delta: f32) void { const s = &(state orelse return); s.core.update(.{ .touch_scroll = delta }); } export fn pardes_tick() void { const s = &(state orelse return); s.core.update(.tick); } /// Render the canonical Surface into a stable, packed JS-facing buffer. /// Returns the number of cells, or zero if initialization/rendering failed. export fn pardes_frame() u32 { const s = &(state orelse return 0); _ = s.arena.reset(.retain_capacity); 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| { out.* = .{ .text = @splat(0), .fg = encodeColor(cell.style.fg), .bg = encodeColor(cell.style.bg), .attrs = encodeAttrs(cell.style), .len = if (cell.default) 1 else cell.len, .flags = @intFromBool(cell.default), }; if (cell.default) out.text[0] = ' ' else @memcpy(out.text[0..cell.len], cell.grapheme()); } return @intCast(count); } 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)); } export fn pardes_frame_cols() u32 { const s = &(state orelse return 0); return s.core.screen_w; } export fn pardes_frame_rows() u32 { const s = &(state orelse return 0); return s.core.screen_h; } export fn pardes_cursor_x() i32 { const s = &(state orelse return -1); return if (s.core.surface.cursor) |cursor| cursor.x else -1; } export fn pardes_cursor_y() i32 { const s = &(state orelse return -1); return if (s.core.surface.cursor) |cursor| cursor.y else -1; } export fn pardes_cursor_bar() u32 { const s = &(state orelse return 0); return @intFromBool(if (s.core.surface.cursor) |cursor| cursor.bar else false); } /// The browser's touch policy asks whether a gesture began on layout chrome. /// A hit returns row:col packed into u32, including snapping near a separator; /// 0xffffffff means body text and therefore one-finger scroll/tap-as-LOOK. export fn pardes_chrome_target(col_arg: u32, row_arg: u32) u32 { const s = &(state orelse return std.math.maxInt(u32)); const col: u16 = @intCast(@min(col_arg, std.math.maxInt(u16))); const row: u16 = @intCast(@min(row_arg, std.math.maxInt(u16))); const target = chromeTarget(s.core, col, row) orelse return std.math.maxInt(u32); return (@as(u32, target.row) << 16) | target.col; } // Effect codes consumed by app.mjs. Unknown/host-specific effects are still // surfaced as `pardes-io` CustomEvents, so embedding the static shell in a // 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(); effect_aux0 = 0; effect_aux1 = 0; const effect = s.core.nextEffect() orelse return 0; return switch (effect) { .spawn => |e| blk: { effect_aux0 = e.pane; putEffect(s, e.cwd.slice()); break :blk 1; }, .write => |e| blk: { effect_aux0 = e.pane; putEffect(s, e.bytes.slice()); break :blk 2; }, .resize_pty => |e| blk: { effect_aux0 = e.pane; effect_aux1 = (@as(u32, e.cols) << 16) | e.rows; break :blk 3; }, .open_link => |e| blk: { putEffect(s, e.slice()); break :blk 4; }, .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); }; break :blk 5; }, .write_dump => blk: { if (s.core.dump_out) |bytes| putEffect(s, bytes); break :blk 6; }, .set_clipboard => blk: { if (s.core.yank) |bytes| putEffect(s, bytes); break :blk 7; }, .lsp => |e| blk: { effect_aux0 = e.id; effect_aux1 = e.pane; putEffect(s, e.arg.slice()); break :blk 8; }, .watch => |e| blk: { effect_aux0 = e.pane; effect_aux1 = @intFromBool(e.on); if (e.on) if (s.core.panes[e.pane]) |pane| if (pane.file) |file| putEffect(s, file.path); break :blk 9; }, .quit => 10, }; } 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)); } export fn pardes_effect_len() u32 { const s = &(state orelse return 0); return @intCast(s.effect_data.items.len); } export fn pardes_effect_aux0() u32 { return effect_aux0; } export fn pardes_effect_aux1() u32 { return effect_aux1; } fn encodeColor(color: pardes.Color) u32 { return switch (color) { .default => 0x01000000, .index => |index| 0x02000000 | @as(u32, index), .rgb => |rgb| (@as(u32, rgb[0]) << 16) | (@as(u32, rgb[1]) << 8) | rgb[2], }; } fn encodeAttrs(style: pardes.CellStyle) u16 { var attrs: u16 = 0; attrs |= @as(u16, @intFromBool(style.bold)) << 0; attrs |= @as(u16, @intFromBool(style.dim)) << 1; attrs |= @as(u16, @intFromBool(style.italic)) << 2; attrs |= @as(u16, @intFromBool(style.blink)) << 3; attrs |= @as(u16, @intFromBool(style.reverse)) << 4; attrs |= @as(u16, @intFromBool(style.invisible)) << 5; attrs |= @as(u16, @intFromBool(style.strikethrough)) << 6; attrs |= @as(u16, @intFromEnum(style.ul)) << 8; return attrs; } fn putEffect(s: *State, bytes: []const u8) void { s.effect_data.clearRetainingCapacity(); appendEffect(s, bytes); } fn appendEffect(s: *State, bytes: []const u8) void { s.effect_data.appendSlice(gpa, bytes) catch {}; } fn putU32(s: *State, value: u32) void { var bytes: [4]u8 = undefined; std.mem.writeInt(u32, &bytes, value, .little); appendEffect(s, &bytes); } const Target = struct { col: u16, row: u16 }; fn chromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?Target { if (row < pardes.TOPBAR_H) return .{ .col = col, .row = row }; for (core.panes, 0..) |slot, id| { if (slot == null) continue; const rect = core.rects[id]; if (row == rect.y and col >= rect.x and col < rect.x + rect.w and col < rect.x + pardes.config.GUTTER) return .{ .col = col, .row = rect.y }; } for (0..core.ncol -| 1) |column| { const handle = core.col_x[column] + core.col_w[column] -| 1; if (col == handle) return .{ .col = handle, .row = row }; } for (0..core.ncol) |column| { if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue; for (0..core.col_n[column] -| 1) |index| { const rect = core.rects[core.col_terms[column][index]]; const handle = rect.y + rect.h -| 1; if (row == handle) return .{ .col = col, .row = handle }; } } for (core.panes, 0..) |slot, id| { if (slot == null) continue; const rect = core.rects[id]; if (row == rect.y and col >= rect.x and col < rect.x + rect.w) return .{ .col = col, .row = rect.y }; } for (0..core.ncol -| 1) |column| { const handle = core.col_x[column] + core.col_w[column] -| 1; if (distance(col, handle) == 1) return .{ .col = handle, .row = row }; } for (0..core.ncol) |column| { if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue; for (0..core.col_n[column] -| 1) |index| { const rect = core.rects[core.col_terms[column][index]]; const handle = rect.y + rect.h -| 1; if (distance(row, handle) == 1) return .{ .col = col, .row = handle }; } } return null; } fn distance(a: u16, b: u16) u16 { return if (a > b) a - b else b - a; }