summaryrefslogtreecommitdiff
path: root/src/shell_bin.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/shell_bin.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/shell_bin.zig')
-rw-r--r--src/shell_bin.zig567
1 files changed, 0 insertions, 567 deletions
diff --git a/src/shell_bin.zig b/src/shell_bin.zig
deleted file mode 100644
index 1090bb2b..00000000
--- a/src/shell_bin.zig
+++ /dev/null
@@ -1,567 +0,0 @@
-//! 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.
-//!
-//! Each host owns one `PromptRcs` for its lifetime. Its files are private
-//! `mkstemp` names, completely written and closed before resolve can expose
-//! them to a child. Concurrent launches therefore share neither a pathname nor
-//! an inode, and a shell can never source another user's predictable /tmp file.
-//!
-//! 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;
-
-extern "c" fn mkstemp(template: [*:0]u8) c_int;
-extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
-
-// ------------------------------------------------- the GUI launch's PATH
-
-/// Bounded storage for the composed PATH. /etc/paths and /etc/paths.d hold ten
-/// directories on a stock machine and a handful more with third-party
-/// packages; 4 KiB is not a limit anyone will meet, and a fixed buffer keeps
-/// this callable from a host that has not built an allocator yet.
-const path_capacity = 4096;
-const max_path_files = 64;
-
-/// macOS: give the PROCESS the PATH a login session would have, but only when
-/// it plainly has not got one.
-///
-/// A GUI launch — Finder, the Dock, `open(1)` — inherits launchd's
-/// environment, and launchd's PATH is `/usr/bin:/bin:/usr/sbin:/sbin`. Nothing
-/// else: no /opt/homebrew/bin, no /usr/local/bin. A launch from a terminal
-/// inherits the shell's PATH and is fine. That difference is the whole bug,
-/// and it is why it reads as intermittent — the same build finds `yazi` when
-/// you start it from a terminal and cannot find it when you start it from the
-/// Dock.
-///
-/// macOS's own answer is /usr/libexec/path_helper, which reads /etc/paths and
-/// /etc/paths.d. LOGIN shells run it and non-login shells do not, and pardes
-/// spawns non-login shells deliberately (see `resolve`) — so a pane cannot fix
-/// this for itself. Nor should it: one environ is inherited by every pty shell
-/// pardes forks, every `/bin/sh -c` filter, and every language server the LSP
-/// client spawns, and `binOf` searching a launchd PATH is a rust-analyzer that
-/// is never found. Fixing the process fixes all of them at once.
-///
-/// ONLY when every entry already in PATH is a system directory. That is the
-/// test for "nobody configured this". path_helper appends pre-existing entries
-/// AFTER the system set, so running it over a real session's PATH would demote
-/// a version manager's shims behind /usr/bin and quietly change which `node`
-/// runs. A configured PATH is left exactly as it is; the launchd case is
-/// unambiguous and is the only one touched.
-pub fn adoptSystemPath() void {
- if (comptime builtin.os.tag != .macos) return;
- var buf: [path_capacity]u8 = undefined;
- var len: usize = 0;
- collectSystemPath(&buf, &len);
- if (len == 0) return;
- const system = buf[0..len];
-
- const current: []const u8 = if (libc.getenv("PATH")) |p| std.mem.span(p) else "";
- if (!allEntriesWithin(current, system)) return;
- if (std.mem.eql(u8, current, system)) return;
-
- var out: [path_capacity:0]u8 = undefined;
- if (len >= out.len) return;
- @memcpy(out[0..len], system);
- out[len] = 0;
- _ = setenv("PATH", out[0..len :0].ptr, 1);
-}
-
-/// Everything a native shell must do TO THE PROCESS before it forks its first
-/// pane, in the order it has to happen, handing back the prompt files those
-/// forks will borrow.
-///
-/// Four hosts performed this ritual by hand and the copies had already
-/// diverged. detached/server.zig forks bash through `resolve` exactly like its
-/// siblings and never set BASH_SILENCE_DEPRECATION_WARNING, so every pane in a
-/// detached session on macOS opened with Apple's zsh-migration banner printed
-/// across the top of it — and nobody noticed, because the three hosts anyone
-/// looks at daily all had the line. That is the failure mode of a four-line
-/// ritual written four times.
-///
-/// The ORDER is the content here. `adoptSystemPath` has to precede the fork
-/// because the child inherits the environ; the setenv has to precede bash
-/// because bash reads it at startup and the rc file is already too late; and
-/// the rc files have to be complete on disk before any child can be handed a
-/// path to one.
-pub fn prepareForFork() PromptRcs {
- adoptSystemPath();
- if (comptime builtin.os.tag.isDarwin())
- _ = setenv("BASH_SILENCE_DEPRECATION_WARNING", "1", 1);
- return PromptRcs.init();
-}
-
-/// /etc/paths, then every file in /etc/paths.d in NAME ORDER, which is the
-/// order path_helper reads them in and therefore the order the directories
-/// take precedence in.
-fn collectSystemPath(buf: []u8, len: *usize) void {
- var file_buf: [path_capacity]u8 = undefined;
- if (readSmall("/etc/paths", &file_buf)) |body| appendLines(buf, len, body);
-
- const io = std.Io.Threaded.global_single_threaded.io();
- var dir = std.Io.Dir.cwd().openDir(io, "/etc/paths.d", .{ .iterate = true }) catch return;
- defer dir.close(io);
-
- // readdir order is undefined and path_helper's is not, so the names are
- // collected and sorted before any of them is read.
- var names: [max_path_files][256]u8 = undefined;
- var name_lens: [max_path_files]usize = undefined;
- var count: usize = 0;
- var it = dir.iterate();
- while (count < names.len) {
- const entry = (it.next(io) catch break) orelse break;
- if (entry.kind == .directory) continue;
- if (entry.name.len == 0 or entry.name.len > names[count].len) continue;
- @memcpy(names[count][0..entry.name.len], entry.name);
- name_lens[count] = entry.name.len;
- count += 1;
- }
- var order: [max_path_files]usize = undefined;
- for (0..count) |i| order[i] = i;
- std.mem.sort(usize, order[0..count], Names{ .names = &names, .lens = &name_lens }, Names.lessThan);
-
- var path_buf: [512]u8 = undefined;
- for (order[0..count]) |i| {
- const name = names[i][0..name_lens[i]];
- const path = std.fmt.bufPrintSentinel(&path_buf, "/etc/paths.d/{s}", .{name}, 0) catch continue;
- if (readSmall(path, &file_buf)) |body| appendLines(buf, len, body);
- }
-}
-
-const Names = struct {
- names: *const [max_path_files][256]u8,
- lens: *const [max_path_files]usize,
-
- fn lessThan(self: Names, a: usize, b: usize) bool {
- return std.mem.order(u8, self.names[a][0..self.lens[a]], self.names[b][0..self.lens[b]]) == .lt;
- }
-};
-
-/// One directory per line, blanks and whitespace ignored — the format both
-/// files use and the only thing path_helper reads out of them.
-fn appendLines(buf: []u8, len: *usize, body: []const u8) void {
- var lines = std.mem.splitScalar(u8, body, '\n');
- while (lines.next()) |raw| appendEntry(buf, len, std.mem.trim(u8, raw, " \t\r"));
-}
-
-/// Append `entry` unless it is already present. Dedup preserves the FIRST
-/// occurrence, which is what makes the order above mean precedence.
-fn appendEntry(buf: []u8, len: *usize, entry: []const u8) void {
- if (entry.len == 0) return;
- if (hasEntry(buf[0..len.*], entry)) return;
- const separator: usize = if (len.* == 0) 0 else 1;
- if (len.* + separator + entry.len > buf.len) return;
- if (separator == 1) {
- buf[len.*] = ':';
- len.* += 1;
- }
- @memcpy(buf[len.*..][0..entry.len], entry);
- len.* += entry.len;
-}
-
-fn hasEntry(list: []const u8, entry: []const u8) bool {
- var it = std.mem.tokenizeScalar(u8, list, ':');
- while (it.next()) |have| if (std.mem.eql(u8, have, entry)) return true;
- return false;
-}
-
-/// Whether `candidate` holds nothing `list` does not. An empty candidate is
-/// within any list: a process with no PATH at all is the launchd case too.
-fn allEntriesWithin(candidate: []const u8, list: []const u8) bool {
- var it = std.mem.tokenizeScalar(u8, candidate, ':');
- while (it.next()) |entry| if (!hasEntry(list, entry)) return false;
- return true;
-}
-
-fn readSmall(path: [:0]const u8, buf: []u8) ?[]const u8 {
- const fd = libc.open(path, .{ .ACCMODE = .RDONLY }, @as(libc.mode_t, 0));
- if (fd < 0) return null;
- defer _ = libc.close(fd);
- var off: usize = 0;
- while (off < buf.len) {
- const n = libc.read(fd, buf[off..].ptr, buf.len - off);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- return null;
- }
- if (n == 0) break;
- off += @intCast(n);
- }
- return buf[0..off];
-}
-
-test "the launchd PATH is replaced and a configured one is left alone" {
- var buf: [256]u8 = undefined;
- var len: usize = 0;
- appendEntry(&buf, &len, "/usr/bin");
- appendEntry(&buf, &len, "/bin");
- appendEntry(&buf, &len, "/usr/bin"); // already there: dedup keeps the first
- appendEntry(&buf, &len, "");
- try std.testing.expectEqualStrings("/usr/bin:/bin", buf[0..len]);
-
- // Exactly the launchd default, in any order: nothing here is a choice.
- try std.testing.expect(allEntriesWithin("/usr/bin:/bin", "/usr/bin:/bin:/sbin"));
- try std.testing.expect(allEntriesWithin("", "/usr/bin"));
- // One entry nobody could have inherited by accident, and the whole PATH is
- // off limits — reordering it behind /usr/bin is how a version manager stops
- // deciding which `node` runs.
- try std.testing.expect(!allEntriesWithin("/Users/x/.cargo/bin:/usr/bin", "/usr/bin:/bin"));
- try std.testing.expect(!allEntriesWithin("/opt/homebrew/bin", "/usr/bin:/bin"));
-}
-
-test "the composed system path is the real one, in path_helper's order" {
- if (comptime builtin.os.tag != .macos) return;
- var buf: [path_capacity]u8 = undefined;
- var len: usize = 0;
- collectSystemPath(&buf, &len);
- const composed = buf[0..len];
- // /etc/paths exists on every mac and leads with these.
- try std.testing.expect(hasEntry(composed, "/usr/bin"));
- try std.testing.expect(hasEntry(composed, "/bin"));
- // ...and its entries come before anything /etc/paths.d contributes, which
- // is the precedence the order encodes.
- try std.testing.expect(std.mem.startsWith(u8, composed, "/usr/local/bin:"));
- // No duplicates: /etc/paths.d files routinely repeat a system directory.
- var seen = std.mem.tokenizeScalar(u8, composed, ':');
- var index: usize = 0;
- while (seen.next()) |entry| : (index += 1) {
- var rest = std.mem.tokenizeScalar(u8, composed, ':');
- var matches: usize = 0;
- while (rest.next()) |other| if (std.mem.eql(u8, other, entry)) {
- matches += 1;
- };
- try std.testing.expectEqual(@as(usize, 1), matches);
- }
-}
-
-/// 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;
-}
-
-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.
-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
- \\
-;
-
-const rc_path_capacity = 64;
-
-/// The two complete, private prompt files a native host lends to every shell
-/// it spawns. No allocation and no global name: moving this value is safe
-/// because it stores lengths, never pointers into its own buffers.
-pub const PromptRcs = struct {
- bash_path: [rc_path_capacity:0]u8 = @splat(0),
- bash_len: u8 = 0,
- fish_path: [rc_path_capacity:0]u8 = @splat(0),
- fish_len: u8 = 0,
- fish_command: [rc_path_capacity + "source ".len:0]u8 = @splat(0),
- fish_command_len: u8 = 0,
-
- pub fn init() PromptRcs {
- var rcs: PromptRcs = .{};
- rcs.bash_len = stage(&rcs.bash_path, "/tmp/pardes-osc133-bash-XXXXXX", bash_rc);
- rcs.fish_len = stage(&rcs.fish_path, "/tmp/pardes-osc133-fish-XXXXXX", fish_rc);
- if (rcs.fishPath()) |path| {
- const command = std.fmt.bufPrintSentinel(&rcs.fish_command, "source {s}", .{path}, 0) catch {
- _ = libc.unlink(path.ptr);
- rcs.fish_len = 0;
- return rcs;
- };
- rcs.fish_command_len = @intCast(command.len);
- }
- return rcs;
- }
-
- pub fn deinit(rcs: *PromptRcs) void {
- if (rcs.bashPath()) |path| _ = libc.unlink(path.ptr);
- if (rcs.fishPath()) |path| _ = libc.unlink(path.ptr);
- rcs.bash_len = 0;
- rcs.fish_len = 0;
- rcs.fish_command_len = 0;
- }
-
- fn bashPath(rcs: *const PromptRcs) ?[:0]const u8 {
- if (rcs.bash_len == 0) return null;
- return rcs.bash_path[0..rcs.bash_len :0];
- }
-
- fn fishPath(rcs: *const PromptRcs) ?[:0]const u8 {
- if (rcs.fish_len == 0) return null;
- return rcs.fish_path[0..rcs.fish_len :0];
- }
-
- fn fishCommand(rcs: *const PromptRcs) ?[:0]const u8 {
- if (rcs.fish_command_len == 0) return null;
- return rcs.fish_command[0..rcs.fish_command_len :0];
- }
-};
-
-/// Create one private 0600 file and reveal its length only after the complete
-/// write and close. Failure leaves no pathname for resolve to hand to a shell.
-fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u8 {
- const path = std.fmt.bufPrintSentinel(path_buf, "{s}", .{template}, 0) catch return 0;
- const fd = mkstemp(path.ptr);
- if (fd < 0) return 0;
- var off: usize = 0;
- while (off < contents.len) {
- const n = libc.write(fd, contents[off..].ptr, contents.len - off);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- _ = libc.close(fd);
- _ = libc.unlink(path.ptr);
- return 0;
- }
- if (n == 0) {
- _ = libc.close(fd);
- _ = libc.unlink(path.ptr);
- return 0;
- }
- off += @intCast(n);
- }
- if (libc.close(fd) != 0) {
- _ = libc.unlink(path.ptr);
- return 0;
- }
- return @intCast(path.len);
-}
-
-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. `prompt_rcs` is host-lifetime
-/// storage and must likewise remain alive through that exec.
-pub fn resolve(bin: []const u8, buf: *[std.fs.max_path_bytes]u8, prompt_rcs: *const PromptRcs) 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 => if (prompt_rcs.bashPath()) |rc| .{ "--rcfile", rc.ptr } else .{ null, null },
- // -C runs AFTER config.fish, which is the whole point (see fish_rc)
- .fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null },
- .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;
- var prompt_rcs = PromptRcs.init();
- defer prompt_rcs.deinit();
-
- // /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, &prompt_rcs);
- 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, &prompt_rcs);
- 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(prompt_rcs.bashPath().?, 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, &prompt_rcs);
- 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, &prompt_rcs);
- try std.testing.expect(libc.access(gone.path, X_OK) == 0);
-}
-
-test "prompt rc owners have private complete files and clean them up" {
- if (builtin.os.tag == .windows) return;
- var a = PromptRcs.init();
- defer a.deinit();
- var b = PromptRcs.init();
- defer b.deinit();
- const a_bash = a.bashPath() orelse return error.TempCreateFailed;
- const b_bash = b.bashPath() orelse return error.TempCreateFailed;
- const a_fish = a.fishPath() orelse return error.TempCreateFailed;
- try std.testing.expect(!std.mem.eql(u8, a_bash, b_bash));
- const fish_command = a.fishCommand() orelse return error.MissingFishCommand;
- try std.testing.expectEqualStrings("source ", fish_command[0.."source ".len]);
- try std.testing.expectEqualStrings(a_fish, fish_command["source ".len..]);
-
- var buf: [bash_rc.len]u8 = undefined;
- const fd = libc.open(a_bash.ptr, .{ .ACCMODE = .RDONLY });
- if (fd < 0) return error.OpenFailed;
- defer _ = libc.close(fd);
- var len: usize = 0;
- while (len < buf.len) {
- const n = libc.read(fd, buf[len..].ptr, buf.len - len);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- return error.ReadFailed;
- }
- if (n == 0) break;
- len += @intCast(n);
- }
- try std.testing.expectEqualStrings(bash_rc, buf[0..len]);
-
- var removed: [rc_path_capacity:0]u8 = @splat(0);
- @memcpy(removed[0..a_bash.len], a_bash);
- removed[a_bash.len] = 0;
- a.deinit();
- try std.testing.expect(libc.access(&removed, 0) < 0);
-}