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:
@@ -75,7 +75,8 @@ in principle, but a terminal grid is small.
|
||||
|
||||
## What works
|
||||
|
||||
- Real shell on a real PTY, with a controlling terminal so job control,
|
||||
- Your login shell (from the passwd database, not `$SHELL`) on a real PTY,
|
||||
started as a login shell, with a controlling terminal so job control,
|
||||
Ctrl-C and SIGWINCH behave
|
||||
- Full SGR rendering: 16/256/true color, bold, italic, underline,
|
||||
strikethrough, inverse, and the bright-on-bold convention
|
||||
@@ -120,9 +121,6 @@ Sway** and screenshots it. This keeps UI checks entirely out of your real
|
||||
Wayland session — nothing appears on screen, and it works while the session is
|
||||
locked.
|
||||
|
||||
Screenshots use `$SHELL` like the real app does; set
|
||||
`VTABS_SHOT_SHELL=/bin/sh` for output uncluttered by your shell's rc files.
|
||||
|
||||
One caveat when driving it: `wtype` loses the first keystroke of every
|
||||
invocation while the compositor adopts its freshly uploaded keymap, so scripted
|
||||
input should begin with a throwaway key. That is a quirk of the injection tool,
|
||||
|
||||
@@ -40,10 +40,6 @@ export WLR_RENDERER=pixman
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
export GSK_RENDERER=cairo
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
# The nix dev shell points SHELL at a minimal bash (no readline, no
|
||||
# programmable completion), which makes screenshots look broken for reasons
|
||||
# that have nothing to do with vtabs. Use the system shell instead.
|
||||
export SHELL="${VTABS_SHOT_SHELL:-/bin/bash}"
|
||||
export SWAYSOCK="$RUNDIR/sway.sock"
|
||||
# Don't let the app fall back to the real session.
|
||||
unset DISPLAY
|
||||
|
||||
+43
-3
@@ -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
|
||||
|
||||
+15
-3
@@ -96,13 +96,14 @@ pub fn create(
|
||||
const shell = try defaultShell(alloc);
|
||||
defer alloc.free(shell);
|
||||
|
||||
// A leading '-' in argv[0] tells the shell to start as a login shell.
|
||||
// A leading '-' in argv[0] tells the shell to start as a login shell,
|
||||
// so it reads the user's profile.
|
||||
const argv0 = try std.fmt.allocPrintSentinel(alloc, "-{s}", .{
|
||||
std.fs.path.basename(shell),
|
||||
}, 0);
|
||||
defer alloc.free(argv0);
|
||||
|
||||
self.pty = try .create(alloc, &.{ shell, argv0 }, .{
|
||||
self.pty = try .create(alloc, shell, &.{argv0}, .{
|
||||
.ws_row = rows,
|
||||
.ws_col = cols,
|
||||
});
|
||||
@@ -126,12 +127,23 @@ pub fn destroy(self: *Session) void {
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Resolve the user's shell, falling back to something that always exists.
|
||||
/// Resolve the shell to run, most authoritative source first.
|
||||
///
|
||||
/// The passwd entry comes first deliberately. $SHELL is only a description of
|
||||
/// the shell that happened to launch us, and plenty of environments overwrite
|
||||
/// it: `nix develop` replaces it with its own bash, and Flatpak pins it to
|
||||
/// /bin/sh. The passwd entry is what the user actually configured, so it is
|
||||
/// the better answer to "the user's default shell". $SHELL remains a fallback
|
||||
/// for systems with no usable passwd entry, such as minimal containers.
|
||||
fn defaultShell(alloc: std.mem.Allocator) ![:0]const u8 {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
if (Pty.loginShell(&buf)) |shell| return alloc.dupeZ(u8, shell);
|
||||
|
||||
if (std.c.getenv("SHELL")) |sh| {
|
||||
const span = std.mem.span(sh);
|
||||
if (span.len > 0) return alloc.dupeZ(u8, span);
|
||||
}
|
||||
|
||||
return alloc.dupeZ(u8, "/bin/sh");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user