Ports the parts of Ghostty's IO layer that don't depend on the terminal: - pty.zig: pty pair, window size, mode, child-side setsid/TIOCSCTTY; master is CLOEXEC with IUTF8 set - Command.zig: fork/execvpe with Ghostty's exact child setup (stdio dup2, best-effort chdir, rlimit restore, os/runtime pre-exec hooks, cgroup handling) and exit status decoding - shell_integration.zig for bash/zsh/fish/elvish/nushell plus feature flags; scripts copied verbatim to resources/shell-integration/ - terminfo generator for xterm-ghostty, byte-identical to Zig's encode output (golden file produced with the real Zig code) This crate opts into unsafe_code = "deny" (fork/exec need it) with allow + SAFETY comments on minimal items. Integration left: config-derived option types are local stand-ins; cgroup D-Bus scope creation is a closure hook; Exec/Termio/Thread/ stream_handler wait for ghostty-terminal. Pty now closes both fds on drop (Zig never auto-closed the slave) - to re-check when wiring Exec. Test Plan: - cargo test -p ghostty-termio (52 tests, incl. real pty spawn test expecting "hi\r\n31 97\r\n" from `echo hi; stty size`)
5.0 KiB
ghostty-termio notes (agent IO1)
Linux only. Crate lints: unsafe_code = "deny" (workspace clippy table copied
into the crate's [lints.clippy]); #[allow(unsafe_code)] only on tiny items
with // SAFETY: (fork, execvpe, _exit, close, sigaction, borrow_raw).
File -> module
pty.zig->pty.rs(Pty,Winsize,Mode,child_pre_exec(master, slave))Command.zig(+apprt/gtk/{pre_exec,post_fork}.zigcgroup logic,cgroup.zigfmtScope) ->command.rstermio/shell_integration.zig->shell_integration.rsterminfo/{main,Source,ghostty}.zig->terminfo/{mod,source,ghostty}.rs(terminfo::ghosttyis aconst Source;encode,encode_to_string,xtgettcap_map). Output verified byte-identical with the Zigencode(golden filetests/data/xterm-ghostty.terminfo, generated withzig run).- Shell scripts copied verbatim to
resources/shell-integration/(workspace root).
Not ported (deferred; need ghostty-terminal / renderer)
Exec.zig, Termio.zig, Thread.zig, stream_handler.zig, mailbox.zig, message.zig,
backend.zig, Options.zig (all reference terminal/renderer/apprt types).
Design / deviations
- Deps
ghostty-configandghostty-terminalREMOVED from Cargo.toml (note in file). Deps: ghostty-os, rustix (pty, termios, process, fs, stdio), nix (signal), libc, log, thiserror. Commanddoes its ownfork+libc::execvpe(not std::process::Command) so the child setup is exactly Ghostty's: dup2 stdio (CLOEXEC cleared), best-effort chdir (failure ignored), restore rlimit (restore_rlimit: Option<Rlimit>replacesglobal_state.rlimits),os_pre_execthenrt_pre_exechooks (Fn() -> Option<u8>,Some(code)=>_exit(code); panics => exit 1), then execvpe. All allocation happens before fork. Exec failure: child prints the same "Requested executable not found" message (other errors:errno=Ninstead of the Zig error name) to stderr and_exit(1); there is noExecFailedInChilderror because the child can never return into caller code.exit_immediately(code)is provided for hooks that want to exit (use instead ofstd::process::exit).- Signal mask: Zig never reset the mask explicitly in Command either; only
Pty::child_pre_execresets 13 signal dispositions to SIG_DFL (same list as Zig), then setsid + TIOCSCTTY and closes slave+master. Close-all-fds is not done (Zig doesn't either; master is CLOEXEC). - stdio fields are
Option<OwnedFd>(callers passtry_clone()d fds);wait(block)returnsResult<Exit, Errno>; non-blocking wait loops until a status is available (as Zig, with ayield_now).setData/getDatadropped (closures capture state). Emptyargs=> argv = [path] (what the Zig doc comment promises; Zig code itself passed an empty argv). - Inherited env is snapshotted with
vars_os()before fork (Zig passesenviron). Pty:slave: Option<OwnedFd>; both fds close on drop (Zig never auto-closed the slave). Master opened CLOEXEC, slave not (like glibc openpty + explicit CLOEXEC). IUTF8 set.- cgroup:
linux_cgroup_pre_exec(&RtPreExecInfo)(child side, waits <=250ms) andlinux_cgroup_post_fork(cmd, &RtPostForkInfo, create_scope)wherecreate_scopeis a closure standing in for the GIO D-Buscgroup.createScope.fmt_scope(pid). LocalLinuxCgroup/GtkSingleInstance/Rt*Infotypes: TODO(integration) from ghostty-config. - shell_integration: local
Command(Shell/Direct) withArgIteratorGeneralquoting rules,ShellIntegrationFeatures(same defaults) - TODO(integration) replace with config types.EnvMap = HashMap<String,String>.setup(resource_dir, &Command, &mut EnvMap, Option<Shell>)->Result<Option<ShellIntegration>, Error>(Error only fromhomedir::home). Darwin/bin/bashspecial case dropped.
Tests (52 total: 47 unit + 5 integration, all passing)
Zig tests ported (35): pty 1; Command 10 (createNullDelimitedEnvMap as prepare(), os/rt pre exec
1+2, rt post fork, redirect stdout, custom env, custom cwd, execve failure); shell_integration 21
(force shell, failure, detectShell, setup features, bash x8, xdg x3, nushell x3, zsh x3);
terminfo 3 (xtgettcap map, Source encode, ghostty encode).
Skipped: Windows/NullPty/Windows env block helpers, refAllDecls tests.
Extras: pty cloexec/IUTF8/mode, Exit decoding, signal exit, nonblocking wait, invalid cwd ignored,
fd leak check, cgroup mode matrices, arg splitter, bundled-resources check, terminfo golden vs Zig,
tic -x compile of the generated source (skipped if tic absent),
tests/pty_command.rs: real /bin/sh -c 'echo hi; stty size' on a 31x97 pty (output
hi\r\n31 97\r\n), controlling-tty check, resize check.
Known gaps / TODO(integration)
- Config-derived types above;
restore_rlimitfrom global state; D-Bus scope creation. Pty::child_pre_execis meant to be called from Exec'sos_pre_exec(see tests/pty_command.rs for the wiring). Flatpak passwd path in ghostty-os needs Pty (ghostty-os TODO) - now available.- Hooks run after fork in a possibly multithreaded process;
linux_cgroup_pre_execallocates and sleeps (same as Zig) - fine on glibc.