PtySpawnOptions has no cwd, so a pty child always starts in the app process's working directory. Any terminal that should open somewhere specific has to smuggle the cd through the shell's own command line.
The gap, top to bottom
PtySpawnOptions (src/runtime/effects.zig:2890) takes key, argv, cols, rows, term, on_event — and nothing else. startRealPty (:11025) forwards exactly argv, env, term, cols, rows into pty.spawn, whose own SpawnOptions (src/runtime/pty.zig:392) carries the same five fields. There is no directory anywhere on the path, so the child inherits the app's cwd from the fork at src/runtime/pty.zig:641.
fx.spawn's SpawnOptions (:5696) has no cwd either, for what it is worth — so this is a consistent absence rather than an inconsistency. It just costs more on the pty side, because a pty child is nearly always an interactive shell and an interactive shell nearly always wants to start somewhere. An app with several terminals open on different directories has no way to express it.
What the workaround looks like
const script = std.fmt.bufPrint(
&script_storage,
"cd '{s}' && exec {s} -i",
.{ path, login_shell },
) catch { /* fall back to a plain interactive shell */ };
argv_storage[0] = login_shell;
argv_storage[1] = "-c";
argv_storage[2] = script;
This works. It is also three problems:
- It is a quoting bug waiting to happen. A path containing a single quote breaks the script, so the caller has to detect that case and fall back to a shell in the wrong directory rather than run a mis-quoted command. A path is not a shell word and should not have to be turned into one.
- It burns the shell's own
-c. The child becomes a -c shell that execs an -i shell, so -c is unavailable for anything else and the process tree has a shape nobody chose.
- It is posix-only by construction. There is no
cmd/powershell spelling of this trick worth trusting, so a Windows branch simply drops the directory.
The ask
/// The child's initial working directory. Null inherits the
/// app process's, as today.
cwd: ?[]const u8 = null,
on PtySpawnOptions, forwarded through pty.SpawnOptions to a chdir in the child.
The one design constraint is already written down: src/runtime/pty.zig:21 states that everything the child touches between fork and exec is async-signal-safe — "login_tty, dup2, close, execve, and _exit — no allocator, no setenv". chdir is async-signal-safe, and the path can be NUL-terminated before the fork alongside the argv/envp arrays that are already built there (:446), so this fits the existing rule rather than bending it. On Windows CreateProcessW (src/runtime/pty_windows.zig:806) takes lpCurrentDirectory directly.
A failed chdir should presumably fail the spawn the same way a failed execve does, through the exec self-pipe (:601) — "the pty forked but the program could not start" is exactly the case that machinery already distinguishes.
Verified against ea98365.
A patch is up: #220. Filed separately as the defect record — close whichever of the two is the redundant one.
PtySpawnOptionshas nocwd, so a pty child always starts in the app process's working directory. Any terminal that should open somewhere specific has to smuggle thecdthrough the shell's own command line.The gap, top to bottom
PtySpawnOptions(src/runtime/effects.zig:2890) takeskey,argv,cols,rows,term,on_event— and nothing else.startRealPty(:11025) forwards exactlyargv,env,term,cols,rowsintopty.spawn, whose ownSpawnOptions(src/runtime/pty.zig:392) carries the same five fields. There is no directory anywhere on the path, so the child inherits the app's cwd from theforkatsrc/runtime/pty.zig:641.fx.spawn'sSpawnOptions(:5696) has nocwdeither, for what it is worth — so this is a consistent absence rather than an inconsistency. It just costs more on the pty side, because a pty child is nearly always an interactive shell and an interactive shell nearly always wants to start somewhere. An app with several terminals open on different directories has no way to express it.What the workaround looks like
This works. It is also three problems:
-c. The child becomes a-cshell thatexecs an-ishell, so-cis unavailable for anything else and the process tree has a shape nobody chose.cmd/powershellspelling of this trick worth trusting, so a Windows branch simply drops the directory.The ask
on
PtySpawnOptions, forwarded throughpty.SpawnOptionsto achdirin the child.The one design constraint is already written down:
src/runtime/pty.zig:21states that everything the child touches betweenforkandexecis async-signal-safe — "login_tty,dup2,close,execve, and_exit— no allocator, nosetenv".chdiris async-signal-safe, and the path can be NUL-terminated before the fork alongside the argv/envp arrays that are already built there (:446), so this fits the existing rule rather than bending it. On WindowsCreateProcessW(src/runtime/pty_windows.zig:806) takeslpCurrentDirectorydirectly.A failed
chdirshould presumably fail the spawn the same way a failedexecvedoes, through the exec self-pipe (:601) — "the pty forked but the program could not start" is exactly the case that machinery already distinguishes.Verified against
ea98365.A patch is up: #220. Filed separately as the defect record — close whichever of the two is the redundant one.