diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-09 10:41:33 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch) | |
| tree | 91fce278abc85e4f7491de8b5f02184c8a2d5a56 /src/tty/tty.zig | |
| parent | 2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff) | |
| download | pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip | |
replace ArrayLists with bounded storage
Diffstat (limited to 'src/tty/tty.zig')
| -rw-r--r-- | src/tty/tty.zig | 183 |
1 files changed, 114 insertions, 69 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig index ad6f04b0..e8e3c1bd 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -37,7 +37,7 @@ pub const Command = struct { winsize: vaxis.Winsize, mouse: vaxis.Mouse, paste: []const u8, - /// a language query finished on a worker; rows are gpa-owned + /// a language query finished on a worker; rows are lsp-domain-owned lsp_done: struct { id: u32, rows: []u8 }, /// a selection-filter worker finished; every stdout is gpa-owned pipe_done: selection_pipe.Response, @@ -83,20 +83,51 @@ const LspJob = struct { arg: []u8, root: []u8, - fn free(j: *LspJob, gpa: std.mem.Allocator) void { - gpa.free(j.path); - gpa.free(j.source); - gpa.free(j.arg); - gpa.free(j.root); - gpa.destroy(j); + fn free(j: *LspJob, allocator: std.mem.Allocator) void { + allocator.free(j.path); + allocator.free(j.source); + allocator.free(j.arg); + allocator.free(j.root); + allocator.destroy(j); } }; +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 full(tasks: *const PipeTasks) bool { + return tasks.len == tasks.items.len; + } + + fn add(tasks: *PipeTasks, task: PipeTask) void { + std.debug.assert(!tasks.full()); + tasks.items[tasks.len] = task; + tasks.len += 1; + } + + 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 Pty = struct { file: std.Io.File, pid: posix.pid_t, @@ -338,6 +369,13 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { const io = init.io; const gpa = init.gpa; + const allocs = pardes.allocators.init(gpa); + defer pardes.allocators.deinit(); + var options = opts; + options.image_allocator = allocs.image; + options.pdf_allocator = allocs.pdf; + options.tree_sitter_allocator = allocs.tree_sitter; + // SIGWINCH must never run vaxis's signal handler: it posts the winsize // event through std.Io.Mutex/Condition, and when the signal lands on a // thread blocked inside an Io.Threaded syscall region (pty readers in @@ -366,20 +404,25 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { // clicks there (see test/snapshots/ghostty-mid.snap). try vx.setMouseMode(tty.writer(), true); - var frame_arena: std.heap.ArenaAllocator = .init(gpa); - defer frame_arena.deinit(); - - // stb_image allocator for image panes - pardes.image.start(io, gpa); - defer pardes.image.stop(); + 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(); + } - var core = if (opts.load_path) |lp| blk: { + var core = if (options.load_path) |lp| blk: { const bytes = try look.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, options, bytes); + } else try pardes.Pardes.init(allocs.pardes, options); defer core.deinit(); + var frame_arena: std.heap.ArenaAllocator = .init(allocs.frame); + defer frame_arena.deinit(); + // shells emit OSC 133 prompt marks via this rc (prompt hiding, click-move) writeFile(shell_bin.bash_rc_path, shell_bin.bash_rc); writeFile(shell_bin.fish_rc_path, shell_bin.fish_rc); @@ -401,7 +444,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { var lsp_task: ?std.Io.Future(anyerror!void) = null; // Selection filters may overlap: a second submit supersedes the first in // core without synchronously canceling a possibly slow shell command. - var pipe_tasks: std.ArrayList(PipeTask) = .empty; + var pipe_tasks: PipeTasks = .{}; // One inotify instance for every watched pane, opened here — before any // thread exists — so the pre-loop effect drain below can already mark the // file a positional path argument opened. -1 off linux: watchPane goes @@ -426,8 +469,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { t.cancel(io) catch {}; lsp_task = null; } - for (pipe_tasks.items) |*task| task.future.cancel(io) catch {}; - pipe_tasks.deinit(gpa); + pipe_tasks.cancelAll(io); // same contract as the pty readers: cancel unblocks the watcher's // read, and only then is the fd safe to close if (watch_task) |*t| { @@ -442,7 +484,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { .pty_read => |pr| gpa.free(pr.bytes), .command => |line| gpa.free(line), .paste => |b| gpa.free(@constCast(b)), - .lsp_done => |d| gpa.free(d.rows), + .lsp_done => |d| allocs.lsp.free(d.rows), .pipe_done => |response_value| { var response = response_value; response.deinit(gpa); @@ -457,12 +499,12 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { // --nested opted out of the whole mechanism, including being an outer // instance; so does any failure to bind, and then children simply open // their own session. - const sock_fd: c_int = if (opts.nested) -1 else nested.listen(); + const sock_fd: c_int = if (options.nested) -1 else nested.listen(); defer nested.unlisten(sock_fd); // Perform the initial spawns BEFORE any worker thread exists: forkpty from // a multithreaded process can wedge the child before exec. - drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, &lsp_task, &pipe_tasks, inotify_fd, &watches, false); + drainEffects(core, &ptys, &gens, io, gpa, allocs.lsp, &loop, &vx, &tty, &lsp_task, &pipe_tasks, inotify_fd, &watches, false); try loop.start(); defer loop.stop(); @@ -621,7 +663,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { .files_changed => check_files = true, .lsp_done => |d| { core.update(.{ .lsp_resp = .{ .id = d.id, .rows = d.rows } }); - gpa.free(d.rows); + allocs.lsp.free(d.rows); // the worker is finished; join it so its future does not // leak (same contract as pty_eof above) if (lsp_task) |*t| { @@ -637,11 +679,7 @@ pub fn run(init: std.process.Init, opts: 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); }, } batch += 1; @@ -679,7 +717,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { } } - drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, &lsp_task, &pipe_tasks, inotify_fd, &watches, true); + drainEffects(core, &ptys, &gens, io, gpa, allocs.lsp, &loop, &vx, &tty, &lsp_task, &pipe_tasks, inotify_fd, &watches, true); if (core.quit) break; // Restore builtin: swap in a core rebuilt from the dump; the live @@ -691,7 +729,7 @@ pub fn run(init: std.process.Init, opts: 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, 0..) |*slot, pid| if (slot.*) |*pt| { pt.reader.cancel(io) catch {}; _ = libc.close(pt.file.handle); @@ -811,14 +849,20 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { // Toggling PETSCII or closing a pane removes its attachment from the // Surface. Release the terminal-side image then, not merely when that // numeric pane slot happens to be reused. - var stale: std.ArrayList(pardes.ImageCacheKey) = .empty; - defer stale.deinit(gpa); - var image_iterator = kitty_handles.iterator(); - while (image_iterator.next()) |entry| - if (!surfaceHasKittyKey(surface, entry.key_ptr.*)) - stale.append(gpa, entry.key_ptr.*) catch {}; - for (stale.items) |key| if (kitty_handles.fetchRemove(key)) |removed| - vx.freeImage(tty.writer(), removed.value.id); + while (true) { + var stale: [pardes.MAX_PANES]pardes.ImageCacheKey = undefined; + var stale_len: usize = 0; + var image_iterator = kitty_handles.iterator(); + while (image_iterator.next()) |entry| { + if (surfaceHasKittyKey(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| if (kitty_handles.fetchRemove(key)) |removed| + vx.freeImage(tty.writer(), removed.value.id); + if (stale_len < stale.len) break; + } if (surface.cursor) |cur| { win.showCursor(cur.x, cur.y); // insert = beam, everything else = the terminal's default shape @@ -847,11 +891,12 @@ fn drainEffects( gens: *[pardes.MAX_PANES]u32, io: std.Io, gpa: std.mem.Allocator, + lsp_allocator: std.mem.Allocator, loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty, lsp_task: *?std.Io.Future(anyerror!void), - pipe_tasks: *std.ArrayList(PipeTask), + pipe_tasks: *PipeTasks, inotify_fd: c_int, watches: *[pardes.MAX_PANES]?Watch, threads_ok: bool, @@ -944,31 +989,31 @@ fn drainEffects( // 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 continue; + const job = lsp_allocator.create(LspJob) catch continue; job.* = .{ .id = q.id, .kind = q.kind, .offset = q.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); continue; }, - .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); continue; }, - .arg = gpa.dupe(u8, q.arg.slice()) catch { - gpa.free(job.path); - gpa.free(job.source); - gpa.destroy(job); + .arg = lsp_allocator.dupe(u8, q.arg.slice()) catch { + lsp_allocator.free(job.path); + lsp_allocator.free(job.source); + lsp_allocator.destroy(job); continue; }, - .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); continue; }, }; @@ -981,24 +1026,24 @@ fn drainEffects( old.cancel(io) catch {}; lsp_task.* = null; } - lsp_task.* = io.concurrent(lspWorker, .{ gpa, job, loop }) catch { - job.free(gpa); + lsp_task.* = io.concurrent(lspWorker, .{ lsp_allocator, job, loop }) catch { + job.free(lsp_allocator); continue; }; }, .pipe => |request| { if (!threads_ok) continue; + if (pipe_tasks.full()) { + core.update(.{ .pipe_resp = .{ .id = request.id, .success = false, .outputs = &.{} } }); + continue; + } const view = core.pipeRequest(request.id) orelse continue; const job = selection_pipe.Job.copy(gpa, view) catch continue; - pipe_tasks.ensureUnusedCapacity(gpa, 1) catch { - job.deinit(gpa); - continue; - }; const future = io.concurrent(pipeWorker, .{ io, gpa, job, loop }) catch { job.deinit(gpa); continue; }; - pipe_tasks.appendAssumeCapacity(.{ .id = request.id, .future = future }); + pipe_tasks.add(.{ .id = request.id, .future = future }); }, .watch => |w| { // starting, the path and the on-disk bytes are read off the core @@ -1019,14 +1064,14 @@ fn drainEffects( /// the whole async execution model: the same shape as readPty — do the slow /// thing on a worker, hand the result to the loop as an event, let the core /// stay a state machine that never blocks. -fn lspWorker(gpa: std.mem.Allocator, job: *LspJob, loop: *Loop) anyerror!void { - defer job.free(gpa); - var arena: std.heap.ArenaAllocator = .init(gpa); +fn lspWorker(allocator: std.mem.Allocator, job: *LspJob, loop: *Loop) anyerror!void { + defer job.free(allocator); + var arena: std.heap.ArenaAllocator = .init(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); + // The shell owns the result buffer; the backend only ever writes to it. + var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); - pardes.lsp.query(gpa, arena.allocator(), .{ + pardes.lsp.query(allocator, arena.allocator(), .{ .kind = job.kind, .path = job.path, .source = job.source, @@ -1034,8 +1079,8 @@ fn lspWorker(gpa: std.mem.Allocator, job: *LspJob, loop: *Loop) anyerror!void { .arg = job.arg, .root = job.root, }, &out.writer); - const rows = gpa.dupe(u8, out.written()) catch return; - loop.postEvent(.{ .lsp_done = .{ .id = job.id, .rows = rows } }) catch gpa.free(rows); + const rows = allocator.dupe(u8, out.written()) catch return; + loop.postEvent(.{ .lsp_done = .{ .id = job.id, .rows = rows } }) catch allocator.free(rows); return; } |
