summaryrefslogtreecommitdiff
path: root/src/gui/gui.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/gui/gui.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig475
1 files changed, 334 insertions, 141 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 39d8c575..951564f0 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -560,6 +560,19 @@ const Msg = union(enum) {
/// a pardes launched inside this one sent us a builtin command line (see
/// lookThread); gpa-owned, like `output` bytes
command: []u8,
+
+ fn deinit(m: Msg, gpa: std.mem.Allocator, lsp_allocator: std.mem.Allocator) void {
+ switch (m) {
+ .output => |o| gpa.free(o.bytes),
+ .lsp => |l| lsp_allocator.free(l.rows),
+ .pipe => |response_value| {
+ var response = response_value;
+ response.deinit(gpa);
+ },
+ .command => |line| gpa.free(line),
+ .eof, .files_changed => {},
+ }
+ }
};
/// The files on open panes, watched through ONE inotify instance. Same shape
@@ -590,54 +603,133 @@ const LspJob = struct {
}
};
+const LspWorkers = struct {
+ active: std.atomic.Value(usize) = .init(0),
+
+ fn start(workers: *LspWorkers) void {
+ _ = workers.active.fetchAdd(1, .monotonic);
+ }
+
+ fn finish(workers: *LspWorkers) void {
+ _ = workers.active.fetchSub(1, .release);
+ }
+
+ fn wait(workers: *LspWorkers) void {
+ var spins: u8 = 0;
+ while (workers.active.load(.acquire) != 0) {
+ spins +%= 1;
+ if (spins == 0) std.Thread.yield() catch {} else std.atomic.spinLoopHint();
+ }
+ }
+};
+
+const max_pipe_tasks = 16;
+
const PipeTask = struct {
id: u32,
future: std.Io.Future(anyerror!void),
};
+const PipeTasks = struct {
+ items: [max_pipe_tasks]PipeTask = undefined,
+ len: usize = 0,
+
+ fn add(tasks: *PipeTasks, task: PipeTask) bool {
+ if (tasks.len == tasks.items.len) return false;
+ tasks.items[tasks.len] = task;
+ tasks.len += 1;
+ return true;
+ }
+
+ fn finish(tasks: *PipeTasks, io: std.Io, id: u32) void {
+ for (tasks.items[0..tasks.len], 0..) |*task, i| if (task.id == id) {
+ task.future.await(io) catch {};
+ tasks.len -= 1;
+ std.mem.copyForwards(PipeTask, tasks.items[i..tasks.len], tasks.items[i + 1 .. tasks.len + 1]);
+ return;
+ };
+ }
+
+ fn cancelAll(tasks: *PipeTasks, io: std.Io) void {
+ for (tasks.items[0..tasks.len]) |*task| task.future.cancel(io) catch {};
+ tasks.len = 0;
+ }
+};
+
+const queue_capacity = 512;
+
+const MessageBatch = struct {
+ items: [queue_capacity]Msg = undefined,
+ len: usize = 0,
+
+ fn slice(batch: *MessageBatch) []Msg {
+ return batch.items[0..batch.len];
+ }
+};
+
const Queue = struct {
gpa: std.mem.Allocator,
+ lsp_allocator: std.mem.Allocator,
+ lsp_workers: *LspWorkers,
sdl_wake: bool, // wake a blocking SDL_WaitEventTimeout on cross-thread push
// 0.16 has no std.Thread.Mutex; critical sections here are a few
- // instructions, so spinning on the lock-free std.atomic.Mutex is enough
+ // instructions, so spinning on the lock-free std.atomic.Mutex is enough.
mutex: std.atomic.Mutex = .unlocked,
- items: std.ArrayList(Msg) = .empty,
+ items: [queue_capacity]Msg = undefined,
+ head: usize = 0,
+ len: usize = 0,
closed: bool = false,
fn lock(q: *Queue) void {
while (!q.mutex.tryLock()) std.atomic.spinLoopHint();
}
+ fn removeAt(q: *Queue, offset: usize) Msg {
+ const removed = q.items[(q.head + offset) % q.items.len];
+ var i = offset;
+ while (i + 1 < q.len) : (i += 1)
+ q.items[(q.head + i) % q.items.len] = q.items[(q.head + i + 1) % q.items.len];
+ q.len -= 1;
+ return removed;
+ }
+
+ /// Output and other refreshable work are lossy under sustained
+ /// backpressure. EOF and pipe completions are admitted by evicting queued
+ /// non-critical messages, so descriptors and futures reach the loop.
fn push(q: *Queue, m: Msg) void {
q.lock();
if (q.closed) {
q.mutex.unlock();
- switch (m) {
- .output => |o| q.gpa.free(o.bytes),
- .lsp => |l| q.gpa.free(l.rows),
- .pipe => |response_value| {
- var response = response_value;
- response.deinit(q.gpa);
- },
- .command => |line| q.gpa.free(line),
- .eof, .files_changed => {},
- }
+ m.deinit(q.gpa, q.lsp_allocator);
return;
}
- q.items.append(q.gpa, m) catch {
- q.mutex.unlock();
- switch (m) {
- .output => |o| q.gpa.free(o.bytes),
- .lsp => |l| q.gpa.free(l.rows),
- .pipe => |response_value| {
- var response = response_value;
- response.deinit(q.gpa);
- },
- .command => |line| q.gpa.free(line),
- .eof, .files_changed => {},
+ if (q.len == q.items.len) {
+ const incoming_critical = switch (m) {
+ .pipe, .eof => true,
+ else => false,
+ };
+ if (!incoming_critical) {
+ q.mutex.unlock();
+ m.deinit(q.gpa, q.lsp_allocator);
+ return;
}
- return;
- };
+ var offset: usize = 0;
+ while (offset < q.len) : (offset += 1) {
+ const queued_critical = switch (q.items[(q.head + offset) % q.items.len]) {
+ .pipe, .eof => true,
+ else => false,
+ };
+ if (!queued_critical) break;
+ }
+ if (offset == q.len) {
+ q.mutex.unlock();
+ m.deinit(q.gpa, q.lsp_allocator);
+ return;
+ }
+ q.removeAt(offset).deinit(q.gpa, q.lsp_allocator);
+ }
+ q.items[(q.head + q.len) % q.items.len] = m;
+ q.len += 1;
q.mutex.unlock();
if (q.sdl_wake) {
var sev = std.mem.zeroes(c.SDL_Event);
@@ -646,30 +738,41 @@ const Queue = struct {
}
}
- /// take the pending messages (caller iterates + deinits outside the lock)
- fn take(q: *Queue) std.ArrayList(Msg) {
+ fn take(q: *Queue) MessageBatch {
q.lock();
defer q.mutex.unlock();
- const local = q.items;
- q.items = .empty;
- return local;
+ var batch: MessageBatch = .{};
+ while (q.len > 0) {
+ batch.items[batch.len] = q.items[q.head];
+ batch.len += 1;
+ q.head = (q.head + 1) % q.items.len;
+ q.len -= 1;
+ }
+ q.head = 0;
+ return batch;
}
- fn close(q: *Queue) void {
+ fn close(q: *Queue, ptys: *[pardes.MAX_PANES]?Pty, gens: *[pardes.MAX_PANES]u32) void {
q.lock();
defer q.mutex.unlock();
q.closed = true;
- for (q.items.items) |m| switch (m) {
- .output => |o| q.gpa.free(o.bytes),
- .lsp => |l| q.gpa.free(l.rows),
- .pipe => |response_value| {
- var response = response_value;
- response.deinit(q.gpa);
- },
- .command => |line| q.gpa.free(line),
- .eof, .files_changed => {},
- };
- q.items.deinit(q.gpa);
+ while (q.len > 0) {
+ const m = q.items[q.head];
+ switch (m) {
+ .eof => |e| {
+ _ = libc.close(e.fd);
+ if (gens[e.pane] == e.gen) {
+ if (ptys[e.pane]) |pt| if (pt.fd == e.fd) {
+ ptys[e.pane] = null;
+ };
+ }
+ },
+ else => m.deinit(q.gpa, q.lsp_allocator),
+ }
+ q.head = (q.head + 1) % q.items.len;
+ q.len -= 1;
+ }
+ q.head = 0;
}
};
@@ -790,14 +893,15 @@ fn reloadChanged(core: *pardes.Pardes, gpa: std.mem.Allocator, watches: *[pardes
/// Answer a language query off the render loop and push the rows to the queue
/// — the async execution model, spelled in the plumbing this shell already has
/// (a detached thread and the mutex queue the pty readers use).
-fn lspThread(gpa: std.mem.Allocator, job: *LspJob, q: *Queue) void {
- defer job.free(gpa);
- var arena: std.heap.ArenaAllocator = .init(gpa);
+fn lspThread(lsp_allocator: std.mem.Allocator, workers: *LspWorkers, job: *LspJob, q: *Queue) void {
+ defer workers.finish();
+ defer job.free(lsp_allocator);
+ var arena: std.heap.ArenaAllocator = .init(lsp_allocator);
defer arena.deinit();
// the shell owns the result buffer; the backend only ever writes to it
- var out: std.Io.Writer.Allocating = .init(gpa);
+ var out: std.Io.Writer.Allocating = .init(lsp_allocator);
defer out.deinit();
- pardes.lsp.query(gpa, arena.allocator(), .{
+ pardes.lsp.query(lsp_allocator, arena.allocator(), .{
.kind = job.kind,
.path = job.path,
.source = job.source,
@@ -805,48 +909,51 @@ fn lspThread(gpa: std.mem.Allocator, job: *LspJob, q: *Queue) void {
.arg = job.arg,
.root = job.root,
}, &out.writer);
- const rows = gpa.dupe(u8, out.written()) catch return;
+ const rows = lsp_allocator.dupe(u8, out.written()) catch return;
q.push(.{ .lsp = .{ .id = job.id, .rows = rows } });
}
/// Copy the query out of the core and hand it to a thread. A detached thread
/// per query is fine at this rate: one keystroke, one query, and the queue
/// already tolerates a late push after close.
-fn spawnLsp(core: *pardes.Pardes, gpa: std.mem.Allocator, q: *Queue, e: anytype) void {
+fn spawnLsp(core: *pardes.Pardes, q: *Queue, e: anytype) void {
+ const lsp_allocator = q.lsp_allocator;
const pane = core.panes[e.pane] orelse return;
// a pane with no file still asks `status` (it is about the backend, not
// the buffer): empty path and source, root from the pane's cwd
const f = pane.file;
- const job = gpa.create(LspJob) catch return;
+ const job = lsp_allocator.create(LspJob) catch return;
job.* = .{
.id = e.id,
.kind = e.kind,
.offset = e.offset,
- .path = gpa.dupe(u8, if (f) |ff| ff.path else "") catch {
- gpa.destroy(job);
+ .path = lsp_allocator.dupe(u8, if (f) |ff| ff.path else "") catch {
+ lsp_allocator.destroy(job);
return;
},
- .source = gpa.dupeZ(u8, if (f) |ff| ff.content else "") catch {
- gpa.free(job.path);
- gpa.destroy(job);
+ .source = lsp_allocator.dupeZ(u8, if (f) |ff| ff.content else "") catch {
+ lsp_allocator.free(job.path);
+ lsp_allocator.destroy(job);
return;
},
- .arg = gpa.dupe(u8, e.arg.slice()) catch {
- gpa.free(job.path);
- gpa.free(job.source);
- gpa.destroy(job);
+ .arg = lsp_allocator.dupe(u8, e.arg.slice()) catch {
+ lsp_allocator.free(job.path);
+ lsp_allocator.free(job.source);
+ lsp_allocator.destroy(job);
return;
},
- .root = gpa.dupe(u8, if (f) |ff| (std.fs.path.dirname(ff.path) orelse "/") else pane.cwdSlice()) catch {
- gpa.free(job.path);
- gpa.free(job.source);
- gpa.free(job.arg);
- gpa.destroy(job);
+ .root = lsp_allocator.dupe(u8, if (f) |ff| (std.fs.path.dirname(ff.path) orelse "/") else pane.cwdSlice()) catch {
+ lsp_allocator.free(job.path);
+ lsp_allocator.free(job.source);
+ lsp_allocator.free(job.arg);
+ lsp_allocator.destroy(job);
return;
},
};
- const th = std.Thread.spawn(.{}, lspThread, .{ gpa, job, q }) catch {
- job.free(gpa);
+ q.lsp_workers.start();
+ const th = std.Thread.spawn(.{}, lspThread, .{ lsp_allocator, q.lsp_workers, job, q }) catch {
+ q.lsp_workers.finish();
+ job.free(lsp_allocator);
return;
};
th.detach();
@@ -865,20 +972,20 @@ fn spawnPipe(
io: std.Io,
gpa: std.mem.Allocator,
q: *Queue,
- tasks: *std.ArrayList(PipeTask),
+ tasks: *PipeTasks,
request: anytype,
) void {
+ if (tasks.len == tasks.items.len) {
+ core.update(.{ .pipe_resp = .{ .id = request.id, .success = false, .outputs = &.{} } });
+ return;
+ }
const view = core.pipeRequest(request.id) orelse return;
const job = selection_pipe.Job.copy(gpa, view) catch return;
- tasks.ensureUnusedCapacity(gpa, 1) catch {
- job.deinit(gpa);
- return;
- };
const future = io.concurrent(pipeThread, .{ io, gpa, job, q }) catch {
job.deinit(gpa);
return;
};
- tasks.appendAssumeCapacity(.{ .id = request.id, .future = future });
+ std.debug.assert(tasks.add(.{ .id = request.id, .future = future }));
}
// ---- the renderer state ----
@@ -1205,8 +1312,16 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
g.pen_x = cell_w;
// ---- the core ----
- pardes.image.start(io, gpa); // stb_image allocator for image panes
- defer pardes.image.stop();
+ const allocs = pardes.allocators.init(gpa);
+ defer pardes.allocators.deinit();
+ pardes.image.start(io, allocs.image); // stb_image allocator for image panes
+ if (comptime pardes.pdf_enabled) pardes.pdf.start(allocs.pdf);
+ pardes.syntax.start(allocs.tree_sitter);
+ defer {
+ pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.stop();
+ }
// Live sessions initialize at the default 80x24 grid; the real window size
// arrives as a resize EVENT on the first loop pass. The core defers the
// shell greeting until after the first resize (so `ls` wraps to the real
@@ -1215,6 +1330,9 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// Dump loads pre-size instead: replayed panes never greet, and sizing at
// init avoids reflowing their replayed content twice.
var opts = opts_in;
+ opts.image_allocator = allocs.image;
+ opts.pdf_allocator = allocs.pdf;
+ opts.tree_sitter_allocator = allocs.tree_sitter;
var pw: c_int = 0;
var ph: c_int = 0;
if (opts.load_path != null) {
@@ -1225,8 +1343,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var core = if (opts.load_path) |lp| blk: {
const bytes = try @import("../look.zig").readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(gpa, opts, bytes);
- } else try pardes.Pardes.init(gpa, opts);
+ break :blk try pardes.Pardes.initFromDump(allocs.pardes, opts, bytes);
+ } else try pardes.Pardes.init(allocs.pardes, opts);
defer core.deinit();
// SDL is itself a native-pixel backend. This is deliberately set after
// construction: argv image panes no longer freeze the startup capability
@@ -1246,15 +1364,20 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var gens: [pardes.MAX_PANES]u32 = @splat(0);
defer for (&ptys) |*slot| if (slot.*) |pt| {
_ = libc.close(pt.fd);
- slot.* = null;
};
- var queue: Queue = .{ .gpa = gpa, .sdl_wake = true };
- defer queue.close();
- var pipe_tasks: std.ArrayList(PipeTask) = .empty;
+ var lsp_workers: LspWorkers = .{};
+ var queue: Queue = .{
+ .gpa = gpa,
+ .lsp_allocator = allocs.lsp,
+ .lsp_workers = &lsp_workers,
+ .sdl_wake = true,
+ };
defer {
- for (pipe_tasks.items) |*task| task.future.cancel(io) catch {};
- pipe_tasks.deinit(gpa);
+ lsp_workers.wait();
+ queue.close(&ptys, &gens);
}
+ var pipe_tasks: PipeTasks = .{};
+ defer pipe_tasks.cancelAll(io);
// One inotify instance for every watched pane, opened here — before any
// thread exists — so the pre-loop drain below can already mark the file a
// positional path argument opened. -1 off linux: watchPane goes quiet and
@@ -1289,7 +1412,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var feed: StdinFeed = .{};
if (test_mode) setStdinRaw() catch {};
- var frame_arena: std.heap.ArenaAllocator = .init(gpa);
+ var frame_arena: std.heap.ArenaAllocator = .init(allocs.frame);
defer frame_arena.deinit();
var animation_clock: AnimationClock = .{};
@@ -1308,7 +1431,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// 2. pty output from the reader threads
var msgs = queue.take();
var check_files = false;
- for (msgs.items) |m| switch (m) {
+ for (msgs.slice()) |m| switch (m) {
.output => |o| {
if (gens[o.pane] == o.gen)
core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
@@ -1323,7 +1446,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
},
.lsp => |l| {
core.update(.{ .lsp_resp = .{ .id = l.id, .rows = l.rows } });
- gpa.free(l.rows);
+ allocs.lsp.free(l.rows);
},
.pipe => |response_value| {
var response = response_value;
@@ -1333,11 +1456,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
.outputs = response.outputs,
} });
response.deinit(gpa);
- for (pipe_tasks.items, 0..) |*task, i| if (task.id == response_value.id) {
- task.future.await(io) catch {};
- _ = pipe_tasks.orderedRemove(i);
- break;
- };
+ pipe_tasks.finish(io, response_value.id);
},
.command => |line| {
core.update(.{ .command = line });
@@ -1348,7 +1467,6 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// a reload — or an undo entry — per write.
.files_changed => check_files = true,
};
- msgs.deinit(gpa);
if (check_files) reloadChanged(core, gpa, &watches);
// 3. steamdeck: poll gamepad axes into virtual cursor / wheel events
pollGamepad(&g, core);
@@ -1364,7 +1482,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var o = core.opts;
o.cols = core.screen_w;
o.rows = core.screen_h; // pre-size: dump panes never greet
- const nc = pardes.Pardes.initFromDump(gpa, o, bytes) catch break :blk;
+ const nc = pardes.Pardes.initFromDump(allocs.pardes, o, bytes) catch break :blk;
for (&ptys) |*slot| if (slot.*) |pt| {
_ = libc.kill(pt.pid, libc.SIG.KILL);
slot.* = null;
@@ -1656,7 +1774,7 @@ fn runWeb(opts_in: pardes.Options) !void {
},
.core = undefined,
.gpa = gpa,
- .frame_arena = .init(gpa),
+ .frame_arena = undefined,
};
const g = &st.g;
// slot (0,0) is the space glyph (blank cells sample alpha=0 → bg only)
@@ -1664,15 +1782,27 @@ fn runWeb(opts_in: pardes.Options) !void {
g.pen_x = cell_w;
// ---- the core: state replays from the embedded dump, no ptys ----
- pardes.image.start(std.Io.failing, gpa); // decode is synchronous; no io on wasm
+ const allocs = pardes.allocators.init(gpa);
+ errdefer pardes.allocators.deinit();
+ st.frame_arena = .init(allocs.frame);
+ errdefer st.frame_arena.deinit();
+ pardes.image.start(std.Io.failing, allocs.image); // decode is synchronous; no io on wasm
+ 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();
syncCanvasSize(g); // size the backing canvas before deriving the grid
var opts = opts_in;
+ opts.image_allocator = allocs.image;
+ opts.pdf_allocator = allocs.pdf;
+ opts.tree_sitter_allocator = allocs.tree_sitter;
var pw: c_int = 0;
var ph: c_int = 0;
_ = c.SDL_GetWindowSizeInPixels(window, &pw, &ph);
opts.cols = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), cell_w)));
opts.rows = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), cell_h)));
- st.core = try pardes.Pardes.initFromDump(gpa, opts, embedded_dump);
+ st.core = try pardes.Pardes.initFromDump(allocs.pardes, opts, embedded_dump);
live_web_state = st;
_ = c.SDL_StartTextInput(window);
@@ -1688,7 +1818,17 @@ fn webFrame(arg: ?*anyopaque) callconv(.c) void {
while (c.SDL_PollEvent(&sev)) dispatch(g, core, &sev);
// 2. effects
drainEffectsWeb(core, st.gpa, g);
- if (core.quit) return std.os.emscripten.emscripten_cancel_main_loop();
+ if (core.quit) {
+ std.os.emscripten.emscripten_cancel_main_loop();
+ live_web_state = null;
+ core.deinit();
+ st.frame_arena.deinit();
+ pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.stop();
+ pardes.allocators.deinit();
+ return;
+ }
// 3. the backing canvas follows the page, the grid follows the canvas
syncCanvasSize(g);
var pw: c_int = 0;
@@ -1730,12 +1870,23 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
const gpa = init.gpa;
const env = init.environ_map;
- pardes.image.start(io, gpa);
- defer pardes.image.stop();
+ const allocs = pardes.allocators.init(gpa);
+ defer pardes.allocators.deinit();
+ pardes.image.start(io, allocs.image);
+ if (comptime pardes.pdf_enabled) pardes.pdf.start(allocs.pdf);
+ pardes.syntax.start(allocs.tree_sitter);
+ defer {
+ pardes.image.stop();
+ if (comptime pardes.pdf_enabled) pardes.pdf.stop();
+ pardes.syntax.stop();
+ }
const grid_cols = envU16(env, "PARDES_TEST_COLS") orelse 80;
const grid_rows = envU16(env, "PARDES_TEST_ROWS") orelse 24;
var opts = opts_in;
+ opts.image_allocator = allocs.image;
+ opts.pdf_allocator = allocs.pdf;
+ opts.tree_sitter_allocator = allocs.tree_sitter;
if (opts.load_path != null) {
opts.cols = grid_cols;
opts.rows = grid_rows;
@@ -1743,8 +1894,8 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
const core = if (opts.load_path) |lp| blk: {
const bytes = try @import("../look.zig").readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(gpa, opts, bytes);
- } else try pardes.Pardes.init(gpa, opts);
+ break :blk try pardes.Pardes.initFromDump(allocs.pardes, opts, bytes);
+ } else try pardes.Pardes.init(allocs.pardes, opts);
defer core.deinit();
// the requested grid arrives as a resize EVENT (not init opts) so the core
// counts a resize and releases the shell greeting on first output
@@ -1763,15 +1914,20 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
var gens: [pardes.MAX_PANES]u32 = @splat(0);
defer for (&ptys) |*slot| if (slot.*) |pt| {
_ = libc.close(pt.fd);
- slot.* = null;
};
- var queue: Queue = .{ .gpa = gpa, .sdl_wake = false };
- defer queue.close();
- var pipe_tasks: std.ArrayList(PipeTask) = .empty;
+ var lsp_workers: LspWorkers = .{};
+ var queue: Queue = .{
+ .gpa = gpa,
+ .lsp_allocator = allocs.lsp,
+ .lsp_workers = &lsp_workers,
+ .sdl_wake = false,
+ };
defer {
- for (pipe_tasks.items) |*task| task.future.cancel(io) catch {};
- pipe_tasks.deinit(gpa);
+ lsp_workers.wait();
+ queue.close(&ptys, &gens);
}
+ var pipe_tasks: PipeTasks = .{};
+ defer pipe_tasks.cancelAll(io);
// no inotify here on purpose: this mode's whole contract is one frame per
// scripted input event, and a reload that arrives on its own clock would
// put a frame in the stream nothing asked for. -1 makes watchPane a no-op.
@@ -1781,7 +1937,7 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort
- var frame_arena: std.heap.ArenaAllocator = .init(gpa);
+ var frame_arena: std.heap.ArenaAllocator = .init(allocs.frame);
defer frame_arena.deinit();
var feed: StdinFeed = .{};
@@ -1795,7 +1951,7 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
if (r.eof) break;
var n_events: usize = r.n_events;
var msgs = queue.take();
- for (msgs.items) |m| switch (m) {
+ for (msgs.slice()) |m| switch (m) {
.output => |o| {
if (gens[o.pane] == o.gen)
core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
@@ -1812,7 +1968,7 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
},
.lsp => |l| {
core.update(.{ .lsp_resp = .{ .id = l.id, .rows = l.rows } });
- gpa.free(l.rows);
+ allocs.lsp.free(l.rows);
n_events += 1;
},
.pipe => |response_value| {
@@ -1824,17 +1980,12 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
} });
response.deinit(gpa);
n_events += 1;
- for (pipe_tasks.items, 0..) |*task, i| if (task.id == response_value.id) {
- task.future.await(io) catch {};
- _ = pipe_tasks.orderedRemove(i);
- break;
- };
+ pipe_tasks.finish(io, response_value.id);
},
// both unreachable here: this mode starts neither thread
.files_changed => {},
.command => |line| gpa.free(line),
};
- msgs.deinit(gpa);
drainEffects(core, &ptys, &gens, io, gpa, &queue, &pipe_tasks, null, -1, &watches, true);
pollCwds(core, &ptys);
// The grid harness polls stdin at the same 16 ms cadence as native
@@ -2062,27 +2213,48 @@ fn setStdinRaw() !void {
// PARDES_TEST_GRID: one text frame per render, prototype-compatible framing.
fn dumpGrid(gpa: std.mem.Allocator, surface: *pardes.Surface) !void {
if (surface.cols == 0 or surface.rows == 0) return;
- // build the whole frame in one buffer → one write → atomic snapshots
- var buf: std.ArrayList(u8) = .empty;
- defer buf.deinit(gpa);
const cur_x: u16 = if (surface.cursor) |cu| cu.x else 0;
const cur_y: u16 = if (surface.cursor) |cu| cu.y else 0;
var hdr: [128]u8 = undefined;
- const hdr_str = std.fmt.bufPrint(&hdr, "\n---FRAME:{d}:{d}:{d}:{d}:{}---\n", .{
+ const header = std.fmt.bufPrint(&hdr, "\n---FRAME:{d}:{d}:{d}:{d}:{}---\n", .{
surface.rows, surface.cols, cur_x, cur_y, surface.cursor != null,
}) catch return;
- try buf.appendSlice(gpa, hdr_str);
+ const footer = "---ENDFRAME---\n";
+ var total = try std.math.add(usize, header.len, footer.len);
+ total = try std.math.add(usize, total, surface.rows);
var y: u16 = 0;
while (y < surface.rows) : (y += 1) {
var x: u16 = 0;
while (x < surface.cols) : (x += 1) {
- const g = surface.at(x, y).grapheme();
- if (g.len > 0) try buf.appendSlice(gpa, g) else try buf.append(gpa, ' ');
+ const grapheme = surface.at(x, y).grapheme();
+ total = try std.math.add(usize, total, if (grapheme.len > 0) grapheme.len else 1);
+ }
+ }
+ const frame = try gpa.alloc(u8, total);
+ defer gpa.free(frame);
+ var at: usize = 0;
+ @memcpy(frame[at..][0..header.len], header);
+ at += header.len;
+ y = 0;
+ while (y < surface.rows) : (y += 1) {
+ var x: u16 = 0;
+ while (x < surface.cols) : (x += 1) {
+ const grapheme = surface.at(x, y).grapheme();
+ if (grapheme.len > 0) {
+ @memcpy(frame[at..][0..grapheme.len], grapheme);
+ at += grapheme.len;
+ } else {
+ frame[at] = ' ';
+ at += 1;
+ }
}
- try buf.append(gpa, '\n');
+ frame[at] = '\n';
+ at += 1;
}
- try buf.appendSlice(gpa, "---ENDFRAME---\n");
- writeFd(1, buf.items);
+ @memcpy(frame[at..][0..footer.len], footer);
+ at += footer.len;
+ std.debug.assert(at == frame.len);
+ writeFd(1, frame);
}
// =====================================================================
@@ -2553,7 +2725,7 @@ fn drainEffects(
io: std.Io,
gpa: std.mem.Allocator,
queue: *Queue,
- pipe_tasks: *std.ArrayList(PipeTask),
+ pipe_tasks: *PipeTasks,
g: ?*Gui, // null in grid test mode (no SDL: clipboard effects are no-ops)
inotify_fd: c_int,
watches: *[pardes.MAX_PANES]?Watch,
@@ -2640,7 +2812,7 @@ fn drainEffects(
defer gpa.free(z);
_ = c.SDL_SetClipboardText(z.ptr);
},
- .lsp => |e| if (threads_ok) spawnLsp(core, gpa, queue, e),
+ .lsp => |e| if (threads_ok) spawnLsp(core, queue, e),
.pipe => |e| if (threads_ok) spawnPipe(core, io, gpa, queue, pipe_tasks, e),
.watch => |w| {
// starting, the path and the on-disk bytes are read off the core
@@ -2702,15 +2874,30 @@ fn drainEffectsWeb(core: *pardes.Pardes, gpa: std.mem.Allocator, g: *Gui) void {
/// window.open in a tiny generated script; single quotes and backslashes are
/// %-escaped so the url can't break out of the JS string literal.
fn openLinkWeb(gpa: std.mem.Allocator, url: []const u8) void {
- var safe: std.ArrayList(u8) = .empty;
- defer safe.deinit(gpa);
+ const prefix = "window.open('";
+ const suffix = "','_blank')";
+ var escaped_len: usize = 0;
+ for (url) |ch| escaped_len = std.math.add(usize, escaped_len, if (ch == '\'' or ch == '\\') 3 else 1) catch return;
+ const script_len = std.math.add(usize, prefix.len + suffix.len, escaped_len) catch return;
+ const script = gpa.allocSentinel(u8, script_len, 0) catch return;
+ defer gpa.free(script);
+ @memcpy(script[0..prefix.len], prefix);
+ var at = prefix.len;
for (url) |ch| switch (ch) {
- '\'' => safe.appendSlice(gpa, "%27") catch return,
- '\\' => safe.appendSlice(gpa, "%5C") catch return,
- else => safe.append(gpa, ch) catch return,
+ '\'' => {
+ @memcpy(script[at..][0..3], "%27");
+ at += 3;
+ },
+ '\\' => {
+ @memcpy(script[at..][0..3], "%5C");
+ at += 3;
+ },
+ else => {
+ script[at] = ch;
+ at += 1;
+ },
};
- const script = std.fmt.allocPrintSentinel(gpa, "window.open('{s}','_blank')", .{safe.items}, 0) catch return;
- defer gpa.free(script);
+ @memcpy(script[at..][0..suffix.len], suffix);
std.os.emscripten.emscripten_run_script(script.ptr);
}
@@ -3125,13 +3312,19 @@ fn prepareNativeImages(
uploadNativeTexture(g, gpa, cmd, place) catch continue;
}
- var stale: std.ArrayList(pardes.ImageCacheKey) = .empty;
- defer stale.deinit(gpa);
- var iterator = g.native_images.iterator();
- while (iterator.next()) |entry|
- if (!surfaceHasNativeKey(surface, entry.key_ptr.*))
- stale.append(gpa, entry.key_ptr.*) catch {};
- for (stale.items) |key| releaseNativeImage(g, key);
+ while (true) {
+ var stale: [pardes.MAX_PANES]pardes.ImageCacheKey = undefined;
+ var stale_len: usize = 0;
+ var iterator = g.native_images.iterator();
+ while (iterator.next()) |entry| {
+ if (surfaceHasNativeKey(surface, entry.key_ptr.*)) continue;
+ stale[stale_len] = entry.key_ptr.*;
+ stale_len += 1;
+ if (stale_len == stale.len) break;
+ }
+ for (stale[0..stale_len]) |key| releaseNativeImage(g, key);
+ if (stale_len < stale.len) break;
+ }
var count: u32 = 0;
for (surface.images[0..surface.nimages]) |maybe| {