use super::*;
use crate::term::options::{KittyFlags, MouseMode};
use std::mem;
use std::ptr;
fn read_master(fd: RawFd, ms: i32) -> Vec<u8> {
let mut out = Vec::new();
let mut buf = [0u8; 1024];
loop {
let mut p = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut p, 1, ms) };
if rc <= 0 {
break;
}
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n <= 0 {
break;
}
out.extend_from_slice(&buf[..n as usize]);
}
out
}
fn open_pty() -> Option<(RawFd, RawFd)> {
let mut master: libc::c_int = -1;
let mut slave: libc::c_int = -1;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if rc == 0 {
Some((master, slave))
} else {
None
}
}
#[test]
fn pty_enter_emits_modes_and_leave_restores() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
let opts = EnterOptions {
mouse: MouseMode::ButtonDrag,
kitty_keyboard: KittyFlags::standard(),
..EnterOptions::default()
};
term.enter(&opts).expect("enter");
let entered = read_master(master, 200);
let s = String::from_utf8_lossy(&entered);
assert!(s.contains("\x1b[?1049h"), "altscreen on: {s:?}");
assert!(s.contains("\x1b[?1006h"), "sgr mouse on: {s:?}");
assert!(s.contains("\x1b[>3u"), "kitty push: {s:?}");
term.write(b"hello").unwrap();
term.flush().unwrap();
assert_eq!(read_master(master, 200), b"hello");
term.leave().expect("leave");
let left = String::from_utf8_lossy(&read_master(master, 200)).into_owned();
assert!(left.contains("\x1b[<u"), "kitty pop: {left:?}");
assert!(
left.ends_with("\x1b[?1049l"),
"altscreen off last: {left:?}"
);
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_read_bytes_and_deadline() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
unsafe {
libc::write(master, b"ab\n".as_ptr() as *const libc::c_void, 3);
}
let got = match term.read(Some(Instant::now() + Duration::from_millis(500))) {
Ok(TermRead::Input(b)) => b.to_vec(),
other => panic!("expected input, got {other:?}"),
};
assert!(!got.is_empty());
let t0 = Instant::now();
match term.read(Some(t0 + Duration::from_millis(50))).unwrap() {
TermRead::Idle => {}
other => panic!("expected idle, got {other:?}"),
}
assert!(t0.elapsed() >= Duration::from_millis(45));
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_waker_interrupts_blocking_read() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
let waker = term.waker().expect("wake pipe should exist");
waker.wake();
waker.wake();
match term.read(Some(Instant::now() + Duration::from_secs(2))) {
Ok(TermRead::Wake) => {}
other => panic!("expected wake, got {other:?}"),
}
let w2 = waker.clone();
let t0 = Instant::now();
let th = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
w2.wake();
});
match term.read(Some(Instant::now() + Duration::from_secs(5))) {
Ok(TermRead::Wake) => {}
other => panic!("expected wake, got {other:?}"),
}
assert!(t0.elapsed() < Duration::from_secs(2), "woke promptly");
th.join().unwrap();
unsafe {
libc::write(master, b"x\n".as_ptr() as *const libc::c_void, 2);
}
let mut p = libc::pollfd {
fd: term.read_fd,
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut p, 1, 2000) };
assert!(rc > 0, "pty byte never became readable");
waker.wake();
let deadline = Some(Instant::now() + Duration::from_secs(2));
match term.read(deadline) {
Ok(TermRead::Input(b)) => assert!(b.starts_with(b"x"), "got {b:?}"),
other => panic!("expected input first, got {other:?}"),
}
match term.read(deadline) {
Ok(TermRead::Wake) => {}
other => panic!("expected the wake to survive, got {other:?}"),
}
drop(term);
waker.wake();
unsafe { libc::close(master) };
}
#[test]
fn pty_cell_pixel_size_from_winsize() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
let mut ws: libc::winsize = unsafe { mem::zeroed() };
ws.ws_col = 80;
ws.ws_row = 24;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as IoctlReq, &ws) };
assert_eq!(term.cell_pixel_size(), None);
ws.ws_xpixel = 80 * 9;
ws.ws_ypixel = 24 * 18;
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as IoctlReq, &ws) };
assert_eq!(
term.cell_pixel_size(),
Some(crate::base::PixelSize::new(9, 18))
);
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_is_tty_and_pipe_is_not() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let term = UnixTerminal::from_fds(slave, slave, true);
assert!(term.is_tty(), "a pty slave is a tty");
drop(term);
unsafe { libc::close(master) };
let mut fds = [0 as libc::c_int; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let term = UnixTerminal::from_fds(fds[0], fds[1], true);
assert!(!term.is_tty(), "a pipe is not a tty");
drop(term);
}
#[test]
fn pty_session_verbs_emit_and_leave_resets() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
term.enter(&EnterOptions::default()).expect("enter");
let _ = read_master(master, 100);
term.set_cursor_style(crate::term::CursorStyle::SteadyBar)
.unwrap();
term.set_title("abstract — demo").unwrap();
term.set_title("second").unwrap(); term.clipboard_copy("hi").unwrap();
term.bell().unwrap();
term.notify("done", crate::term::NotifyChannel::Osc9)
.unwrap();
term.notify("kdone", crate::term::NotifyChannel::Osc99)
.unwrap();
term.notify("belled", crate::term::NotifyChannel::BellOnly)
.unwrap();
term.flush().unwrap();
let sent = read_master(master, 200);
let s = String::from_utf8_lossy(&sent);
assert!(s.contains("\x1b[6 q"), "cursor style: {s:?}");
assert_eq!(s.matches("\x1b[22;0t").count(), 1, "one title push: {s:?}");
assert!(s.contains("\x1b]0;abstract — demo\x1b\\"), "{s:?}");
assert!(s.contains("\x1b]0;second\x1b\\"), "{s:?}");
assert!(s.contains("\x1b]52;c;aGk=\x1b\\"), "clipboard: {s:?}");
assert!(s.contains("\x1b]9;done\x1b\\"), "notify osc9: {s:?}");
assert!(s.contains("\x1b]99;;kdone\x1b\\"), "notify osc99: {s:?}");
assert_eq!(s.matches('\x07').count(), 2, "bell + bell-fallback: {s:?}");
term.leave().expect("leave");
let left = String::from_utf8_lossy(&read_master(master, 200)).into_owned();
let altscreen_off = left.find("\x1b[?1049l").expect("altscreen exit");
let style_reset = left.find("\x1b[0 q").expect("cursor style reset");
let title_pop = left.find("\x1b[23;0t").expect("title pop");
assert!(
style_reset > altscreen_off && title_pop > altscreen_off,
"verb resets apply to the main screen: {left:?}"
);
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_suspend_restores_then_reenters() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
let opts = EnterOptions {
kitty_keyboard: KittyFlags::standard(),
..EnterOptions::default()
};
term.enter(&opts).expect("enter");
term.set_cursor_style(crate::term::CursorStyle::SteadyBar)
.unwrap();
term.flush().unwrap();
let _ = read_master(master, 100);
term.suspend().expect("suspend round-trip");
let bytes = read_master(master, 200);
let s = String::from_utf8_lossy(&bytes);
let off = s.find("\x1b[?1049l").expect("left altscreen");
let style_reset = s.find("\x1b[0 q").expect("style reset on suspend");
let back_on = s.rfind("\x1b[?1049h").expect("re-entered altscreen");
let kitty_back = s.rfind("\x1b[>3u").expect("kitty flags re-pushed");
assert!(
off < back_on && style_reset < back_on && kitty_back > off,
"{s:?}"
);
let kitty_pop = s.find("\x1b[<u").expect("kitty pop on suspend");
let paste_off = s.find("\x1b[?2004l").expect("paste off on suspend");
let paste_back = s.rfind("\x1b[?2004h").expect("paste re-enabled");
assert!(kitty_pop < paste_off, "pop before paste-off: {s:?}");
assert!(paste_off < paste_back, "restore after teardown: {s:?}");
assert!(paste_back < kitty_back, "paste on before kitty push: {s:?}");
term.write(b"post").unwrap();
term.flush().unwrap();
assert_eq!(read_master(master, 200), b"post");
term.leave().unwrap();
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_runtime_kitty_push_pops_on_suspend_repushes_on_resume_and_pops_on_leave() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
term.enter(&EnterOptions::default()).expect("enter"); let entered = read_master(master, 100);
assert!(
!String::from_utf8_lossy(&entered).contains("\x1b[>"),
"enter must not push: env could not claim the protocol"
);
term.set_kitty_keyboard(KittyFlags::standard()).unwrap();
term.flush().unwrap();
let s = String::from_utf8_lossy(&read_master(master, 100)).into_owned();
assert!(s.contains("\x1b[>3u"), "runtime push: {s:?}");
assert!(!s.contains("\x1b[<u"), "first push pops nothing: {s:?}");
term.set_kitty_keyboard(KittyFlags::standard()).unwrap();
term.flush().unwrap();
assert!(
read_master(master, 60).is_empty(),
"re-setting identical flags must emit nothing"
);
term.suspend().expect("suspend round-trip");
let s = String::from_utf8_lossy(&read_master(master, 200)).into_owned();
let pop = s.find("\x1b[<u").expect("suspend pops the runtime push");
let off = s.find("\x1b[?1049l").expect("left altscreen");
let back = s.rfind("\x1b[?1049h").expect("re-entered altscreen");
let repush = s.rfind("\x1b[>3u").expect("resume re-pushes");
assert!(
pop < off && back < repush,
"pop inside teardown, re-push inside re-enter: {s:?}"
);
term.leave().expect("leave");
let s = String::from_utf8_lossy(&read_master(master, 100)).into_owned();
assert_eq!(
s.matches("\x1b[<u").count(),
1,
"final leave pops exactly the one live push: {s:?}"
);
drop(term);
unsafe { libc::close(master) };
}
#[test]
fn pty_resize_via_sigwinch_pipe() {
let Some((master, slave)) = open_pty() else {
eprintln!("skipping: openpty unavailable in this environment");
return;
};
let mut term = UnixTerminal::from_fds(slave, slave, true);
let mut ws: libc::winsize = unsafe { mem::zeroed() };
ws.ws_col = 80;
ws.ws_row = 24;
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as IoctlReq, &ws) };
term.enter(&EnterOptions::default()).expect("enter");
let _ = read_master(master, 100);
ws.ws_col = 132;
ws.ws_row = 43;
unsafe {
libc::ioctl(master, libc::TIOCSWINSZ as IoctlReq, &ws);
libc::kill(libc::getpid(), libc::SIGWINCH);
}
match term.read(Some(Instant::now() + Duration::from_secs(2))) {
Ok(TermRead::Resize(sz)) => assert_eq!(sz, Size::new(132, 43)),
other => panic!("expected resize, got {other:?}"),
}
term.leave().unwrap();
drop(term);
unsafe { libc::close(master) };
}