summaryrefslogtreecommitdiff
path: root/src/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui.zig')
-rw-r--r--src/gui.zig154
1 files changed, 78 insertions, 76 deletions
diff --git a/src/gui.zig b/src/gui.zig
index 516a6b79..2653a056 100644
--- a/src/gui.zig
+++ b/src/gui.zig
@@ -1,6 +1,6 @@
//! The SDL3 GPU shell: owns an SDL window + event loop, translates SDL input
//! into core events, performs the core's effects (fork ptys, write them,
-//! resize them — same duties as tty.zig, this is also native linux), and
+//! resize them — same duties as tty.zig, this is also native), and
//! rasterizes the core's Surface: one instanced quad per cell, glyphs from an
//! stb_truetype R8 atlas. Test modes: PARDES_TEST_GRID=1 is headless (no SDL,
//! stdin escape sequences in, text grid frames out); PARDES_TEST=1 keeps the
@@ -13,10 +13,11 @@
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
-const linux = std.os.linux;
+const libc = std.c;
const vaxis = @import("vaxis"); // test modes only: the stdin escape-seq parser
const ghostty_vt = @import("ghostty-vt"); // 256-color palette for .index cells
const pardes = @import("pardes.zig");
+const look = @import("look.zig");
const is_emscripten = builtin.os.tag == .emscripten;
@@ -34,6 +35,12 @@ extern "c" fn _exit(status: c_int) noreturn;
extern "c" fn emscripten_get_element_css_size(target: [*:0]const u8, width: *f64, height: *f64) c_int;
const EMSCRIPTEN_RESULT_SUCCESS: c_int = 0;
+// 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));
+
const log = std.log.scoped(.gui);
const font_ttf = @embedFile("AdwaitaMono-Regular.ttf");
@@ -271,9 +278,9 @@ fn normCell(norm: f32, win: f32, cell: f32) u16 {
// ---- pty plumbing: reader threads feed a mutex-protected queue ----
-const Pty = struct { fd: c_int, pid: linux.pid_t };
+const Pty = struct { fd: c_int, pid: libc.pid_t };
-const Msg = union(enum) { output: struct { pane: u8, bytes: []u8 }, eof: u8 };
+const Msg = union(enum) { output: struct { pane: u8, gen: u32, bytes: []u8 }, eof: struct { pane: u8, gen: u32, fd: c_int } };
const Queue = struct {
gpa: std.mem.Allocator,
@@ -335,24 +342,23 @@ const Queue = struct {
}
};
-fn readPtyThread(gpa: std.mem.Allocator, fd: c_int, pane: u8, q: *Queue) void {
+fn readPtyThread(gpa: std.mem.Allocator, fd: c_int, pane: u8, gen: u32, q: *Queue) void {
var buf: [0x10000]u8 = undefined;
while (true) {
- const rc = linux.read(fd, &buf, buf.len);
- const n: isize = @bitCast(rc);
+ const n = libc.read(fd, &buf, buf.len);
if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
+ if (libc.errno(n) == .INTR) continue;
break; // EIO when the child exits: treat as EOF
}
if (n == 0) break;
const bytes = gpa.dupe(u8, buf[0..@intCast(n)]) catch break;
- q.push(.{ .output = .{ .pane = pane, .bytes = bytes } });
+ q.push(.{ .output = .{ .pane = pane, .gen = gen, .bytes = bytes } });
}
- q.push(.{ .eof = pane });
+ q.push(.{ .eof = .{ .pane = pane, .gen = gen, .fd = fd } });
}
-fn spawnReader(gpa: std.mem.Allocator, pt: Pty, pane: u8, q: *Queue) void {
- const th = std.Thread.spawn(.{}, readPtyThread, .{ gpa, pt.fd, pane, q }) catch return;
+fn spawnReader(gpa: std.mem.Allocator, pt: Pty, pane: u8, gen: u32, q: *Queue) void {
+ const th = std.Thread.spawn(.{}, readPtyThread, .{ gpa, pt.fd, pane, gen, q }) catch return;
th.detach();
}
@@ -621,8 +627,11 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
writeFile(pardes.bash_rc);
var ptys: [pardes.MAX_PANES]?Pty = @splat(null);
+ // per-slot spawn generation: drops a dead shell's late output/eof when its
+ // pane id has been respawned (see drainEffects .spawn)
+ var gens: [pardes.MAX_PANES]u32 = @splat(0);
defer for (&ptys) |*slot| if (slot.*) |pt| {
- _ = linux.close(pt.fd);
+ _ = libc.close(pt.fd);
slot.* = null;
};
var queue: Queue = .{ .gpa = gpa, .sdl_wake = true };
@@ -630,8 +639,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// initial spawns BEFORE any worker thread exists: forkpty from a
// multithreaded process can wedge the child before exec (see tty.zig).
- drainEffects(core, &ptys, gpa, &queue, &g, false);
- for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), &queue);
+ drainEffects(core, &ptys, &gens, gpa, &queue, &g, false);
+ for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), gens[id], &queue);
_ = c.SDL_StartTextInput(window);
@@ -657,22 +666,23 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var msgs = queue.take();
for (msgs.items) |m| switch (m) {
.output => |o| {
- core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
+ if (gens[o.pane] == o.gen)
+ core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
gpa.free(o.bytes);
},
- .eof => |id| {
- if (ptys[id]) |pt| {
- _ = linux.close(pt.fd);
- ptys[id] = null;
+ .eof => |e| {
+ _ = libc.close(e.fd); // the dead reader's master — stale or current
+ if (gens[e.pane] == e.gen) {
+ ptys[e.pane] = null;
+ core.update(.{ .eof = .{ .pane = e.pane } });
}
- core.update(.{ .eof = .{ .pane = id } });
},
};
msgs.deinit(gpa);
// 3. steamdeck: poll gamepad axes into virtual cursor / wheel events
pollGamepad(&g, core);
// 4. effects
- drainEffects(core, &ptys, gpa, &queue, &g, true);
+ drainEffects(core, &ptys, &gens, gpa, &queue, &g, true);
if (core.quit) break;
// 5. live cwd for tags/look: cheap /proc readlink per pane, per frame
pollCwds(core, &ptys);
@@ -894,14 +904,17 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
writeFile(pardes.bash_rc);
var ptys: [pardes.MAX_PANES]?Pty = @splat(null);
+ // per-slot spawn generation: drops a dead shell's late output/eof when its
+ // pane id has been respawned (see drainEffects .spawn)
+ var gens: [pardes.MAX_PANES]u32 = @splat(0);
defer for (&ptys) |*slot| if (slot.*) |pt| {
- _ = linux.close(pt.fd);
+ _ = libc.close(pt.fd);
slot.* = null;
};
var queue: Queue = .{ .gpa = gpa, .sdl_wake = false };
defer queue.close();
- drainEffects(core, &ptys, gpa, &queue, null, false);
- for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), &queue);
+ drainEffects(core, &ptys, &gens, gpa, &queue, null, false);
+ for (&ptys, 0..) |*slot, id| if (slot.*) |pt| spawnReader(gpa, pt, @intCast(id), gens[id], &queue);
setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort
@@ -921,21 +934,22 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
var msgs = queue.take();
for (msgs.items) |m| switch (m) {
.output => |o| {
- core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
+ if (gens[o.pane] == o.gen)
+ core.update(.{ .output = .{ .pane = o.pane, .bytes = o.bytes } });
gpa.free(o.bytes);
n_events += 1;
},
- .eof => |id| {
- if (ptys[id]) |pt| {
- _ = linux.close(pt.fd);
- ptys[id] = null;
+ .eof => |e| {
+ _ = libc.close(e.fd); // the dead reader's master — stale or current
+ if (gens[e.pane] == e.gen) {
+ ptys[e.pane] = null;
+ core.update(.{ .eof = .{ .pane = e.pane } });
}
- core.update(.{ .eof = .{ .pane = id } });
n_events += 1;
},
};
msgs.deinit(gpa);
- drainEffects(core, &ptys, gpa, &queue, null, true);
+ drainEffects(core, &ptys, &gens, gpa, &queue, null, true);
pollCwds(core, &ptys);
if (n_events == 0) continue; // idle tick: nothing changed, no frame
_ = frame_arena.reset(.retain_capacity);
@@ -981,8 +995,7 @@ const StdinFeed = struct {
if ((fds[0].revents & (posix.POLL.HUP | posix.POLL.ERR)) != 0) out.eof = true;
return out;
}
- const rc = linux.read(0, f.buf[f.fill..].ptr, f.buf.len - f.fill);
- const rn: isize = @bitCast(rc);
+ const rn = libc.read(0, f.buf[f.fill..].ptr, f.buf.len - f.fill);
if (rn < 0) return out;
if (rn == 0) {
out.eof = true;
@@ -1413,6 +1426,7 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void {
fn drainEffects(
core: *pardes.Pardes,
ptys: *[pardes.MAX_PANES]?Pty,
+ gens: *[pardes.MAX_PANES]u32,
gpa: std.mem.Allocator,
queue: *Queue,
g: ?*Gui, // null in grid test mode (no SDL: clipboard effects are no-ops)
@@ -1420,7 +1434,15 @@ fn drainEffects(
) void {
while (core.nextEffect()) |effect| switch (effect) {
.spawn => |sp| {
- std.debug.assert(ptys[sp.pane] == null);
+ // 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. Kill
+ // it; its detached reader wakes on child death and the gen-guarded
+ // eof closes the old fd (not here — the reader still reads it).
+ if (ptys[sp.pane]) |old| {
+ _ = libc.kill(old.pid, libc.SIG.KILL);
+ 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;
@@ -1433,13 +1455,8 @@ fn drainEffects(
ptys[sp.pane] = pt;
// report the pane's starting directory back to the core (tags)
var lbuf: [1024]u8 = undefined;
- var pbuf: [64]u8 = undefined;
- if (std.fmt.bufPrintSentinel(&pbuf, "/proc/{d}/cwd", .{pt.pid}, 0) catch null) |path| {
- const rc = linux.readlinkat(linux.AT.FDCWD, path, &lbuf, lbuf.len);
- const n: isize = @bitCast(rc);
- if (n > 0) core.setCwd(sp.pane, lbuf[0..@intCast(n)]);
- }
- if (threads_ok) spawnReader(gpa, pt, sp.pane, queue);
+ if (look.shellCwd(pt.pid, &lbuf)) |wd| core.setCwd(sp.pane, wd);
+ if (threads_ok) spawnReader(gpa, pt, sp.pane, gens[sp.pane], queue);
},
.write => |w| {
if (ptys[w.pane]) |pt| writeFd(pt.fd, w.bytes.slice());
@@ -1447,7 +1464,7 @@ fn drainEffects(
.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.fd, posix.T.IOCSWINSZ, @intFromPtr(&ws));
+ _ = posix.system.ioctl(pt.fd, TIOCSWINSZ, @intFromPtr(&ws));
}
},
.open_link => {}, // native: look opens panes instead; nothing to launch
@@ -1458,21 +1475,17 @@ fn drainEffects(
if (f.path.len >= pathbuf.len) continue;
@memcpy(pathbuf[0..f.path.len], f.path);
pathbuf[f.path.len] = 0;
- const rc = linux.open(pathbuf[0..f.path.len :0], .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) continue;
- const fd: c_int = @intCast(sfd);
+ 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);
- _ = linux.close(fd);
+ _ = libc.close(fd);
},
.write_dump => {
const out = core.dump_out orelse continue;
- const rc = linux.open(pardes.dump.default_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) continue;
- const fd: c_int = @intCast(sfd);
+ const fd = libc.open(pardes.dump.default_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
+ if (fd < 0) continue;
writeFd(fd, out);
- _ = linux.close(fd);
+ _ = libc.close(fd);
},
.set_clipboard => {
if (g == null) continue;
@@ -1500,8 +1513,8 @@ fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) Pty {
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
if (cwd) |cd| _ = chdir(cd);
- const argv: [4:null]?[*:0]const u8 = .{ "/usr/bin/bash", "--rcfile", "/tmp/pardes-osc133.bash", null };
- _ = execv("/usr/bin/bash", &argv);
+ const argv: [4:null]?[*:0]const u8 = .{ bash_path, "--rcfile", "/tmp/pardes-osc133.bash", null };
+ _ = execv(bash_path, &argv);
_exit(127);
}
return .{ .fd = master, .pid = pid };
@@ -1510,11 +1523,7 @@ fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) Pty {
fn pollCwds(core: *pardes.Pardes, ptys: *[pardes.MAX_PANES]?Pty) void {
for (ptys, 0..) |slot, id| if (slot) |pt| {
var lbuf: [1024]u8 = undefined;
- var pbuf: [64]u8 = undefined;
- const path = std.fmt.bufPrintSentinel(&pbuf, "/proc/{d}/cwd", .{pt.pid}, 0) catch continue;
- const rc = linux.readlinkat(linux.AT.FDCWD, path, &lbuf, lbuf.len);
- const n: isize = @bitCast(rc);
- if (n > 0) core.setCwd(id, lbuf[0..@intCast(n)]);
+ if (look.shellCwd(pt.pid, &lbuf)) |cwd| core.setCwd(id, cwd);
};
}
@@ -2073,11 +2082,9 @@ fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u
const tmp_path = std.fmt.bufPrintSentinel(&tmp_buf, "{s}/latest.ppm.tmp", .{g.capture_dir}, 0) catch return error.CapturePathTooLong;
const final_path = std.fmt.bufPrintSentinel(&final_buf, "{s}/latest.ppm", .{g.capture_dir}, 0) catch return error.CapturePathTooLong;
- const orc = linux.open(tmp_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const sfd: isize = @bitCast(orc);
- if (sfd < 0) return error.CaptureWriteFailed;
- const fd: c_int = @intCast(sfd);
- defer _ = linux.close(fd);
+ const fd = libc.open(tmp_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
+ if (fd < 0) return error.CaptureWriteFailed;
+ defer _ = libc.close(fd);
var header: [64]u8 = undefined;
const hdr = std.fmt.bufPrint(&header, "P6\n{d} {d}\n255\n", .{ width, height }) catch return error.CaptureWriteFailed;
@@ -2096,9 +2103,7 @@ fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u
}
writeFd(fd, row_rgb);
}
- const rrc = linux.rename(tmp_path, final_path);
- const rs: isize = @bitCast(rrc);
- if (rs < 0) return error.CaptureWriteFailed;
+ if (libc.rename(tmp_path, final_path) != 0) return error.CaptureWriteFailed;
}
// =====================================================================
@@ -2377,21 +2382,18 @@ fn envU16(env: *std.process.Environ.Map, name: []const u8) ?u16 {
}
fn writeFile(data: []const u8) void {
- const rc = linux.open("/tmp/pardes-osc133.bash", .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) return;
- const fd: c_int = @intCast(sfd);
- defer _ = linux.close(fd);
+ 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 rc = linux.write(fd, data[off..].ptr, data.len - off);
- const n: isize = @bitCast(rc);
+ const n = libc.write(fd, data[off..].ptr, data.len - off);
if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
+ if (libc.errno(n) == .INTR) continue;
return;
}
off += @intCast(n);