summaryrefslogtreecommitdiff
path: root/src/tty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-30 23:35:45 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit19f7322100062b7c1adcde3376063ce6c1d8c72d (patch)
treeb76347dffe0347c29a38b4cad6031678683dba74 /src/tty.zig
parent753d5451ca7d09dbc5ff44f3c2dcf9a47fb87f96 (diff)
downloadpardes-19f7322100062b7c1adcde3376063ce6c1d8c72d.tar.gz
pardes-19f7322100062b7c1adcde3376063ce6c1d8c72d.zip
structure: backends into src/{tty,gui,lsp}, pane kinds and builtins into their own files
The core now lies FLAT at src/ and every subdirectory is one backend, so a file being in no directory at all is what says it is core. Pane-kind bodies leave pardes.zig for term_pane.zig / file_pane.zig / output_pane.zig, leaving it the layout, the event/effect machine and the generic render loop. Builtins are one struct each in builtins.zig, and the enum is folded out of the file's own declaration list at comptime — a zig file IS a struct, so the list of builtins and the builtins themselves are the same text. Adding one is writing a struct. Key paths deliberately stay one table for the config pass. Pure refactor: no golden moved.
Diffstat (limited to 'src/tty.zig')
-rw-r--r--src/tty.zig624
1 files changed, 0 insertions, 624 deletions
diff --git a/src/tty.zig b/src/tty.zig
deleted file mode 100644
index 0bb7ef63..00000000
--- a/src/tty.zig
+++ /dev/null
@@ -1,624 +0,0 @@
-//! The terminal shell: owns the event loop and all IO. Translates vaxis
-//! events into core events, performs the core's effects (fork ptys, write
-//! them, resize them), and hands the core's Surface to vaxis cell-for-cell —
-//! the canonical interface rendered with no interpretation.
-const std = @import("std");
-const builtin = @import("builtin");
-const posix = std.posix;
-const libc = std.c;
-const vaxis = @import("vaxis");
-const pardes = @import("pardes.zig");
-const tracy = @import("tracy.zig");
-const look = @import("look.zig");
-
-extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int;
-extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
-extern "c" fn chdir(path: [*:0]const u8) c_int;
-extern "c" fn _exit(status: c_int) noreturn;
-extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
-
-// absolute shell path per OS: execv must not search PATH (no allocation
-// between fork and exec)
-const bash_path: [*:0]const u8 = if (builtin.os.tag == .linux) "/usr/bin/bash" else "/bin/bash";
-// TIOCSWINSZ: absent from std.c.T on darwin — _IOW('t', 103, winsize)
-const TIOCSWINSZ: c_int = @bitCast(@as(u32, if (@hasDecl(posix.T, "IOCSWINSZ")) posix.T.IOCSWINSZ else 0x80087467));
-
-pub const Command = struct {
- pub var value: union(enum) {
- nop,
- quit,
- tick,
- key_press: vaxis.Key,
- pty_read: struct { id: usize, bytes: []u8 },
- pty_eof: struct { id: usize, gen: u32 },
- winsize: vaxis.Winsize,
- mouse: vaxis.Mouse,
- paste: []const u8,
- /// a language query finished on a worker; rows are gpa-owned
- lsp_done: struct { id: u32, rows: []u8 },
- } = .nop;
-};
-const Loop = vaxis.Loop(@TypeOf(Command.value));
-
-/// One language query, owned by the worker that runs it. Everything the
-/// backend may read is copied in here before the worker starts: the core goes
-/// on editing the moment the effect is drained, so a borrowed slice would be a
-/// use-after-free the length of one keystroke.
-const LspJob = struct {
- id: u32,
- kind: pardes.lsp.Kind,
- offset: u32,
- path: []u8,
- source: [:0]u8,
- 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);
- }
-};
-
-const Pty = struct {
- file: std.Io.File,
- pid: posix.pid_t,
- reader: std.Io.Future(anyerror!void),
-};
-
-pub fn run(init: std.process.Init, opts: pardes.Options) !void {
- const io = init.io;
- const gpa = init.gpa;
-
- // 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
- // read(2), the main thread parked in queue.pop) a contended lock re-enters
- // the Io machinery and Syscall.start hits `unreachable` — panic, then the
- // panic-time terminal restore used to write through the same Io and
- // recurse until stack overflow. Reproduced by resizing the outer terminal
- // (e.g. a font-size change) while shells run. Block it here, before any
- // thread exists (threads inherit the mask, so vaxis's handler never
- // fires), and take it synchronously on the sigwait thread below instead.
- var winch_set = posix.sigemptyset();
- posix.sigaddset(&winch_set, posix.SIG.WINCH);
- posix.sigprocmask(posix.SIG.BLOCK, &winch_set, null);
-
- var tty_buf: [0x10000]u8 = undefined;
- var tty = try vaxis.Tty.init(io, &tty_buf);
- defer tty.deinit(); // restore cooked termios LAST, after vx flushed its resets
- var vx = try vaxis.init(io, gpa, init.environ_map, .{ .system_clipboard_allocator = gpa });
- defer vx.deinit(gpa, tty.writer());
- try vx.enterAltScreen(tty.writer());
- defer vx.exitAltScreen(tty.writer()) catch {};
- // requests 1002;1003;1004;1006 (cell-coordinate SGR; called pre-query, so
- // vaxis never upgrades to 1016 pixel mode). Note: ghostty's GTK apprt drops
- // middle press+release BEFORE mouse reporting when the desktop sets
- // gtk-enable-primary-paste=false — no mode we request can surface middle
- // 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();
-
- var core = if (opts.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);
- defer core.deinit();
-
- // shells emit OSC 133 prompt marks via this rc (prompt hiding, click-move)
- writeFile(pardes.bash_rc);
- // macos: apple's bash 3.2 prints the zsh-deprecation banner into every
- // pane unless this is in the env BEFORE bash starts (the rc is too late)
- if (comptime builtin.os.tag.isDarwin()) _ = setenv("BASH_SILENCE_DEPRECATION_WARNING", "1", 1);
-
- var kitty_handles: [pardes.MAX_PANES]?vaxis.Image = @splat(null);
- var ptys: [pardes.MAX_PANES]?Pty = @splat(null);
- // per-slot spawn generation: a reused pane id ignores the old shell's
- // late pty_eof (which would otherwise close the NEW pty on that slot)
- var gens: [pardes.MAX_PANES]u32 = @splat(0);
- // the single in-flight language query (see the .lsp effect)
- var lsp_task: ?std.Io.Future(anyerror!void) = null;
- var loop: Loop = .init(io, &tty, &vx);
- defer {
- // reap the reader tasks (cancel interrupts a blocked read) before
- // closing the masters — the runtime joins those threads on exit and a
- // reader stuck in read(2) would hang the process — then drain the
- // queue: leftover events own gpa bytes and would dump as leaks.
- for (&ptys) |*slot| if (slot.*) |*pt| {
- pt.reader.cancel(io) catch {};
- _ = libc.close(pt.file.handle);
- slot.* = null;
- };
- // join the query worker BEFORE the drain below, or its late post
- // lands in a queue nobody empties again and the rows leak
- if (lsp_task) |*t| {
- t.cancel(io) catch {};
- lsp_task = null;
- }
- while (loop.tryEvent() catch null) |ev| switch (ev) {
- .pty_read => |pr| gpa.free(pr.bytes),
- .paste => |b| gpa.free(@constCast(b)),
- .lsp_done => |d| gpa.free(d.rows),
- else => {},
- };
- }
-
- // 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, false);
-
- try loop.start();
- defer loop.stop();
- // resize watcher: plain detached thread (not io.concurrent — teardown
- // joins those, and sigwait never returns); dies with the process
- (try std.Thread.spawn(.{}, winchWatch, .{ &loop, &vx, &tty })).detach();
- vx.queryTerminal(tty.writer(), std.Io.Duration.fromMilliseconds(2)) catch {};
-
- // now threads are fine: start a reader task per pty
- for (&ptys, 0..) |*slot, id| if (slot.*) |*pt| {
- pt.reader = try io.concurrent(readPty, .{ io, gpa, pt.file, id, gens[id], &loop });
- };
-
- var pending: ?@TypeOf(Command.value) = .tick;
- while (!core.quit) {
- var event = if (pending) |ev| blk: {
- pending = null;
- break :blk ev;
- } else try loop.nextEvent();
- const tz_event = tracy.zone(@src(), "event");
- // Apply every queued INPUT event, then render ONCE — the gui shell
- // drains SDL's queue the same way. Without this a wheel flick is fifty
- // full render+repaint (and re-highlight) cycles instead of one.
- // pty output keeps a frame per chunk: progress paints as it arrives.
- var stop = false;
- var batch: usize = 0;
- while (true) {
- var output = false;
- switch (event) {
- .nop, .tick => {},
- .quit => stop = true,
- .winsize => |ws| {
- try vx.resize(gpa, tty.writer(), ws);
- core.update(.{ .resize = .{ .cols = @intCast(ws.cols), .rows = @intCast(ws.rows) } });
- },
- .pty_read => |pr| {
- core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } });
- gpa.free(pr.bytes);
- output = true;
- },
- .pty_eof => |e| if (gens[e.id] == e.gen) {
- if (ptys[e.id]) |*pt| {
- pt.reader.await(io) catch {}; // reader just finished; join it or its future leaks
- _ = libc.close(pt.file.handle);
- ptys[e.id] = null;
- }
- core.update(.{ .eof = .{ .pane = @intCast(e.id) } });
- },
- .key_press => |key| core.update(.{ .key = .{
- .cp = mapKey(effCp(key)),
- .text = key.text orelse "",
- .ctrl = key.mods.ctrl,
- .alt = key.mods.alt,
- .shift = key.mods.shift,
- } }),
- .mouse => |m| {
- const button: ?pardes.Mouse.Button = switch (m.button) {
- .left => .left,
- .middle => .middle,
- .right => .right,
- .wheel_up => .wheel_up,
- .wheel_down => .wheel_down,
- .wheel_left => .wheel_left,
- .wheel_right => .wheel_right,
- .none => .none, // button-less motion: hover tracking
- else => null,
- };
- if (button) |b| core.update(.{ .mouse = .{
- .button = b,
- .kind = switch (m.type) {
- .press => .press,
- .release => .release,
- .motion => .motion,
- .drag => .drag,
- },
- .col = @intCast(m.col),
- .row = @intCast(m.row),
- .ctrl = m.mods.ctrl,
- } });
- },
- .paste => |bytes| {
- core.update(.{ .paste = bytes });
- gpa.free(@constCast(bytes));
- },
- .lsp_done => |d| {
- core.update(.{ .lsp_resp = .{ .id = d.id, .rows = d.rows } });
- gpa.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| {
- t.await(io) catch {};
- lsp_task = null;
- }
- },
- }
- batch += 1;
- if (stop or output or batch >= 64) break;
- event = (try loop.tryEvent()) orelse break;
- }
- tz_event.end();
- if (stop) break;
-
- drainEffects(core, &ptys, &gens, io, gpa, &loop, &vx, &tty, &lsp_task, true);
- if (core.quit) break;
-
- // Restore builtin: swap in a core rebuilt from the dump; the live
- // shells die with their masters (readers canceled, gens bumped so
- // their late eofs never touch the replay panes)
- if (core.takeRestore()) |rp| blk: {
- const bytes = look.readFile(gpa, rp) catch break :blk;
- defer gpa.free(bytes);
- 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;
- for (&ptys, 0..) |*slot, pid| if (slot.*) |*pt| {
- pt.reader.cancel(io) catch {};
- _ = libc.close(pt.file.handle);
- slot.* = null;
- gens[pid] +%= 1;
- };
- kitty_handles = @splat(null);
- core.deinit();
- core = nc;
- }
-
- // live cwd for tags/look: cheap per-pane lookup, per frame
- for (&ptys, 0..) |*slot, id| if (slot.*) |pt| {
- var lbuf: [1024]u8 = undefined;
- if (look.shellCwd(pt.pid, &lbuf)) |cwd| core.setCwd(id, cwd);
- };
-
- // ---- render: surface -> vaxis, cell for cell ----
- _ = frame_arena.reset(.retain_capacity);
- const tz_core = tracy.zone(@src(), "core.render");
- const surface = try core.render(frame_arena.allocator());
- tz_core.end();
- const tz_cells = tracy.zone(@src(), "surface->vaxis");
- const win = vx.window();
- win.clear();
- var y: u16 = 0;
- while (y < surface.rows) : (y += 1) {
- var x: u16 = 0;
- while (x < surface.cols) : (x += 1) {
- const cell = surface.at(x, y);
- if (cell.default) continue;
- win.writeCell(x, y, .{
- .char = .{ .grapheme = cell.grapheme() },
- .style = vaxisStyle(cell.style),
- });
- }
- }
- tz_cells.end();
- // pixel attachments (kitty graphics): transmit once per pane, then
- // re-place every frame (placements aren't persistent)
- for (surface.images[0..surface.nimages]) |maybe| {
- const place = maybe orelse continue;
- if (kitty_handles[place.pane] == null and vx.caps.kitty_graphics) {
- const enc = std.base64.standard.Encoder;
- if (gpa.alloc(u8, enc.calcSize(place.rgba.len))) |b64| {
- defer gpa.free(b64);
- _ = enc.encode(b64, place.rgba);
- kitty_handles[place.pane] = vx.transmitPreEncodedImage(tty.writer(), b64, @intCast(place.iw), @intCast(place.ih), .rgba) catch null;
- } else |_| {}
- }
- if (kitty_handles[place.pane]) |h| {
- const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
- h.draw(child, .{ .scale = .contain }) catch {};
- }
- }
- if (surface.cursor) |cur| {
- win.showCursor(cur.x, cur.y);
- // insert = beam, everything else = the terminal's default shape
- win.setCursorShape(if (cur.bar) .beam else .default);
- }
- const tz_render = tracy.zone(@src(), "vx.render");
- try vx.render(tty.writer());
- tz_render.end();
- tracy.frameMark();
- }
-}
-
-fn drainEffects(
- core: *pardes.Pardes,
- ptys: *[pardes.MAX_PANES]?Pty,
- gens: *[pardes.MAX_PANES]u32,
- io: std.Io,
- gpa: std.mem.Allocator,
- loop: *Loop,
- vx: *vaxis.Vaxis,
- tty: *vaxis.Tty,
- lsp_task: *?std.Io.Future(anyerror!void),
- threads_ok: bool,
-) void {
- while (core.nextEffect()) |effect| switch (effect) {
- .spawn => |sp| {
- // the core reuses pane ids and there is no close effect: a deleted
- // pane's shell lives in its slot until a respawn lands here — reap
- // it (cancel joins the reader; its late eof is ignored by gen)
- if (ptys[sp.pane]) |*old| {
- old.reader.cancel(io) catch {};
- _ = libc.close(old.file.handle);
- ptys[sp.pane] = null;
- }
- gens[sp.pane] +%= 1;
- const cwd = sp.cwd.slice();
- var cwd_buf: [256:0]u8 = undefined;
- var cwd_z: ?[*:0]const u8 = null;
- if (cwd.len > 0) {
- @memcpy(cwd_buf[0..cwd.len], cwd);
- cwd_buf[cwd.len] = 0;
- cwd_z = @ptrCast(&cwd_buf);
- }
- const child = forkShell(cwd_z, core.screen_h, core.screen_w);
- ptys[sp.pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} } };
- // report the pane's starting directory back to the core (tags)
- var lbuf: [1024]u8 = undefined;
- if (look.shellCwd(child.pid, &lbuf)) |wd| core.setCwd(sp.pane, wd);
- if (threads_ok) {
- if (ptys[sp.pane]) |*pt| {
- pt.reader = io.concurrent(readPty, .{ io, gpa, pt.file, @as(usize, sp.pane), gens[sp.pane], loop }) catch pt.reader;
- }
- }
- },
- .write => |w| {
- if (ptys[w.pane]) |pt| writeFd(pt.file.handle, w.bytes.slice());
- },
- .resize_pty => |rs| {
- if (ptys[rs.pane]) |pt| {
- const ws: posix.winsize = .{ .row = rs.rows, .col = rs.cols, .xpixel = 0, .ypixel = 0 };
- _ = posix.system.ioctl(pt.file.handle, TIOCSWINSZ, @intFromPtr(&ws));
- }
- },
- .open_link => |url| look.openLink(url.slice()), // desktop browser
- .save_file => |sf| {
- const pane = core.panes[sf.pane] orelse continue;
- const f = pane.file orelse continue;
- var pathbuf: [4096:0]u8 = undefined;
- if (f.path.len >= pathbuf.len) continue;
- @memcpy(pathbuf[0..f.path.len], f.path);
- pathbuf[f.path.len] = 0;
- const fd = libc.open(pathbuf[0..f.path.len :0], .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
- if (fd < 0) continue;
- writeFd(fd, f.content);
- _ = libc.close(fd);
- },
- .write_dump => {
- const out = core.dump_out orelse continue;
- var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf) orelse continue;
- const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
- if (fd < 0) continue;
- writeFd(fd, out);
- _ = libc.close(fd);
- core.setLastDump(path);
- },
- .set_clipboard => {
- // mirror the core's yank register out via OSC 52
- if (core.yank) |y| vx.copyToSystemClipboard(tty.writer(), y, gpa) catch {};
- },
- .lsp => |q| {
- if (!threads_ok) continue; // pre-loop drain: nothing to answer to yet
- const pane = core.panes[q.pane] orelse continue;
- // 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;
- job.* = .{
- .id = q.id,
- .kind = q.kind,
- .offset = q.offset,
- .path = gpa.dupe(u8, if (f) |ff| ff.path else "") catch {
- gpa.destroy(job);
- continue;
- },
- .source = gpa.dupeZ(u8, if (f) |ff| ff.content else "") catch {
- gpa.free(job.path);
- gpa.destroy(job);
- continue;
- },
- .arg = gpa.dupe(u8, q.arg.slice()) catch {
- gpa.free(job.path);
- gpa.free(job.source);
- gpa.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);
- continue;
- },
- };
- // ponytail: ONE query in flight, so one future slot. Replacing it
- // cancels-then-joins the previous worker, which for a backend that
- // ignores cancellation means waiting out a query the user already
- // abandoned. Queries are milliseconds; make this a real pool the
- // day a backend takes long enough to notice.
- if (lsp_task.*) |*old| {
- old.cancel(io) catch {};
- lsp_task.* = null;
- }
- lsp_task.* = io.concurrent(lspWorker, .{ gpa, job, loop }) catch {
- job.free(gpa);
- continue;
- };
- },
- .quit => {},
- };
-}
-
-/// Answer a language query off the event loop and post the rows back. This is
-/// 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);
- defer arena.deinit();
- // the shell owns the result buffer; the backend only ever writes to it
- var out: std.Io.Writer.Allocating = .init(gpa);
- defer out.deinit();
- pardes.lsp.query(gpa, arena.allocator(), .{
- .kind = job.kind,
- .path = job.path,
- .source = job.source,
- .offset = job.offset,
- .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);
- return;
-}
-
-/// Consume SIGWINCH synchronously (it is blocked in every thread) and post
-/// the new size as a winsize event from normal thread context — the one place
-/// vaxis's Io-backed queue is safe to touch on a resize.
-fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
- var set = posix.sigemptyset();
- posix.sigaddset(&set, posix.SIG.WINCH);
- while (true) {
- var sig: c_int = 0;
- if (libc.sigwait(&set, &sig) != 0) continue;
- if (vx.state.in_band_resize) continue; // terminal reports via CSI 48
- const ws = tty.getWinsize() catch continue;
- loop.postEvent(.{ .winsize = ws }) catch {};
- }
-}
-
-fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.File, pid: posix.pid_t } {
- var master: c_int = undefined;
- const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
- const pid = forkpty(&master, null, null, &ws);
- if (pid == 0) {
- // the blocked-SIGWINCH mask survives fork AND exec — unblock it or
- // bash/vim in the pane would never see resizes (sigprocmask is
- // async-signal-safe)
- var set = posix.sigemptyset();
- posix.sigaddset(&set, posix.SIG.WINCH);
- posix.sigprocmask(posix.SIG.UNBLOCK, &set, null);
- if (cwd) |c| _ = chdir(c);
- const argv: [4:null]?[*:0]const u8 = .{ bash_path, "--rcfile", "/tmp/pardes-osc133.bash", null };
- _ = execv(bash_path, &argv);
- _exit(127);
- }
- return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid };
-}
-
-fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, id: usize, gen: u32, loop: *Loop) anyerror!void {
- var read_buf: [0x10000]u8 = undefined;
- var reader = pty.readerStreaming(io, &read_buf);
- while (true) {
- var buf: [0x10000]u8 = undefined;
- var vec = [_][]u8{&buf};
- const n = reader.interface.readVec(&vec) catch break;
- if (n == 0) break;
- const bytes = try gpa.dupe(u8, buf[0..n]);
- loop.postEvent(.{ .pty_read = .{ .id = id, .bytes = bytes } }) catch {
- gpa.free(bytes);
- break;
- };
- }
- // non-blocking: a teardown cancel only unblocks one wait, so a blocking
- // post into a full queue here could hang the exit
- _ = loop.tryPostEvent(.{ .pty_eof = .{ .id = id, .gen = gen } }) catch {};
-}
-
-/// The effective codepoint the way vaxis Key.matches sees it: a single-char
-/// text wins (shift resolved by the terminal), else the shifted codepoint.
-fn effCp(key: vaxis.Key) u21 {
- if (key.text) |t| {
- const view = std.unicode.Utf8View.init(t) catch return key.codepoint;
- var it = view.iterator();
- if (it.nextCodepoint()) |c| {
- if (it.nextCodepoint() == null) return c;
- }
- }
- return key.shifted_codepoint orelse key.codepoint;
-}
-
-/// vaxis functional-key codepoints -> core Key constants (ASCII ones already
-/// coincide: enter/tab/escape/backspace pass through).
-fn mapKey(cp: u21) u21 {
- return switch (cp) {
- vaxis.Key.up => pardes.Key.up,
- vaxis.Key.down => pardes.Key.down,
- vaxis.Key.left => pardes.Key.left,
- vaxis.Key.right => pardes.Key.right,
- vaxis.Key.home => pardes.Key.home,
- vaxis.Key.end => pardes.Key.end,
- vaxis.Key.page_up => pardes.Key.page_up,
- vaxis.Key.page_down => pardes.Key.page_down,
- vaxis.Key.delete => pardes.Key.delete,
- else => cp,
- };
-}
-
-fn vaxisStyle(s: pardes.CellStyle) vaxis.Style {
- return .{
- .fg = vaxisColor(s.fg),
- .bg = vaxisColor(s.bg),
- .bold = s.bold,
- .dim = s.dim,
- .italic = s.italic,
- .blink = s.blink,
- .reverse = s.reverse,
- .invisible = s.invisible,
- .strikethrough = s.strikethrough,
- .ul_style = switch (s.ul) {
- .off => .off,
- .single => .single,
- .double => .double,
- .curly => .curly,
- .dotted => .dotted,
- .dashed => .dashed,
- },
- };
-}
-
-fn vaxisColor(c: pardes.Color) vaxis.Color {
- return switch (c) {
- .default => .default,
- .index => |i| .{ .index = i },
- .rgb => |rgb| .{ .rgb = rgb },
- };
-}
-
-fn writeFile(data: []const u8) void {
- const fd = libc.open("/tmp/pardes-osc133.bash", .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
- if (fd < 0) return;
- defer _ = libc.close(fd);
- writeFd(fd, data);
-}
-
-fn writeFd(fd: c_int, data: []const u8) void {
- var off: usize = 0;
- while (off < data.len) {
- const n = libc.write(fd, data[off..].ptr, data.len - off);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- return;
- }
- off += @intCast(n);
- }
-}