summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig21
-rw-r--r--src/config.zig8
-rw-r--r--src/gui/gui.zig25
-rw-r--r--src/pardes.zig25
-rw-r--r--src/shell_bin.zig221
-rw-r--r--src/tty/tty.zig22
6 files changed, 292 insertions, 30 deletions
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 <name>` 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 <name>` 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);