diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-03 23:37:10 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | f3f0e27372d8cf111269dde674d4581cf4cb0c41 (patch) | |
| tree | 4cc53bb5893ece129ec4c3908380c46d0e691378 /main.zig | |
| parent | a354d34329de597b80943c78e2afd84f235ee505 (diff) | |
| download | pardes-f3f0e27372d8cf111269dde674d4581cf4cb0c41.tar.gz pardes-f3f0e27372d8cf111269dde674d4581cf4cb0c41.zip | |
sdl: main + ui_sdl rework, build.zig updates
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 324 |
1 files changed, 228 insertions, 96 deletions
@@ -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, ¤t); + 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, |
