From 8aafc3fa24c7475a07259eb06cd5d217f510df98 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 5 Aug 2026 19:32:30 -0300 Subject: add a Shell builtin, default fish, with per-family prompt integration --- src/builtins.zig | 21 ++++++ src/config.zig | 8 ++ src/gui/gui.zig | 25 +++--- src/pardes.zig | 25 +++--- src/shell_bin.zig | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tty/tty.zig | 22 +++--- 6 files changed, 292 insertions(+), 30 deletions(-) create mode 100644 src/shell_bin.zig (limited to 'src') diff --git a/src/builtins.zig b/src/builtins.zig index aaeab5fa..9fa459f2 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -226,6 +226,27 @@ pub const Theme = struct { } }; +/// The binary a NEW terminal execs: `Shell fish`, `Shell zsh`, +/// `Shell /opt/homebrew/bin/bash`. A bare name is looked up on the shells' +/// side against the handful of directories a shell actually lives in — not +/// $PATH, because the lookup has to finish BEFORE the fork (a PATH search +/// allocates, and nothing between fork and exec may). +/// +/// Takes a NAME, so it has no leader path and none in the topbar, for the same +/// reason Theme has neither. Panes already open keep the shell they are +/// running; this is what the next one starts. +pub const Shell = struct { + pub fn run(c: Ctx) void { + const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); + // A name too long for the buffer is not silently truncated into a + // DIFFERENT binary — `/very/long/path/to/fish` clipped is a path that + // either does not exist or, worse, does. + if (want.len == 0 or want.len > c.p.shell_buf.len) return; + @memcpy(c.p.shell_buf[0..want.len], want); + c.p.shell_len = @intCast(want.len); + } +}; + /// ...and the list of what Theme takes, as a buffer you walk. Its rows are /// `Theme ` COMMANDS rather than locations, so n/N execute them instead /// of looking them (output_pane.Traits.executes) and stepping the list wears diff --git a/src/config.zig b/src/config.zig index a8e3639f..9451a2a7 100644 --- a/src/config.zig +++ b/src/config.zig @@ -128,6 +128,8 @@ pub const leader_path = paths: { // reason Look and Exec have none) .ThemeSel = "tt", .Theme = null, + // ...and `Shell` takes the name of a binary, so it has none either + .Shell = null, // the image toggles join the same `t` group; Palette takes `l` because // `p` is Petscii's and `c` is Colors'. .Petscii = "tp", @@ -430,6 +432,12 @@ test "wheel drift guard" { // ---- layout numbers that are taste ---- +/// What a terminal pane runs until someone says otherwise (the Shell builtin, +/// or a `Shell ` line in the config file). A bare NAME, resolved against +/// the usual bin directories at spawn time — so a machine without it falls +/// back rather than opening a pane that dies at exec. +pub const default_shell = "fish"; + /// the file pane's line-number gutter, in columns pub const PREFIX_W: u16 = 5; /// soft wrap (the Wrap builtin): the glyph a wrapped row ends with, in the one diff --git a/src/gui/gui.zig b/src/gui/gui.zig index a4883fa6..e1d5c86d 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -28,6 +28,7 @@ const selection_pipe = @import("../selection_pipe.zig"); const is_emscripten = builtin.os.tag == .emscripten; const temp_file = if (is_emscripten) struct {} else @import("../temp_file.zig"); +const shell_bin = if (is_emscripten) struct {} else @import("../shell_bin.zig"); pub const c = @cImport({ @cDefine("SDL_DISABLE_OLD_NAMES", "1"); @@ -44,9 +45,6 @@ extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int 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)); @@ -1272,7 +1270,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { core.native_images = true; // shells emit OSC 133 prompt marks via this rc (prompt hiding, click-move) - writeFile(pardes.bash_rc); + writeFile(shell_bin.bash_rc_path, shell_bin.bash_rc); + writeFile(shell_bin.fish_rc_path, shell_bin.fish_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); @@ -1775,7 +1774,8 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { if (opts.load_path == null) core.update(.{ .resize = .{ .cols = grid_cols, .rows = grid_rows } }); - writeFile(pardes.bash_rc); + writeFile(shell_bin.bash_rc_path, shell_bin.bash_rc); + writeFile(shell_bin.fish_rc_path, shell_bin.fish_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); @@ -2599,7 +2599,7 @@ fn drainEffects( cwd_buf[cwd.len] = 0; cwd_z = @ptrCast(&cwd_buf); } - const pt = forkShell(cwd_z, core.screen_h, core.screen_w); + const pt = forkShell(core.shellBin(), cwd_z, core.screen_h, core.screen_w); ptys[sp.pane] = pt; // report the pane's starting directory back to the core (tags) var lbuf: [1024]u8 = undefined; @@ -2678,14 +2678,17 @@ fn drainEffects( }; } -fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) Pty { +fn forkShell(bin: []const u8, cwd: ?[*:0]const u8, rows: u16, cols: u16) Pty { var master: c_int = undefined; + // resolved BEFORE the fork, into this frame, which the child inherits: + // nothing between fork and exec may allocate, and a PATH search would + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const spawn = shell_bin.resolve(bin, &path_buf); const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; const pid = forkpty(&master, null, null, &ws); if (pid == 0) { if (cwd) |cd| _ = chdir(cd); - const argv: [4:null]?[*:0]const u8 = .{ bash_path, "--rcfile", "/tmp/pardes-osc133.bash", null }; - _ = execv(bash_path, &argv); + _ = execv(spawn.path, &spawn.argv); _exit(127); } return .{ .fd = master, .pid = pid }; @@ -4281,8 +4284,8 @@ fn envU16(env: *std.process.Environ.Map, name: []const u8) ?u16 { return std.fmt.parseInt(u16, raw, 10) catch null; } -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)); +fn writeFile(path: [*:0]const u8, data: []const u8) void { + const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644)); if (fd < 0) return; defer _ = libc.close(fd); writeFd(fd, data); diff --git a/src/pardes.zig b/src/pardes.zig index 61950562..08536544 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2232,14 +2232,6 @@ fn pathUnder(a: []const u8, b: []const u8) bool { } const tutor_text = @embedFile("tutor.txt"); -const rc_path = "/tmp/pardes-osc133.bash"; -pub const bash_rc = - \\[ -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}" - \\trap 'printf "\e]133;C\a"' DEBUG - \\ -; // ---- theme ---- @@ -3960,6 +3952,14 @@ pub const Pardes = struct { /// its first theme. No frame is rendered in that interval. animate_theme_changes: bool = false, colors_on: bool = true, + /// The binary a NEW terminal execs (the Shell builtin). Inline and fixed + /// like every other short string the core keeps, and `0` means "the + /// default" rather than a copy of it — so there is no constructor to + /// remember and config.default_shell stays the single place the default is + /// written. Existing panes are not touched: a shell you are already in is + /// a process, not a setting. + shell_buf: [255]u8 = undefined, + shell_len: u8 = 0, /// soft line breaks in file panes (the Wrap builtin). ON by default: a line /// running off the right edge is text you cannot read without scrolling /// sideways for it, and the reason to reach for the toggle is the rare @@ -4322,6 +4322,13 @@ pub const Pardes = struct { return true; } + /// What a shell should exec for the next terminal — a bare name to be + /// looked up, or an absolute path to be used as it stands. The resolving + /// is the shells' half: the core has no filesystem to ask. + pub fn shellBin(p: *const Pardes) []const u8 { + return if (p.shell_len == 0) config.default_shell else p.shell_buf[0..p.shell_len]; + } + /// Post the transient message on `id`'s last row. Called by a SHELL once /// the IO it narrates has actually happened, exactly as openNewFile above /// completes a `new_file` effect: the core neither writes files nor owns a @@ -11033,7 +11040,7 @@ pub const Pardes = struct { /// something that has somewhere to put it. fn takesArg(b: Builtin) bool { switch (b) { - .Restore, .Find, .Grep, .Rename, .WsSymbols, .Theme, .Look, .Exec => return true, + .Restore, .Find, .Grep, .Rename, .WsSymbols, .Theme, .Shell, .Look, .Exec => return true, // Font is a BRANCH and not another name in that prong for the // reason output_pane.traits has the same shape: it is not in the // enum at all off the gui, and a comptime-false `if` is the one diff --git a/src/shell_bin.zig b/src/shell_bin.zig new file mode 100644 index 00000000..ecd80937 --- /dev/null +++ b/src/shell_bin.zig @@ -0,0 +1,221 @@ +//! Turning the name of a shell into something a freshly forked child can exec, +//! and into the argv that hands that shell its prompt marks. +//! +//! Native-shell side, like temp_file.zig and message.zig, and for the same +//! reason: it touches the filesystem, and the core does not. The core carries +//! only the NAME (Pardes.shellBin, what the Shell builtin was given); which +//! family that is, what to write for it, where to write it and where the +//! binary actually lives all live here, and both frontends call it rather than +//! keeping a copy each. Nothing here imports the core, which is also what lets +//! it be its own std-only test module. +//! +//! ALL OF THIS RUNS IN THE PARENT. Between fork and exec a process may not +//! allocate, and a $PATH search does — which is the same reason the exec is +//! `execv` on an absolute path and never `execvp`. So the lookup is a handful +//! of `access` calls over the directories a shell actually lives in, done +//! before the fork, into a caller buffer that the child then inherits through +//! its copy of the stack. +const std = @import("std"); +const builtin = @import("builtin"); + +const libc = std.c; +const X_OK: c_int = 1; + +/// Prompt integration, per shell FAMILY rather than per binary: pardes hides +/// prompt rows, moves the cursor by clicking one, and tells a command's output +/// from the line that asked for it, and all three read the OSC 133 marks a +/// shell has to be talked into emitting. Every family needs different words +/// for the same four marks and a different way to be handed them, so the +/// binary a pane is about to exec picks one of these and there is nothing to +/// configure. +pub const ShellRc = enum { bash, fish, none }; + +/// Which family a shell binary belongs to, by the BASENAME's prefix — the +/// whole heuristic. A prefix and not an exact match because a real system +/// spells them `bash`, `/usr/bin/bash`, `bash-5.2`, `fish-3.7`, and pinning +/// exact names would mean a list to maintain against other people's packaging. +/// It costs a false positive on a program called `fishing`, which is a shell +/// nobody has. +/// +/// `none` is not a failure: it execs the binary plain and the pane works, it +/// just has no prompt marks, so prompts are not hidden and a click on one does +/// not move the shell's cursor. Everything else about the pane is unaffected. +/// +/// ponytail: two families and a fallback. zsh is the obvious third and is NOT +/// here because it is shaped differently — it has no `--rcfile`, so it needs a +/// whole ZDOTDIR directory staged with a .zshrc that re-sources the user's, +/// plus an env var set before exec. Add it when someone runs zsh in pardes and +/// misses prompt hiding; the rc text itself is four lines (precmd/preexec). +pub fn shellRc(bin: []const u8) ShellRc { + const slash = std.mem.lastIndexOfScalar(u8, bin, '/'); + const base = if (slash) |s| bin[s + 1 ..] else bin; + if (std.mem.startsWith(u8, base, "bash")) return .bash; + if (std.mem.startsWith(u8, base, "fish")) return .fish; + return .none; +} + +pub const bash_rc_path = "/tmp/pardes-osc133.bash"; +pub const fish_rc_path = "/tmp/pardes-osc133.fish"; + +pub const bash_rc = + \\[ -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}" + \\trap 'printf "\e]133;C\a"' DEBUG + \\ +; + +/// fish is handed this with `-C`, which runs AFTER config.fish — and it has to, +/// because the first thing it does is copy the user's own `fish_prompt` to call +/// it from the middle of ours. Loaded any earlier it would copy the default and +/// silently replace whatever the user actually configured. +/// +/// The other half is why there is no `source ~/.config/fish/config.fish` line +/// the way the bash rc sources .bashrc: bash is being started with `--rcfile`, +/// which REPLACES its startup file, so the rc has to put it back. `-C` adds to +/// fish's startup instead of standing in for it. +/// +/// C and D come off fish's own `fish_preexec`/`fish_postexec` events rather +/// than being spliced into the prompt, which is what bash's DEBUG trap is +/// working around. +pub const fish_rc = + \\functions -c fish_prompt __pardes_user_prompt + \\function fish_prompt + \\ printf '\e]133;A;cl=line\a' + \\ __pardes_user_prompt + \\ printf '\e]133;B\a' + \\end + \\function __pardes_preexec --on-event fish_preexec + \\ printf '\e]133;C\a' + \\end + \\function __pardes_postexec --on-event fish_postexec + \\ printf '\e]133;D\a' + \\end + \\ +; + +test "shell family is the basename's prefix, and anything else runs unadorned" { + try std.testing.expectEqual(ShellRc.fish, shellRc("fish")); + try std.testing.expectEqual(ShellRc.fish, shellRc("/usr/bin/fish")); + try std.testing.expectEqual(ShellRc.fish, shellRc("/opt/homebrew/bin/fish")); + try std.testing.expectEqual(ShellRc.bash, shellRc("bash")); + try std.testing.expectEqual(ShellRc.bash, shellRc("/bin/bash")); + // packaged with a version on the end, which is why this is a prefix + try std.testing.expectEqual(ShellRc.bash, shellRc("/usr/bin/bash-5.2")); + try std.testing.expectEqual(ShellRc.fish, shellRc("/usr/local/bin/fish-3.7")); + // a directory that merely CONTAINS the word is not the shell's name + try std.testing.expectEqual(ShellRc.none, shellRc("/opt/fish/bin/nu")); + // no marks, still a shell + try std.testing.expectEqual(ShellRc.none, shellRc("/usr/bin/zsh")); + try std.testing.expectEqual(ShellRc.none, shellRc("/bin/sh")); + try std.testing.expectEqual(ShellRc.none, shellRc("nu")); + try std.testing.expectEqual(ShellRc.none, shellRc("")); +} + +/// The directories a shell binary is actually installed in. Not $PATH: see the +/// header. `/opt/homebrew` and `/opt/local` are where a mac keeps the shells +/// that did not ship with it, which is every shell anyone chooses on purpose. +const bin_dirs = [_][]const u8{ + "/usr/bin/", + "/bin/", + "/usr/local/bin/", + "/opt/homebrew/bin/", + "/opt/local/bin/", + "/usr/sbin/", +}; + +/// Last resorts, in order, when the configured shell is not installed: the +/// shell pardes used to hardcode, then the one POSIX says exists. A pane that +/// opens with the wrong shell beats a pane whose child dies at exec and shows +/// nothing but an immediate EOF. +const fallbacks = [_][]const u8{ + if (builtin.os.tag == .linux) "/usr/bin/bash" else "/bin/bash", + "/bin/sh", +}; + +pub const Spawn = struct { + path: [*:0]const u8, + /// argv for execv. Shorter forms stop at their first null, which is what + /// execv reads anyway, so one width covers all three families. + argv: [4:null]?[*:0]const u8, +}; + +/// `bin` is whatever the Shell builtin was given — a bare name to look up, or +/// a path (anything with a `/`) to take at its word. `buf` holds the resolved +/// path for as long as the returned Spawn is used, which for a caller that is +/// about to fork means: until the child execs. +pub fn resolve(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) Spawn { + const path = find(bin, buf) orelse fallback(buf); + // the family comes off the path that will ACTUALLY be executed, not the + // name that was asked for — `Shell sh` on a system where that is a symlink + // to bash still has no `--rcfile` promise attached to it, and a resolved + // /usr/bin/fish reads as fish whether it was reached by name or by path + const marks: [2]?[*:0]const u8 = switch (shellRc(std.mem.span(path))) { + .bash => .{ "--rcfile", bash_rc_path }, + // -C runs AFTER config.fish, which is the whole point (see fish_rc) + .fish => .{ "-C", "source " ++ fish_rc_path }, + .none => .{ null, null }, + }; + return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } }; +} + +fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 { + if (bin.len == 0 or bin.len + 1 > buf.len) return null; + if (std.mem.indexOfScalar(u8, bin, '/') != null) { + @memcpy(buf[0..bin.len], bin); + buf[bin.len] = 0; + const p: [*:0]const u8 = @ptrCast(buf); + return if (libc.access(p, X_OK) == 0) p else null; + } + for (bin_dirs) |dir| { + if (dir.len + bin.len + 1 > buf.len) continue; + @memcpy(buf[0..dir.len], dir); + @memcpy(buf[dir.len..][0..bin.len], bin); + buf[dir.len + bin.len] = 0; + const p: [*:0]const u8 = @ptrCast(buf); + if (libc.access(p, X_OK) == 0) return p; + } + return null; +} + +fn fallback(buf: *[std.fs.max_path_bytes]u8) [*:0]const u8 { + for (fallbacks) |f| { + @memcpy(buf[0..f.len], f); + buf[f.len] = 0; + const p: [*:0]const u8 = @ptrCast(buf); + if (libc.access(p, X_OK) == 0) return p; + } + // nothing executable anywhere we know to look: exec will fail and the pane + // will show an immediate EOF, which is the honest report of that machine. + // buf already holds the last candidate, NUL and all. + return @ptrCast(buf); +} + +test "a path is taken at its word, a name is looked up, and both pick their own marks" { + if (builtin.os.tag == .windows) return; + var buf: [std.fs.max_path_bytes]u8 = undefined; + + // /bin/sh exists on every unix this builds for and is in no family, so it + // pins the resolve-by-path arm AND the unadorned argv + const sh = resolve("/bin/sh", &buf); + try std.testing.expectEqualStrings("/bin/sh", std.mem.span(sh.path)); + try std.testing.expect(sh.argv[1] == null); + + // a name with no slash is searched for; whatever it resolves to, it is a + // bash and so carries --rcfile pointing at the rc the shells write + const bash = resolve("bash", &buf); + try std.testing.expect(shellRc(std.mem.span(bash.path)) == .bash); + try std.testing.expectEqualStrings("--rcfile", std.mem.span(bash.argv[1].?)); + try std.testing.expectEqualStrings(bash_rc_path, std.mem.span(bash.argv[2].?)); + + // nothing is installed under this name, so the fallback answers — and the + // fallback is a real executable, not the name that failed + const missing = resolve("zznosuchshell", &buf); + try std.testing.expect(!std.mem.eql(u8, "zznosuchshell", std.mem.span(missing.path))); + try std.testing.expect(libc.access(missing.path, X_OK) == 0); + + // an absolute path that does not exist falls back too, rather than being + // handed to exec to fail on + const gone = resolve("/zz/no/such/shell", &buf); + try std.testing.expect(libc.access(gone.path, X_OK) == 0); +} diff --git a/src/tty/tty.zig b/src/tty/tty.zig index b5b4ff75..22449719 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -12,6 +12,7 @@ const pardes = @import("../pardes.zig"); const tracy = @import("../tracy.zig"); const look = @import("../look.zig"); const temp_file = @import("../temp_file.zig"); +const shell_bin = @import("../shell_bin.zig"); const message = @import("../message.zig"); const selection_pipe = @import("../selection_pipe.zig"); @@ -21,9 +22,6 @@ 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)); @@ -379,7 +377,8 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { defer core.deinit(); // shells emit OSC 133 prompt marks via this rc (prompt hiding, click-move) - writeFile(pardes.bash_rc); + writeFile(shell_bin.bash_rc_path, shell_bin.bash_rc); + writeFile(shell_bin.fish_rc_path, shell_bin.fish_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); @@ -854,7 +853,7 @@ fn drainEffects( cwd_buf[cwd.len] = 0; cwd_z = @ptrCast(&cwd_buf); } - const child = forkShell(cwd_z, core.screen_h, core.screen_w); + const child = forkShell(core.shellBin(), 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; @@ -1105,8 +1104,12 @@ fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { } } -fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.File, pid: posix.pid_t } { +fn forkShell(bin: []const u8, cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.File, pid: posix.pid_t } { var master: c_int = undefined; + // resolved BEFORE the fork, into this frame, which the child inherits: + // nothing between fork and exec may allocate, and a PATH search would + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const spawn = shell_bin.resolve(bin, &path_buf); const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; const pid = forkpty(&master, null, null, &ws); if (pid == 0) { @@ -1117,8 +1120,7 @@ fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.Fi 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); + _ = execv(spawn.path, &spawn.argv); _exit(127); } return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid }; @@ -1203,8 +1205,8 @@ fn vaxisColor(c: pardes.Color) vaxis.Color { }; } -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)); +fn writeFile(path: [*:0]const u8, data: []const u8) void { + const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644)); if (fd < 0) return; defer _ = libc.close(fd); writeFd(fd, data); -- cgit v1.3