summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig33
-rw-r--r--main.zig324
-rw-r--r--term.zig11
-rw-r--r--ui_sdl.zig201
4 files changed, 400 insertions, 169 deletions
diff --git a/build.zig b/build.zig
index fa987360..b3ae8e37 100644
--- a/build.zig
+++ b/build.zig
@@ -57,18 +57,21 @@ pub fn build(b: *std.Build) void {
, .{ sysroot, sysroot, sysroot }));
} else null;
+ const exe_name = targetBinaryName(b, "pardes", target.result);
+ const replay_name = targetBinaryName(b, "pardes-replay", target.result);
const exe = b.addExecutable(.{
- .name = "pardes",
+ .name = exe_name,
.linkage = exe_linkage,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("main.zig"),
.strip = strip,
+ .link_libc = true,
}),
});
const replay = b.addExecutable(.{
- .name = "pardes-replay",
+ .name = replay_name,
.linkage = exe_linkage,
.root_module = b.createModule(.{
.target = target,
@@ -146,12 +149,16 @@ pub fn build(b: *std.Build) void {
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
var ghostty_vt_for_tests: ?*std.Build.Module = null;
+ const ghostty_simd = switch (target.result.os.tag) {
+ .emscripten, .openbsd => false,
+ else => true,
+ };
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
- .simd = !is_emscripten,
+ .simd = ghostty_simd,
})) |dep| {
// Accessing ghostty's modules runs its build(), which (because vaxis is
// a shared dependency, deduplicated to the same module) wires ghostty's
@@ -664,6 +671,26 @@ fn detectEmscriptenSysroot(b: *std.Build) ?[]const u8 {
return null;
}
+fn targetBinaryName(b: *std.Build, base: []const u8, target: std.Target) []const u8 {
+ return switch (target.os.tag) {
+ .linux, .emscripten => base,
+ .macos, .freebsd, .openbsd, .netbsd, .dragonfly => b.fmt("{s}-{s}-{s}", .{
+ base,
+ @tagName(target.os.tag),
+ archName(target.cpu.arch),
+ }),
+ else => base,
+ };
+}
+
+fn archName(arch: std.Target.Cpu.Arch) []const u8 {
+ return switch (arch) {
+ .x86_64 => "x64",
+ .aarch64 => "arm64",
+ else => @tagName(arch),
+ };
+}
+
fn envPath(b: *std.Build, name: []const u8) ?[]const u8 {
return b.graph.environ_map.get(name);
}
diff --git a/main.zig b/main.zig
index 742d43b4..dbd35f68 100644
--- a/main.zig
+++ b/main.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const builtin = @import("builtin");
const vaxis = @import("ui");
const ghostty_vt = @import("ghostty-vt");
const Term = @import("term.zig");
@@ -8,6 +9,11 @@ const dump = @import("dump.zig");
const paint = @import("paint.zig");
const pane_ui = @import("pane_ui.zig");
+const os_tag = builtin.os.tag;
+const posix = std.posix;
+const linux = std.os.linux;
+const shell_reports_cwd = os_tag == .macos;
+
// A TUI can't let std.log hit stderr — it's the same tty as our alt-screen, so the
// message lands on top of the UI (this is the stray text you saw when opening yazi:
// ghostty-vt's parser warns via std.log about sequences it doesn't implement, e.g.
@@ -15,7 +21,7 @@ const pane_ui = @import("pane_ui.zig");
// stays clean and the messages are still there to inspect.
pub const std_options: std.Options = .{ .logFn = fileLog };
-var log_fd: i32 = -1;
+var log_fd: posix.fd_t = -1;
fn fileLog(
comptime level: std.log.Level,
comptime scope: @EnumLiteral(),
@@ -23,21 +29,85 @@ fn fileLog(
args: anytype,
) void {
if (log_fd < 0) {
- const rc = std.os.linux.open("/tmp/pardes.log", .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true, .APPEND = true }, 0o644);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) return; // can't open the log: drop the message (never touch stderr)
- log_fd = @intCast(sfd);
+ log_fd = openCreateTrunc("/tmp/pardes.log", true) orelse return;
}
var buf: [4096]u8 = undefined;
const prefix = "[" ++ @tagName(level) ++ "] " ++ @tagName(scope) ++ ": ";
const line = std.fmt.bufPrint(&buf, prefix ++ format ++ "\n", args) catch return;
- _ = std.os.linux.write(log_fd, line.ptr, line.len);
+ writeFdBestEffort(log_fd, line);
}
fn nowNs() u64 {
- var ts: std.os.linux.timespec = undefined;
- _ = std.os.linux.clock_gettime(std.os.linux.CLOCK.MONOTONIC, &ts);
- return @intCast(ts.sec * 1_000_000_000 + ts.nsec);
+ switch (os_tag) {
+ .linux => {
+ var ts: linux.timespec = undefined;
+ _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
+ return @intCast(ts.sec * 1_000_000_000 + ts.nsec);
+ },
+ else => {
+ var ts: posix.timespec = undefined;
+ switch (posix.errno(posix.system.clock_gettime(posix.CLOCK.MONOTONIC, &ts))) {
+ .SUCCESS => return @intCast(ts.sec * 1_000_000_000 + ts.nsec),
+ else => return 0,
+ }
+ },
+ }
+}
+
+fn openCreateTrunc(path: [*:0]const u8, append: bool) ?posix.fd_t {
+ switch (os_tag) {
+ .linux => {
+ const rc = linux.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true, .APPEND = append }, 0o644);
+ const sfd: isize = @bitCast(rc);
+ return if (sfd < 0) null else @intCast(sfd);
+ },
+ else => return posix.openatZ(posix.AT.FDCWD, path, .{
+ .ACCMODE = .WRONLY,
+ .CREAT = true,
+ .TRUNC = true,
+ .APPEND = append,
+ }, 0o644) catch null,
+ }
+}
+
+fn closeFdBestEffort(fd: posix.fd_t) void {
+ switch (os_tag) {
+ .linux => _ = linux.close(fd),
+ else => switch (posix.errno(posix.system.close(fd))) {
+ .SUCCESS, .INTR => {},
+ else => {},
+ },
+ }
+}
+
+fn writeFdBestEffort(fd: posix.fd_t, bytes: []const u8) void {
+ var off: usize = 0;
+ while (off < bytes.len) {
+ switch (os_tag) {
+ .linux => {
+ const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
+ const n: isize = @bitCast(rc);
+ if (n < 0) {
+ if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
+ return;
+ }
+ if (n == 0) return;
+ off += @intCast(n);
+ },
+ else => {
+ const rc = posix.system.write(fd, bytes[off..].ptr, bytes.len - off);
+ switch (posix.errno(rc)) {
+ .SUCCESS => {
+ const n: usize = @intCast(rc);
+ if (n == 0) return;
+ off += n;
+ },
+ .INTR => continue,
+ else => return,
+ }
+ },
+ }
+ }
}
extern "c" fn forkpty(
@@ -268,13 +338,7 @@ const file_pane_tail = " " ++ file_pane_builtins_str;
const topbar_str = "Kill Newcol Tutor Debug Colors NextColor Dump";
fn writeStdoutBestEffort(bytes: []const u8) void {
- var off: usize = 0;
- while (off < bytes.len) {
- const rc = std.os.linux.write(1, bytes[off..].ptr, bytes.len - off);
- const n: isize = @bitCast(rc);
- if (n <= 0) return;
- off += @intCast(n);
- }
+ writeFdBestEffort(1, bytes);
}
fn paintStartupTopbar() void {
@@ -296,7 +360,13 @@ fn writeStartupPerfMarker(start_ns: u64) void {
// repositioning of the shell cursor within the typed input) so enterTty can
// land the shell cursor where the modal cursor navigated (start vs end).
const rc_path = "/tmp/pardes-osc133.bash";
-const bash_rc =
+const bash_rc = if (shell_reports_cwd)
+ \\[ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc"
+ \\PS1='\[\e]7;file://localhost$PWD\a\]\[\e]133;A;cl=line\a\]'"$PS1"'\[\e]133;B\a\]'
+ \\PROMPT_COMMAND='printf "\e]133;D\a"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
+ \\trap 'printf "\e]133;C\a"' DEBUG
+ \\
+else
\\[ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc"
\\PS1='\[\e]133;A;cl=line\a\]'"$PS1"'\[\e]133;B\a\]'
\\PROMPT_COMMAND='printf "\e]133;D\a"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
@@ -305,19 +375,9 @@ const bash_rc =
;
fn writeFile(path: [*:0]const u8, data: []const u8) void {
- const l = std.os.linux;
- const rc = l.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) return;
- const fd: i32 = @intCast(sfd);
- defer _ = l.close(fd);
- var off: usize = 0;
- while (off < data.len) {
- const w = l.write(fd, data[off..].ptr, data.len - off);
- const sw: isize = @bitCast(w);
- if (sw <= 0) break;
- off += @intCast(sw);
- }
+ const fd = openCreateTrunc(path, false) orelse return;
+ defer closeFdBestEffort(fd);
+ writeFdBestEffort(fd, data);
}
fn saveFile(io: std.Io, f: *Term.File) !void {
@@ -341,17 +401,7 @@ fn fork_pty(argv: []const ?[*:0]const u8, cwd: ?[*:0]const u8, ws: std.posix.win
fn termWrite(t: *Term, bytes: []const u8) void {
if (t.pty.handle < 0) return; // file pane: no pty
- var off: usize = 0;
- while (off < bytes.len) {
- const rc = std.os.linux.write(t.pty.handle, bytes[off..].ptr, bytes.len - off);
- // raw syscall: -errno, not the libc -1 sentinel. Check the sign.
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(std.os.linux.E.INTR))) continue;
- break;
- }
- off += @intCast(n);
- }
+ writeFdBestEffort(t.pty.handle, bytes);
}
fn fileLineCount(content: []const u8) usize {
@@ -446,30 +496,47 @@ fn padOutputBelowEdits(t: *Term) void {
while (i < pad) : (i += 1) termWrite(t, "\r");
}
-// The shell's current working directory (via /proc/<pid>/cwd). `buf` holds it.
-// A raw linux syscall returns -errno; check the sign directly. (std.posix.errno
-// is the LIBC convention here — `rc == -1` — and we link libc, so it misreads a
-// raw -2/ENOENT as success and we'd slice buf[0..~2^64]. file panes (pid 0) and
-// exited shells both hit this.)
+// The shell's current working directory. Linux keeps the strongest existing
+// behavior by reading /proc/<pid>/cwd, which follows external `cd` commands
+// without shell cooperation. macOS uses OSC 7 emitted by our bash rc. BSDs use
+// the spawn cwd cached on Term until a native live-cwd implementation is added.
fn termCwd(t: *Term, buf: []u8) []const u8 {
if (!t.isTerminal()) return "?"; // file/image pane: no shell, no cwd
- var pathbuf: [64]u8 = undefined;
- const path = std.fmt.bufPrintZ(&pathbuf, "/proc/{d}/cwd", .{t.pid}) catch return "?";
- const rc = std.os.linux.readlinkat(std.os.linux.AT.FDCWD, path, buf.ptr, buf.len);
- const n: isize = @bitCast(rc);
- if (n < 0) return "?";
- return buf[0..@intCast(n)];
+ switch (os_tag) {
+ .linux => {
+ var pathbuf: [64]u8 = undefined;
+ const path = std.fmt.bufPrintZ(&pathbuf, "/proc/{d}/cwd", .{t.pid}) catch return "?";
+ const rc = linux.readlinkat(linux.AT.FDCWD, path, buf.ptr, buf.len);
+ const n: isize = @bitCast(rc);
+ if (n < 0) return "?";
+ return buf[0..@intCast(n)];
+ },
+ .macos, .freebsd, .openbsd, .netbsd, .dragonfly => return t.cwd orelse "?",
+ else => return t.cwd orelse "?",
+ }
}
// True if `path` exists and is a directory. open(O_DIRECTORY) fails with ENOTDIR
// for anything else, so no stat struct needed.
fn isDir(path: [*:0]const u8) bool {
- const l = std.os.linux;
- const rc = l.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) return false;
- _ = l.close(@intCast(sfd));
- return true;
+ switch (os_tag) {
+ .linux => {
+ const rc = linux.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0);
+ const sfd: isize = @bitCast(rc);
+ if (sfd < 0) return false;
+ _ = linux.close(@intCast(sfd));
+ return true;
+ },
+ else => {
+ const fd = posix.openatZ(posix.AT.FDCWD, path, .{
+ .ACCMODE = .RDONLY,
+ .DIRECTORY = true,
+ .CLOEXEC = true,
+ }, 0) catch return false;
+ closeFdBestEffort(fd);
+ return true;
+ },
+ }
}
// The LIVE prefix of a window tag: mode indicator + cwd/path. Regenerated every
@@ -736,8 +803,20 @@ fn wordAtCol(str: []const u8, col: u16) []const u8 {
// TIOCSWINSZ: set the pty size; the kernel raises SIGWINCH in the child so it
// (and anything running in it) reflows to the new grid.
fn pty_set_size(pty: std.Io.File, rows: u16, cols: u16) void {
- const ws: std.posix.winsize = .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
- _ = std.posix.system.ioctl(pty.handle, std.posix.T.IOCSWINSZ, @intFromPtr(&ws));
+ const ws: posix.winsize = .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
+ _ = posix.system.ioctl(pty.handle, tiocswinszReq(), @intFromPtr(&ws));
+}
+
+fn ioctlReq(bits: u32) c_int {
+ return @bitCast(bits);
+}
+
+fn tiocswinszReq() c_int {
+ return switch (os_tag) {
+ .linux => @intCast(posix.T.IOCSWINSZ),
+ .macos, .freebsd, .openbsd, .netbsd, .dragonfly => ioctlReq(0x80087467),
+ else => @intCast(posix.T.IOCSWINSZ),
+ };
}
// One reader task per terminal. Tags each chunk with the terminal's slot id so
@@ -763,6 +842,7 @@ fn feedPtyRead(gpa: std.mem.Allocator, terms: *[MAX_TERMS]?*Term, active: usize,
defer gpa.free(pr.bytes);
if (pr.id >= MAX_TERMS) return;
if (terms[pr.id]) |t| {
+ if (shell_reports_cwd) updateCwdFromOsc7(gpa, t, pr.bytes);
t.tty_stream.appendSlice(gpa, pr.bytes) catch {};
t.stream.nextSlice(pr.bytes);
// follow new output to the bottom, so a command's result shows even in a
@@ -773,11 +853,83 @@ fn feedPtyRead(gpa: std.mem.Allocator, terms: *[MAX_TERMS]?*Term, active: usize,
}
}
+fn updateCwdFromOsc7(gpa: std.mem.Allocator, t: *Term, bytes: []const u8) void {
+ if (!t.isTerminal()) return;
+ for (bytes) |b| switch (t.cwd_osc_state) {
+ .normal => {
+ if (b == 0x1b) t.cwd_osc_state = .esc;
+ },
+ .esc => {
+ t.cwd_osc_state = if (b == ']') .osc else if (b == 0x1b) .esc else .normal;
+ },
+ .osc => {
+ t.cwd_osc_state = if (b == '7') .osc7 else if (b == 0x1b) .esc else .normal;
+ },
+ .osc7 => {
+ if (b == ';') {
+ t.cwd_osc.clearRetainingCapacity();
+ t.cwd_osc_state = .collect;
+ } else {
+ t.cwd_osc_state = if (b == 0x1b) .esc else .normal;
+ }
+ },
+ .collect => switch (b) {
+ 0x07 => {
+ finishCwdOsc(gpa, t);
+ },
+ 0x1b => {
+ t.cwd_osc_state = .st_esc;
+ },
+ else => {
+ if (t.cwd_osc.items.len < std.fs.max_path_bytes) {
+ t.cwd_osc.append(gpa, b) catch {
+ t.cwd_osc_state = .normal;
+ };
+ } else {
+ t.cwd_osc.clearRetainingCapacity();
+ t.cwd_osc_state = .normal;
+ }
+ },
+ },
+ .st_esc => {
+ if (b == '\\') finishCwdOsc(gpa, t) else t.cwd_osc_state = .normal;
+ },
+ };
+}
+
+fn finishCwdOsc(gpa: std.mem.Allocator, t: *Term) void {
+ defer {
+ t.cwd_osc.clearRetainingCapacity();
+ t.cwd_osc_state = .normal;
+ }
+ const path = osc7Path(t.cwd_osc.items) orelse return;
+ if (path.len == 0 or path[0] != '/') return;
+ const next = gpa.dupe(u8, path) catch return;
+ if (t.cwd) |old| gpa.free(old);
+ t.cwd = next;
+}
+
+fn osc7Path(raw: []const u8) ?[]const u8 {
+ if (std.mem.startsWith(u8, raw, "file://")) {
+ const rest = raw["file://".len..];
+ const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null;
+ return rest[slash..];
+ }
+ if (std.mem.startsWith(u8, raw, "file:")) return raw["file:".len..];
+ return raw;
+}
+
// fork the pty + boot the emulator. Forking is the dangerous part in a
// multithreaded process, so this does NOT start the reader task (no new
// threads) — call startReader once the term is wired in.
-fn forkTerm(gpa: std.mem.Allocator, init_cols: u16, init_rows: u16, cwd: ?[*:0]const u8) !*Term {
+fn forkTerm(io: std.Io, gpa: std.mem.Allocator, init_cols: u16, init_rows: u16, cwd: ?[*:0]const u8) !*Term {
const argv = [_]?[*:0]const u8{ "/usr/bin/bash", "--rcfile", rc_path };
+ const cwd_copy = if (cwd) |c| try gpa.dupe(u8, std.mem.span(c)) else blk: {
+ var current: [std.fs.max_path_bytes]u8 = undefined;
+ const n = try std.process.currentPath(io, &current);
+ break :blk try gpa.dupe(u8, current[0..n]);
+ };
+ errdefer gpa.free(cwd_copy);
const child = fork_pty(&argv, cwd, .{ .row = init_rows, .col = init_cols, .xpixel = 0, .ypixel = 0 });
const t = try gpa.create(Term);
errdefer gpa.destroy(t);
@@ -787,6 +939,7 @@ fn forkTerm(gpa: std.mem.Allocator, init_cols: u16, init_rows: u16, cwd: ?[*:0]c
.term = try ghostty_vt.Terminal.init(gpa, .{ .cols = init_cols, .rows = init_rows, .max_scrollback = 16 * 1024 * 1024 }),
.stream = undefined,
.reader = .{ .any_future = null, .result = {} }, // no-op until startReader
+ .cwd = cwd_copy,
.cols = init_cols,
.rows = init_rows,
};
@@ -839,29 +992,8 @@ fn parsePathLine(tok: []const u8) struct { path: []const u8, line: usize } {
return .{ .path = tok, .line = 0 };
}
-// slurp a whole file via linux syscalls (kept flat; no std.fs). Caller frees.
-fn readFileAlloc(gpa: std.mem.Allocator, path: [*:0]const u8) ![]u8 {
- const l = std.os.linux;
- const rc = l.open(path, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0);
- const sfd: isize = @bitCast(rc);
- if (sfd < 0) return error.OpenFailed;
- const fd: i32 = @intCast(sfd);
- defer _ = l.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- errdefer buf.deinit(gpa);
- var chunk: [64 * 1024]u8 = undefined;
- while (true) {
- const rrc = l.read(fd, &chunk, chunk.len);
- // raw syscall: -errno, not the libc -1 sentinel. Check the sign.
- const n: isize = @bitCast(rrc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(l.E.INTR))) continue;
- return error.ReadFailed;
- }
- if (n == 0) break;
- try buf.appendSlice(gpa, chunk[0..@intCast(n)]);
- }
- return buf.toOwnedSlice(gpa);
+fn readFileAlloc(io: std.Io, gpa: std.mem.Allocator, path: [*:0]const u8) ![]u8 {
+ return std.Io.Dir.cwd().readFileAlloc(io, std.mem.span(path), gpa, .limited(std.math.maxInt(usize)));
}
fn refreshFileHighlightsVisible(gpa: std.mem.Allocator, f: *Term.File, rows: usize) void {
@@ -927,8 +1059,8 @@ fn setFileContentStaleSyntax(gpa: std.mem.Allocator, f: *Term.File, new: []u8) v
// layout/selection/edit machinery. No pty (handle -1), no reader, a stub 1x1
// emulator (only its allocator-owned bits are touched). `line` (1-based, 0=none)
// scrolls the target near the top. Caller wires it into the layout + frees later.
-fn openFileView(gpa: std.mem.Allocator, rp: [*:0]const u8, line: usize, cols: u16, rows: u16) !*Term {
- const content = try readFileAlloc(gpa, rp);
+fn openFileView(io: std.Io, gpa: std.mem.Allocator, rp: [*:0]const u8, line: usize, cols: u16, rows: u16) !*Term {
+ const content = try readFileAlloc(io, gpa, rp);
errdefer gpa.free(content);
const path = try gpa.dupe(u8, std.mem.span(rp));
errdefer gpa.free(path);
@@ -1102,7 +1234,7 @@ fn ttyForDir(
} else return null;
var dirbuf: [1024]u8 = undefined;
const dirz = std.fmt.bufPrintZ(&dirbuf, "{s}", .{dir}) catch return null;
- const nt = forkTerm(gpa, sw, sh, dirz.ptr) catch return null;
+ const nt = forkTerm(io, gpa, sw, sh, dirz.ptr) catch return null;
terms[free] = nt;
startReader(io, gpa, loop, nt, free) catch {};
const rc = if (layout.ncol > 0) layout.ncol - 1 else 0;
@@ -2449,7 +2581,7 @@ fn actOnSelection(
if (sl == null) break i;
} else null;
if (slot) |free| {
- if (forkTerm(gpa, sw, sh, rp)) |nt| {
+ if (forkTerm(io, gpa, sw, sh, rp)) |nt| {
terms[free] = nt;
startReader(io, gpa, loop, nt, free) catch {};
const f = layoutFindTerm(layout, id).?;
@@ -2492,7 +2624,7 @@ fn actOnSelection(
const opened: ?*Term = if (as_image)
(openImageView(gpa, rp, sw, sh, !images_ok) catch null) // petscii by default when no kitty
else
- (openFileView(gpa, rp, pl.line, sw, sh) catch null);
+ (openFileView(io, gpa, rp, pl.line, sw, sh) catch null);
if (opened) |nt| {
terms[free] = nt;
// If a doc pane (file/image) already exists, SPLIT it
@@ -2579,7 +2711,7 @@ fn actOnSelection(
if (sl == null) break i;
} else null;
if (slot) |free| if (layout.ncol < MAX_COLS) {
- if (forkTerm(gpa, sw, sh, null)) |nt| {
+ if (forkTerm(io, gpa, sw, sh, null)) |nt| {
terms[free] = nt;
startReader(io, gpa, loop, nt, free) catch {};
layoutAppendColumn(layout, free);
@@ -2740,16 +2872,16 @@ pub fn main(init: std.process.Init) !void {
// it while still single-threaded avoids that.
if (tty_only) {
// tmux-style: a single shell, already in tty mode, filling one column.
- terms[0] = try forkTerm(gpa, screen_w, screen_h, null);
+ terms[0] = try forkTerm(io, gpa, screen_w, screen_h, null);
terms[0].?.mode = .tty;
layout.ncol = 1;
layout.col_n[0] = 1;
layout.col_terms[0][0] = 0;
} else {
// editor default: two columns, the left one split into two windows.
- terms[0] = try forkTerm(gpa, screen_w, screen_h, null);
- terms[1] = try forkTerm(gpa, screen_w, screen_h, null);
- terms[2] = try forkTerm(gpa, screen_w, screen_h, null);
+ terms[0] = try forkTerm(io, gpa, screen_w, screen_h, null);
+ terms[1] = try forkTerm(io, gpa, screen_w, screen_h, null);
+ terms[2] = try forkTerm(io, gpa, screen_w, screen_h, null);
// greet each fresh shell with `ls` once it's been sized (see the resize
// loop): sending it now wraps the listing to the full-screen width.
for (terms[0..3]) |slot| if (slot) |t| {
@@ -3115,7 +3247,7 @@ pub fn main(init: std.process.Init) !void {
if (s == null) break i;
} else null;
if (free_slot) |slot| {
- if (forkTerm(gpa, screen_w, screen_h, null)) |nt| {
+ if (forkTerm(io, gpa, screen_w, screen_h, null)) |nt| {
terms[slot] = nt;
startReader(io, gpa, &loop, nt, slot) catch {};
const f = layoutFindTerm(&layout, active).?;
@@ -3646,7 +3778,7 @@ fn dumpSnapshot(
},
} else if (t.image) |iv| blk: {
const pz = try std.fmt.allocPrintSentinel(alloc, "{s}", .{iv.path}, 0);
- const bytes = readFileAlloc(alloc, pz) catch "";
+ const bytes = readFileAlloc(io, alloc, pz) catch "";
break :blk .{
.kind = .image,
.tag = tag,
diff --git a/term.zig b/term.zig
index cb451b7c..7201c7c7 100644
--- a/term.zig
+++ b/term.zig
@@ -84,6 +84,8 @@ pub const EditSnap = struct {
cur_off: u16,
};
+pub const CwdOscState = enum { normal, esc, osc, osc7, collect, st_esc };
+
/// Persistent per-terminal state: the pty handle, the ghostty emulator and its
/// (stateful) VT parser, the background reader task, plus its layout weight and
/// text selection.
@@ -94,10 +96,15 @@ pub const EditSnap = struct {
pub const Term = @This();
pty: std.Io.File,
-pid: std.posix.pid_t, // the shell child; used to read /proc/<pid>/cwd for the tag
+pid: std.posix.pid_t, // shell child; Linux uses /proc/<pid>/cwd for live cwd tags
term: ghostty_vt.Terminal,
stream: ghostty_vt.TerminalStream,
reader: std.Io.Future(anyerror!void),
+/// Last known shell cwd for platforms without Linux procfs. macOS updates this
+/// from OSC 7 emitted by our shell rc; BSDs use the spawn cwd as a fallback.
+cwd: ?[]u8 = null,
+cwd_osc_state: CwdOscState = .normal,
+cwd_osc: std.ArrayList(u8) = .empty,
/// When set, this pane is a (read/editable) text FILE view, not a terminal: its
/// body comes from `content` instead of the emulator, and all the tty-specific
@@ -196,6 +203,8 @@ pub fn deinit(self: *Term, io: std.Io, gpa: std.mem.Allocator) void {
self.pty.close(io);
}
self.tty_stream.deinit(gpa);
+ if (self.cwd) |cwd| gpa.free(cwd);
+ self.cwd_osc.deinit(gpa);
self.ed_undo.deinit(gpa);
self.ed_redo.deinit(gpa);
self.tag_tail.deinit(gpa);
diff --git a/ui_sdl.zig b/ui_sdl.zig
index 0b2a2fd2..ee7f118c 100644
--- a/ui_sdl.zig
+++ b/ui_sdl.zig
@@ -25,6 +25,7 @@ const vaxis = @import("vaxis");
const ui_sdl_config = @import("ui_sdl_config");
const posix = std.posix;
const linux = std.os.linux;
+const os_tag = builtin.os.tag;
const is_emscripten = builtin.os.tag == .emscripten;
pub const c = @cImport({
@@ -150,7 +151,7 @@ capture_xfer_size: u32 = 0,
capture_frame: u64 = 0,
trace_enabled: bool = false,
-trace_fd: c_int = -1,
+trace_fd: posix.fd_t = -1,
trace_every: u32 = 1,
trace_frame: u64 = 0,
@@ -233,9 +234,93 @@ fn envFlag(env_map: *std.process.Environ.Map, name: []const u8) bool {
fn nowNs() i128 {
if (is_emscripten) return @intCast(c.SDL_GetTicksNS());
- var ts: linux.timespec = undefined;
- _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
- return @as(i128, ts.sec) * 1_000_000_000 + @as(i128, ts.nsec);
+ switch (os_tag) {
+ .linux => {
+ var ts: linux.timespec = undefined;
+ _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
+ return @as(i128, ts.sec) * 1_000_000_000 + @as(i128, ts.nsec);
+ },
+ else => {
+ var ts: posix.timespec = undefined;
+ switch (posix.errno(posix.system.clock_gettime(posix.CLOCK.MONOTONIC, &ts))) {
+ .SUCCESS => return @as(i128, ts.sec) * 1_000_000_000 + @as(i128, ts.nsec),
+ else => return 0,
+ }
+ },
+ }
+}
+
+fn openCreateTrunc(path: [*:0]const u8) ?posix.fd_t {
+ switch (os_tag) {
+ .linux => {
+ const fd_rc = linux.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
+ const fd_s: isize = @bitCast(fd_rc);
+ return if (fd_s < 0) null else @intCast(fd_s);
+ },
+ else => return posix.openatZ(posix.AT.FDCWD, path, .{
+ .ACCMODE = .WRONLY,
+ .CREAT = true,
+ .TRUNC = true,
+ }, 0o644) catch null,
+ }
+}
+
+fn closeFdBestEffort(fd: posix.fd_t) void {
+ switch (os_tag) {
+ .linux => _ = linux.close(fd),
+ else => switch (posix.errno(posix.system.close(fd))) {
+ .SUCCESS, .INTR => {},
+ else => {},
+ },
+ }
+}
+
+fn writeAllFd(fd: posix.fd_t, bytes: []const u8) !void {
+ var off: usize = 0;
+ while (off < bytes.len) {
+ switch (os_tag) {
+ .linux => {
+ const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
+ const n: isize = @bitCast(rc);
+ if (n < 0) {
+ if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
+ return error.WriteFailed;
+ }
+ if (n == 0) return error.WriteFailed;
+ off += @intCast(n);
+ },
+ else => {
+ const rc = posix.system.write(fd, bytes[off..].ptr, bytes.len - off);
+ switch (posix.errno(rc)) {
+ .SUCCESS => {
+ const n: usize = @intCast(rc);
+ if (n == 0) return error.WriteFailed;
+ off += n;
+ },
+ .INTR => continue,
+ else => return error.WriteFailed,
+ }
+ },
+ }
+ }
+}
+
+fn writeAllFdIgnore(fd: posix.fd_t, bytes: []const u8) void {
+ writeAllFd(fd, bytes) catch {};
+}
+
+fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) !void {
+ switch (os_tag) {
+ .linux => {
+ const rr = linux.rename(old_path, new_path);
+ const rs: isize = @bitCast(rr);
+ if (rs < 0) return error.RenameFailed;
+ },
+ else => switch (posix.errno(posix.system.rename(old_path, new_path))) {
+ .SUCCESS => {},
+ else => return error.RenameFailed,
+ },
+ }
}
fn nsToUs(delta: i128) u64 {
@@ -302,7 +387,7 @@ pub fn init(
}
const trace_enabled = envFlag(env_map, "PARDES_SDL_TRACE");
const trace_every = envU32(env_map, "PARDES_SDL_TRACE_EVERY") orelse 1;
- var trace_fd: c_int = -1;
+ var trace_fd: posix.fd_t = -1;
if (!is_emscripten and trace_enabled) {
if (env_map.get("PARDES_SDL_TRACE_FILE")) |path| {
trace_fd = openTraceFile(path);
@@ -463,7 +548,7 @@ fn initGl(
test_mode: bool,
capture_dir: ?[]const u8,
trace_enabled: bool,
- trace_fd: c_int,
+ trace_fd: posix.fd_t,
trace_every: u32,
) !SdlBackend {
_ = c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_PROFILE_MASK, c.SDL_GL_CONTEXT_PROFILE_ES);
@@ -606,7 +691,7 @@ pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writ
}
if (self.capture_tex) |t| c.SDL_ReleaseGPUTexture(self.device, t);
if (self.capture_xfer) |x| c.SDL_ReleaseGPUTransferBuffer(self.device, x);
- if (self.trace_fd >= 0 and self.trace_fd != 2) _ = linux.close(self.trace_fd);
+ if (self.trace_fd >= 0 and self.trace_fd != 2) closeFdBestEffort(self.trace_fd);
c.SDL_ReleaseGPUBuffer(self.device, self.vbuf);
c.SDL_ReleaseGPUTransferBuffer(self.device, self.vxfer);
self.alloc.free(self.cell_keys);
@@ -994,17 +1079,7 @@ fn dumpGrid(self: *SdlBackend) !void {
}
buf.appendSlice(self.alloc, "---ENDFRAME---\n") catch return;
- // write to stdout (fd 1)
- var off: usize = 0;
- while (off < buf.items.len) {
- const rc = linux.write(1, buf.items[off..].ptr, buf.items.len - off);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return;
- }
- off += @intCast(n);
- }
+ try writeAllFd(1, buf.items);
}
fn captureSwapchain(self: *SdlBackend, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32) !void {
@@ -1094,11 +1169,8 @@ fn writeCapturePpm(self: *SdlBackend, pixels: []const u8, width: u32, height: u3
const tmp_path = std.fmt.bufPrintZ(&tmp_buf, "{s}/latest.ppm.tmp", .{dir}) catch return error.CapturePathTooLong;
const final_path = std.fmt.bufPrintZ(&final_buf, "{s}/latest.ppm", .{dir}) catch return error.CapturePathTooLong;
- const fd_rc = linux.open(tmp_path.ptr, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const fd_s: isize = @bitCast(fd_rc);
- if (fd_s < 0) return error.CaptureWriteFailed;
- const fd: c_int = @intCast(fd_s);
- defer _ = linux.close(fd);
+ const fd = openCreateTrunc(tmp_path.ptr) orelse return error.CaptureWriteFailed;
+ defer closeFdBestEffort(fd);
var header: [64]u8 = undefined;
const hdr = std.fmt.bufPrint(&header, "P6\n{d} {d}\n255\n", .{ width, height }) catch return error.CaptureWriteFailed;
@@ -1134,47 +1206,14 @@ fn writeCapturePpm(self: *SdlBackend, pixels: []const u8, width: u32, height: u3
try writeAllFd(fd, row_rgb);
}
- const rr = linux.rename(tmp_path.ptr, final_path.ptr);
- const rs: isize = @bitCast(rr);
- if (rs < 0) return error.CaptureWriteFailed;
+ renameZ(tmp_path.ptr, final_path.ptr) catch return error.CaptureWriteFailed;
self.capture_frame += 1;
}
-fn writeAllFd(fd: c_int, bytes: []const u8) !void {
- var off: usize = 0;
- while (off < bytes.len) {
- const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return error.CaptureWriteFailed;
- }
- if (n == 0) return error.CaptureWriteFailed;
- off += @intCast(n);
- }
-}
-
-fn openTraceFile(path: []const u8) c_int {
+fn openTraceFile(path: []const u8) posix.fd_t {
var buf: [4096]u8 = undefined;
const z = std.fmt.bufPrintZ(&buf, "{s}", .{path}) catch return 2;
- const fd_rc = linux.open(z.ptr, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const fd_s: isize = @bitCast(fd_rc);
- if (fd_s < 0) return 2;
- return @intCast(fd_s);
-}
-
-fn writeAllFdIgnore(fd: c_int, bytes: []const u8) void {
- var off: usize = 0;
- while (off < bytes.len) {
- const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return;
- }
- if (n == 0) return;
- off += @intCast(n);
- }
+ return openCreateTrunc(z.ptr) orelse 2;
}
fn tracef(self: *SdlBackend, comptime fmt: []const u8, args: anytype) void {
@@ -1759,7 +1798,7 @@ fn SdlLoop(comptime EventType: type) type {
if (!is_emscripten and self.vx.test_mode) {
// read winsize from stdin (fd 0 = the pty slave)
var ws: posix.winsize = std.mem.zeroes(posix.winsize);
- const iorc = posix.system.ioctl(0, posix.T.IOCGWINSZ, @intFromPtr(&ws));
+ const iorc = posix.system.ioctl(0, tiocgwinszReq(), @intFromPtr(&ws));
if (iorc < 0) return; // fd 0 is not a pty
const cols: u16 = ws.col;
const rows: u16 = ws.row;
@@ -1968,14 +2007,9 @@ fn SdlLoop(comptime EventType: type) type {
_ = posix.poll(&fds, 50) catch continue;
if ((fds[0].revents & posix.POLL.IN) == 0) continue;
- const rc = linux.read(0, buf[read_start..].ptr, buf.len - read_start);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- break;
- }
+ const n = readStdin(buf[read_start..]) catch break;
if (n == 0) break;
- const len: usize = read_start + @as(usize, @intCast(n));
+ const len: usize = read_start + n;
var seq_start: usize = 0;
while (seq_start < len) {
const result = parser.parse(buf[seq_start..len], null) catch break;
@@ -2023,11 +2057,40 @@ fn setStdinRaw() !void {
term.lflag.ICANON = false;
term.lflag.IEXTEN = false;
term.lflag.ISIG = false;
- term.cc[@intFromEnum(linux.V.MIN)] = 1;
- term.cc[@intFromEnum(linux.V.TIME)] = 0;
+ term.cc[@intFromEnum(posix.V.MIN)] = 1;
+ term.cc[@intFromEnum(posix.V.TIME)] = 0;
try posix.tcsetattr(0, .NOW, term);
}
+fn ioctlReq(bits: u32) c_int {
+ return @bitCast(bits);
+}
+
+fn tiocgwinszReq() c_int {
+ return switch (os_tag) {
+ .linux => @intCast(posix.T.IOCGWINSZ),
+ .macos, .freebsd, .openbsd, .netbsd, .dragonfly => ioctlReq(0x40087468),
+ else => @intCast(posix.T.IOCGWINSZ),
+ };
+}
+
+fn readStdin(buf: []u8) !usize {
+ switch (os_tag) {
+ .linux => {
+ while (true) {
+ const rc = linux.read(0, buf.ptr, buf.len);
+ const n: isize = @bitCast(rc);
+ if (n < 0) {
+ if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
+ return error.ReadFailed;
+ }
+ return @intCast(n);
+ }
+ },
+ else => return posix.read(0, buf),
+ }
+}
+
// ---- test-mode SIGWINCH handler ----
// Signal handlers can't take closures, so we just set a flag; the stdin reader
// (SdlLoop.stdinRunTyped) checks it and reads + posts the new winsize.