Use the user's login shell, and fix login-shell argv

Two separate problems made the terminal run the wrong shell:

Pty.create took a single argv slice and used argv[0] as both the exec path
and the argument vector's first entry. Session passed {shell, "-shell"},
so the shell received "-zsh" as an ordinary argument rather than as its
argv[0]. Bash quietly tolerated it; zsh rejects it and exits immediately,
which closed the tab and took the window down with it. The exec path and
argv are now separate parameters.

Shell resolution preferred $SHELL, which describes whichever shell launched
us rather than the one the user configured. Inside 'nix develop' that is
Nix's own minimal bash, so the terminal never opened the user's zsh. We now
read the login shell from the passwd database and keep $SHELL only as a
fallback for systems without a usable passwd entry.

shot.sh no longer forces a shell, so screenshots reflect real behavior.
This commit is contained in:
Greyson Parrelli committed 2026-08-11 09:11:53 -04:00
1 parent 738cead680
commit 7af5cf72e6
4 files changed
+60 -14

No files matched your search

+43 -3
View File
@@ -41,6 +41,14 @@ const c = struct {
extern "c" fn _exit(code: c_int) noreturn;
extern "c" fn kill(pid: pid_t, sig: c_int) c_int;
extern "c" fn waitpid(pid: pid_t, status: ?*c_int, options: c_int) pid_t;
extern "c" fn getuid() std.c.uid_t;
extern "c" fn getpwuid_r(
uid: std.c.uid_t,
pwd: *std.c.passwd,
buf: [*]u8,
buflen: usize,
result: *?*std.c.passwd,
) c_int;
};
const O_RDWR = 0x0002;
@@ -69,11 +77,18 @@ pub const Error = error{
ForkFailed,
};
/// Open a PTY pair and fork `argv` onto the slave side. The child gets its
/// Open a PTY pair and fork `path` onto the slave side. The child gets its
/// own session with the slave as controlling terminal, so job control and
/// signals (Ctrl-C, SIGWINCH) work the way a shell expects.
///
/// `path` is the executable to run and `argv` is the argument vector it
/// sees, including argv[0]. These are separate because a login shell is
/// started by running e.g. /usr/bin/zsh with an argv[0] of "-zsh"; folding
/// them together would pass "-zsh" as an ordinary argument instead, which
/// shells either misparse or reject outright.
pub fn create(
alloc: std.mem.Allocator,
path: [:0]const u8,
argv: []const [:0]const u8,
size: Winsize,
) !Pty {
@@ -105,7 +120,7 @@ pub fn create(
const pid = c.fork();
if (pid < 0) return Error.ForkFailed;
if (pid == 0) {
childExec(master, slave_path_z, argv_z, envp_z);
childExec(master, slave_path_z, path, argv_z, envp_z);
// childExec only returns on failure, and a forked child has no
// sensible way to report that back to us.
c._exit(127);
@@ -118,6 +133,7 @@ pub fn create(
fn childExec(
master: fd_t,
slave_path: [:0]const u8,
path: [:0]const u8,
argv: [:null]const ?[*:0]const u8,
envp: [:null]const ?[*:0]const u8,
) void {
@@ -139,7 +155,31 @@ fn childExec(
if (c.dup2(slave, 2) < 0) return;
if (slave > 2) _ = c.close(slave);
_ = c.execvpe(argv[0].?, argv.ptr, envp.ptr);
_ = c.execvpe(path.ptr, argv.ptr, envp.ptr);
}
/// The login shell recorded for the current user in the passwd database,
/// copied into `buf`. Returns null if there is no entry or it names no shell.
///
/// This is the shell the user actually configured (what `chsh` sets). Unlike
/// $SHELL it cannot be overwritten by whatever environment the terminal was
/// launched from.
pub fn loginShell(buf: []u8) ?[]const u8 {
// getpwuid_r writes the strings it returns into a caller-provided scratch
// buffer, kept separate from `buf` so the result survives the copy out.
var scratch: [4096]u8 = undefined;
var pw: std.c.passwd = undefined;
var result: ?*std.c.passwd = null;
if (c.getpwuid_r(c.getuid(), &pw, &scratch, scratch.len, &result) != 0) return null;
const entry = result orelse return null;
const shell = entry.shell orelse return null;
const span = std.mem.span(shell);
if (span.len == 0 or span.len > buf.len) return null;
@memcpy(buf[0..span.len], span);
return buf[0..span.len];
}
/// Copy the current environment, forcing the variables that describe what