feat(termio): port pty, child command spawning, shell integration
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`)
This commit is contained in:
1 parent
6792799fbe
commit
59bc84fde0
21 files changed
+7120
-28
No files matched your search
@@ -0,0 +1,269 @@
|
||||
xterm-ghostty|ghostty|Ghostty,
|
||||
am,
|
||||
bce,
|
||||
ccc,
|
||||
hs,
|
||||
km,
|
||||
mc5i,
|
||||
mir,
|
||||
msgr,
|
||||
npc,
|
||||
xenl,
|
||||
AX,
|
||||
Tc,
|
||||
Su,
|
||||
XT,
|
||||
fullkbd,
|
||||
colors#256,
|
||||
cols#80,
|
||||
it#8,
|
||||
lines#24,
|
||||
pairs#32767,
|
||||
acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
|
||||
Smulx=\E[4:%p1%dm,
|
||||
Setulc=\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
|
||||
Ss=\E[%p1%d q,
|
||||
Se=\E[2 q,
|
||||
Ms=\E]52;%p1%s;%p2%s\007,
|
||||
Sync=\E[?2026%?%p1%{1}%-%tl%eh%;,
|
||||
BD=\E[?2004l,
|
||||
BE=\E[?2004h,
|
||||
PS=\E[200~,
|
||||
PE=\E[201~,
|
||||
XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
|
||||
xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
|
||||
RV=\E[>c,
|
||||
rv=\E\\[[0-9]+;[0-9]+;[0-9]+c,
|
||||
XR=\E[>0q,
|
||||
xr=\EP>\\|[ -~]+a\E\\,
|
||||
Enmg=\E[?69h,
|
||||
Dsmg=\E[?69l,
|
||||
Clmg=\E[s,
|
||||
Cmg=\E[%i%p1%d;%p2%ds,
|
||||
clear=\E[H\E[2J,
|
||||
E3=\E[3J,
|
||||
fe=\E[?1004h,
|
||||
fd=\E[?1004l,
|
||||
kxIN=\E[I,
|
||||
kxOUT=\E[O,
|
||||
bel=^G,
|
||||
blink=\E[5m,
|
||||
bold=\E[1m,
|
||||
cbt=\E[Z,
|
||||
civis=\E[?25l,
|
||||
cnorm=\E[?12l\E[?25h,
|
||||
cr=\r,
|
||||
csr=\E[%i%p1%d;%p2%dr,
|
||||
cub=\E[%p1%dD,
|
||||
cub1=^H,
|
||||
cud=\E[%p1%dB,
|
||||
cud1=^J,
|
||||
cuf=\E[%p1%dC,
|
||||
cuf1=\E[C,
|
||||
cup=\E[%i%p1%d;%p2%dH,
|
||||
cuu=\E[%p1%dA,
|
||||
cuu1=\E[A,
|
||||
cvvis=\E[?12;25h,
|
||||
dch=\E[%p1%dP,
|
||||
dch1=\E[P,
|
||||
dim=\E[2m,
|
||||
dl=\E[%p1%dM,
|
||||
dl1=\E[M,
|
||||
dsl=\E]2;\007,
|
||||
ech=\E[%p1%dX,
|
||||
ed=\E[J,
|
||||
el=\E[K,
|
||||
el1=\E[1K,
|
||||
flash=\E[?5h$<100/>\E[?5l,
|
||||
fsl=^G,
|
||||
home=\E[H,
|
||||
hpa=\E[%i%p1%dG,
|
||||
ht=^I,
|
||||
hts=\EH,
|
||||
ich=\E[%p1%d@,
|
||||
ich1=\E[@,
|
||||
il=\E[%p1%dL,
|
||||
il1=\E[L,
|
||||
ind=\n,
|
||||
indn=\E[%p1%dS,
|
||||
initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
|
||||
invis=\E[8m,
|
||||
oc=\E]104\007,
|
||||
op=\E[39;49m,
|
||||
rc=\E8,
|
||||
rep=%p1%c\E[%p2%{1}%-%db,
|
||||
rev=\E[7m,
|
||||
ri=\EM,
|
||||
rin=\E[%p1%dT,
|
||||
ritm=\E[23m,
|
||||
rmacs=\E(B,
|
||||
rmam=\E[?7l,
|
||||
rmcup=\E[?1049l,
|
||||
rmir=\E[4l,
|
||||
rmkx=\E[?1l\E>,
|
||||
rmso=\E[27m,
|
||||
rmul=\E[24m,
|
||||
rmxx=\E[29m,
|
||||
setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
|
||||
setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
|
||||
setrgbb=\E[48:2:%p1%d:%p2%d:%p3%dm,
|
||||
setrgbf=\E[38:2:%p1%d:%p2%d:%p3%dm,
|
||||
sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
|
||||
sgr0=\E(B\E[m,
|
||||
sitm=\E[3m,
|
||||
smacs=\E(0,
|
||||
smam=\E[?7h,
|
||||
smcup=\E[?1049h,
|
||||
smir=\E[4h,
|
||||
smkx=\E[?1h\E=,
|
||||
smso=\E[7m,
|
||||
smul=\E[4m,
|
||||
smxx=\E[9m,
|
||||
tbc=\E[3g,
|
||||
tsl=\E]2;,
|
||||
u6=\E[%i%d;%dR,
|
||||
u7=\E[6n,
|
||||
u8=\E[?%[;0123456789]c,
|
||||
u9=\E[c,
|
||||
vpa=\E[%i%p1%dd,
|
||||
kDC=\E[3;2~,
|
||||
kDC3=\E[3;3~,
|
||||
kDC4=\E[3;4~,
|
||||
kDC5=\E[3;5~,
|
||||
kDC6=\E[3;6~,
|
||||
kDC7=\E[3;7~,
|
||||
kDN=\E[1;2B,
|
||||
kDN3=\E[1;3B,
|
||||
kDN4=\E[1;4B,
|
||||
kDN5=\E[1;5B,
|
||||
kDN6=\E[1;6B,
|
||||
kDN7=\E[1;7B,
|
||||
kEND=\E[1;2F,
|
||||
kEND3=\E[1;3F,
|
||||
kEND4=\E[1;4F,
|
||||
kEND5=\E[1;5F,
|
||||
kEND6=\E[1;6F,
|
||||
kEND7=\E[1;7F,
|
||||
kHOM=\E[1;2H,
|
||||
kHOM3=\E[1;3H,
|
||||
kHOM4=\E[1;4H,
|
||||
kHOM5=\E[1;5H,
|
||||
kHOM6=\E[1;6H,
|
||||
kHOM7=\E[1;7H,
|
||||
kIC=\E[2;2~,
|
||||
kIC3=\E[2;3~,
|
||||
kIC4=\E[2;4~,
|
||||
kIC5=\E[2;5~,
|
||||
kIC6=\E[2;6~,
|
||||
kIC7=\E[2;7~,
|
||||
kLFT=\E[1;2D,
|
||||
kLFT3=\E[1;3D,
|
||||
kLFT4=\E[1;4D,
|
||||
kLFT5=\E[1;5D,
|
||||
kLFT6=\E[1;6D,
|
||||
kLFT7=\E[1;7D,
|
||||
kNXT=\E[6;2~,
|
||||
kNXT3=\E[6;3~,
|
||||
kNXT4=\E[6;4~,
|
||||
kNXT5=\E[6;5~,
|
||||
kNXT6=\E[6;6~,
|
||||
kNXT7=\E[6;7~,
|
||||
kPRV=\E[5;2~,
|
||||
kPRV3=\E[5;3~,
|
||||
kPRV4=\E[5;4~,
|
||||
kPRV5=\E[5;5~,
|
||||
kPRV6=\E[5;6~,
|
||||
kPRV7=\E[5;7~,
|
||||
kRIT=\E[1;2C,
|
||||
kRIT3=\E[1;3C,
|
||||
kRIT4=\E[1;4C,
|
||||
kRIT5=\E[1;5C,
|
||||
kRIT6=\E[1;6C,
|
||||
kRIT7=\E[1;7C,
|
||||
kUP=\E[1;2A,
|
||||
kUP3=\E[1;3A,
|
||||
kUP4=\E[1;4A,
|
||||
kUP5=\E[1;5A,
|
||||
kUP6=\E[1;6A,
|
||||
kUP7=\E[1;7A,
|
||||
kbs=^?,
|
||||
kcbt=\E[Z,
|
||||
kcub1=\EOD,
|
||||
kcud1=\EOB,
|
||||
kcuf1=\EOC,
|
||||
kcuu1=\EOA,
|
||||
kdch1=\E[3~,
|
||||
kend=\EOF,
|
||||
kent=\EOM,
|
||||
kf1=\EOP,
|
||||
kf10=\E[21~,
|
||||
kf11=\E[23~,
|
||||
kf12=\E[24~,
|
||||
kf13=\E[1;2P,
|
||||
kf14=\E[1;2Q,
|
||||
kf15=\E[1;2R,
|
||||
kf16=\E[1;2S,
|
||||
kf17=\E[15;2~,
|
||||
kf18=\E[17;2~,
|
||||
kf19=\E[18;2~,
|
||||
kf2=\EOQ,
|
||||
kf20=\E[19;2~,
|
||||
kf21=\E[20;2~,
|
||||
kf22=\E[21;2~,
|
||||
kf23=\E[23;2~,
|
||||
kf24=\E[24;2~,
|
||||
kf25=\E[1;5P,
|
||||
kf26=\E[1;5Q,
|
||||
kf27=\E[1;5R,
|
||||
kf28=\E[1;5S,
|
||||
kf29=\E[15;5~,
|
||||
kf3=\EOR,
|
||||
kf30=\E[17;5~,
|
||||
kf31=\E[18;5~,
|
||||
kf32=\E[19;5~,
|
||||
kf33=\E[20;5~,
|
||||
kf34=\E[21;5~,
|
||||
kf35=\E[23;5~,
|
||||
kf36=\E[24;5~,
|
||||
kf37=\E[1;6P,
|
||||
kf38=\E[1;6Q,
|
||||
kf39=\E[1;6R,
|
||||
kf4=\EOS,
|
||||
kf40=\E[1;6S,
|
||||
kf41=\E[15;6~,
|
||||
kf42=\E[17;6~,
|
||||
kf43=\E[18;6~,
|
||||
kf44=\E[19;6~,
|
||||
kf45=\E[20;6~,
|
||||
kf46=\E[21;6~,
|
||||
kf47=\E[23;6~,
|
||||
kf48=\E[24;6~,
|
||||
kf49=\E[1;3P,
|
||||
kf5=\E[15~,
|
||||
kf50=\E[1;3Q,
|
||||
kf51=\E[1;3R,
|
||||
kf52=\E[1;3S,
|
||||
kf53=\E[15;3~,
|
||||
kf54=\E[17;3~,
|
||||
kf55=\E[18;3~,
|
||||
kf56=\E[19;3~,
|
||||
kf57=\E[20;3~,
|
||||
kf58=\E[21;3~,
|
||||
kf59=\E[23;3~,
|
||||
kf6=\E[17~,
|
||||
kf60=\E[24;3~,
|
||||
kf61=\E[1;4P,
|
||||
kf62=\E[1;4Q,
|
||||
kf63=\E[1;4R,
|
||||
kf7=\E[18~,
|
||||
kf8=\E[19~,
|
||||
kf9=\E[20~,
|
||||
khome=\EOH,
|
||||
kich1=\E[2~,
|
||||
kind=\E[1;2B,
|
||||
kmous=\E[<,
|
||||
knp=\E[6~,
|
||||
kpp=\E[5~,
|
||||
kri=\E[1;2A,
|
||||
rs1=\E]\E\\\Ec,
|
||||
sc=\E7,
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Integration test: run a real child attached to a pty.
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
os::fd::{AsRawFd, OwnedFd},
|
||||
};
|
||||
|
||||
use ghostty_termio::{Command, Pty, Winsize, command::Exit, pty::child_pre_exec};
|
||||
|
||||
fn dup(fd: &OwnedFd) -> OwnedFd {
|
||||
fd.try_clone().unwrap()
|
||||
}
|
||||
|
||||
/// Read from the master until the child side hangs up (EIO on Linux).
|
||||
fn read_until_hangup(master: OwnedFd) -> String {
|
||||
let mut f = File::from(master);
|
||||
let mut out = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match f.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => out.extend_from_slice(&buf[..n]),
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
fn run_on_pty(script: &str, size: Winsize) -> (String, Exit) {
|
||||
let mut pty = Pty::open(size).unwrap();
|
||||
let slave = pty.slave.take().unwrap();
|
||||
|
||||
let (master_raw, _) = pty.raw_fds();
|
||||
let slave_raw = slave.as_raw_fd();
|
||||
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-c", script]);
|
||||
cmd.stdin = Some(dup(&slave));
|
||||
cmd.stdout = Some(dup(&slave));
|
||||
cmd.stderr = Some(dup(&slave));
|
||||
cmd.os_pre_exec = Some(Box::new(move || {
|
||||
// Make the slave our controlling terminal in a new session; this
|
||||
// also closes the master/slave descriptors in the child.
|
||||
match child_pre_exec(master_raw, slave_raw) {
|
||||
Ok(()) => None,
|
||||
Err(_) => Some(1),
|
||||
}
|
||||
}));
|
||||
cmd.start().unwrap();
|
||||
|
||||
// Close our copies of the slave so we see a hangup when the child exits.
|
||||
drop(cmd.stdin.take());
|
||||
drop(cmd.stdout.take());
|
||||
drop(cmd.stderr.take());
|
||||
drop(slave);
|
||||
|
||||
let exit = cmd.wait(true).unwrap();
|
||||
let out = read_until_hangup(pty.master);
|
||||
(out, exit)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_and_stty_size_on_pty() {
|
||||
let size = Winsize {
|
||||
ws_row: 31,
|
||||
ws_col: 97,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
let (out, exit) = run_on_pty("echo hi; stty size", size);
|
||||
assert_eq!(exit, Exit::Exited(0));
|
||||
// A pty translates \n to \r\n.
|
||||
assert_eq!(out, "hi\r\n31 97\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_has_controlling_terminal() {
|
||||
// `tty` succeeds only with a terminal on stdin; /dev/tty only opens when
|
||||
// we have a controlling terminal (TIOCSCTTY).
|
||||
let (out, exit) = run_on_pty(
|
||||
"tty >/dev/null && echo tty; echo x > /dev/tty && echo ctty",
|
||||
Winsize::default(),
|
||||
);
|
||||
assert_eq!(exit, Exit::Exited(0));
|
||||
assert!(out.contains("tty\r\n"), "{out:?}");
|
||||
assert!(out.contains("ctty\r\n"), "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_is_visible_to_child() {
|
||||
let size = Winsize {
|
||||
ws_row: 10,
|
||||
ws_col: 20,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
let (out, _) = run_on_pty("stty size", size);
|
||||
assert_eq!(out, "10 20\r\n");
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! The terminfo source emitted by the Rust port must be byte-identical to the
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
//! output of the Zig `terminfo/ghostty.zig` `encode` (golden file generated
|
||||
//! with the Zig reference implementation).
|
||||
|
||||
use ghostty_termio::terminfo;
|
||||
|
||||
#[test]
|
||||
fn terminfo_source_matches_zig_output() {
|
||||
let expected = include_str!("data/xterm-ghostty.terminfo");
|
||||
assert_eq!(expected, terminfo::ghostty.encode_to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminfo_compiles_with_tic_if_available() {
|
||||
let Ok(dir) = std::env::var("CARGO_TARGET_TMPDIR") else {
|
||||
return;
|
||||
};
|
||||
let src = std::path::Path::new(&dir).join("xterm-ghostty.src");
|
||||
std::fs::write(&src, terminfo::ghostty.encode_to_string()).unwrap();
|
||||
let out = std::path::Path::new(&dir).join("terminfo-out");
|
||||
let _ = std::fs::create_dir_all(&out);
|
||||
let Ok(status) = std::process::Command::new("tic")
|
||||
.arg("-x")
|
||||
.arg("-o")
|
||||
.arg(&out)
|
||||
.arg(&src)
|
||||
.status()
|
||||
else {
|
||||
return; // tic not installed
|
||||
};
|
||||
assert!(status.success());
|
||||
}
|
||||
Reference in new issue
Block a user