use std::{
ffi::{c_int, c_void},
fs::{File, OpenOptions},
io::{self, Read, Write},
mem::MaybeUninit,
os::fd::{AsFd, AsRawFd, BorrowedFd},
sync::atomic::{AtomicBool, Ordering},
};
use libc::{
CS7, CS8, ECHO, ECHOCTL, ECHOE, ECHOK, ECHOKE, ECHONL, ICANON, ICRNL, IEXTEN, IGNCR, IGNPAR,
IMAXBEL, INLCR, INPCK, ISIG, ISTRIP, IXANY, IXOFF, IXON, NOFLSH, OCRNL, ONLCR, ONLRET, ONOCR,
OPOST, PARENB, PARMRK, PARODD, PENDIN, SIGTTOU, TCSADRAIN, TCSAFLUSH, TIOCGWINSZ, TIOCSWINSZ,
TOSTOP, cfgetispeed, cfgetospeed, cfmakeraw, cfsetispeed, cfsetospeed, ioctl, sigaction,
sigemptyset, sighandler_t, siginfo_t, sigset_t, tcflag_t, tcgetattr, tcsetattr, termios,
winsize,
};
#[cfg(target_os = "linux")]
use libc::{IUTF8, OLCUC};
#[cfg(not(target_os = "linux"))]
const IUTF8: libc::tcflag_t = 0;
#[cfg(not(target_os = "linux"))]
const OLCUC: libc::tcflag_t = 0;
use super::{TermSize, Terminal};
use crate::{
cutils::cerr,
system::{interface::ProcessId, make_zeroed_sigaction},
};
const INPUT_FLAGS: tcflag_t = IGNPAR
| PARMRK
| INPCK
| ISTRIP
| INLCR
| IGNCR
| ICRNL
| IXON
| IXANY
| IXOFF
| IMAXBEL
| IUTF8;
const OUTPUT_FLAGS: tcflag_t = OPOST | OLCUC | ONLCR | OCRNL | ONOCR | ONLRET;
const CONTROL_FLAGS: tcflag_t = CS7 | CS8 | PARENB | PARODD;
const LOCAL_FLAGS: tcflag_t = ISIG
| ICANON
| ECHO
| ECHOE
| ECHOK
| ECHONL
| NOFLSH
| TOSTOP
| IEXTEN
| ECHOCTL
| ECHOKE
| PENDIN;
static GOT_SIGTTOU: AtomicBool = AtomicBool::new(false);
unsafe fn tcsetattr_nobg(fd: c_int, flags: c_int, tp: *const termios) -> io::Result<()> {
let setattr = || cerr(unsafe { tcsetattr(fd, flags, tp) }).map(|_| ());
catching_sigttou(setattr)
}
fn catching_sigttou(mut function: impl FnMut() -> io::Result<()>) -> io::Result<()> {
extern "C" fn on_sigttou(_signal: c_int, _info: *mut siginfo_t, _: *mut c_void) {
GOT_SIGTTOU.store(true, Ordering::SeqCst);
}
let action = {
let mut raw: libc::sigaction = make_zeroed_sigaction();
raw.sa_sigaction = on_sigttou as *const () as sighandler_t;
raw.sa_mask = {
let mut sa_mask = MaybeUninit::<sigset_t>::uninit();
unsafe {
sigemptyset(sa_mask.as_mut_ptr());
sa_mask.assume_init()
}
};
raw.sa_flags = 0;
raw
};
GOT_SIGTTOU.store(false, Ordering::SeqCst);
let original_action = unsafe {
let mut original_action = MaybeUninit::<sigaction>::uninit();
sigaction(SIGTTOU, &action, original_action.as_mut_ptr());
original_action.assume_init()
};
let result = loop {
match function() {
Ok(_) => break Ok(()),
Err(err) => {
let got_sigttou = GOT_SIGTTOU.load(Ordering::SeqCst);
if got_sigttou || err.kind() != io::ErrorKind::Interrupted {
break Err(err);
}
}
}
};
unsafe { sigaction(SIGTTOU, &original_action, std::ptr::null_mut()) };
result
}
pub struct UserTerm {
tty: File,
original_termios: Option<termios>,
}
impl UserTerm {
pub fn open() -> io::Result<Self> {
Ok(Self {
tty: OpenOptions::new().read(true).write(true).open("/dev/tty")?,
original_termios: None,
})
}
pub(crate) fn get_size(&self) -> io::Result<TermSize> {
let mut term_size = MaybeUninit::<TermSize>::uninit();
cerr(unsafe {
ioctl(
self.tty.as_raw_fd(),
TIOCGWINSZ,
term_size.as_mut_ptr().cast::<winsize>(),
)
})?;
Ok(unsafe { term_size.assume_init() })
}
pub fn copy_to<D: AsFd>(&self, dst: &D) -> io::Result<()> {
let src = self.tty.as_raw_fd();
let dst = dst.as_fd().as_raw_fd();
let (tt_src, mut tt_dst) = unsafe {
let mut tt_src = MaybeUninit::<termios>::uninit();
let mut tt_dst = MaybeUninit::<termios>::uninit();
cerr(tcgetattr(src, tt_src.as_mut_ptr()))?;
cerr(tcgetattr(dst, tt_dst.as_mut_ptr()))?;
(tt_src.assume_init(), tt_dst.assume_init())
};
tt_dst.c_iflag &= !INPUT_FLAGS;
tt_dst.c_oflag &= !OUTPUT_FLAGS;
tt_dst.c_cflag &= !CONTROL_FLAGS;
tt_dst.c_lflag &= !LOCAL_FLAGS;
tt_dst.c_iflag |= tt_src.c_iflag & INPUT_FLAGS;
tt_dst.c_oflag |= tt_src.c_oflag & OUTPUT_FLAGS;
tt_dst.c_cflag |= tt_src.c_cflag & CONTROL_FLAGS;
tt_dst.c_lflag |= tt_src.c_lflag & LOCAL_FLAGS;
tt_dst.c_cc.copy_from_slice(&tt_src.c_cc);
unsafe {
let mut speed = cfgetospeed(&tt_src);
if speed == libc::B0 {
speed = libc::B38400;
}
cfsetospeed(&mut tt_dst, speed);
speed = cfgetispeed(&tt_src);
cfsetispeed(&mut tt_dst, speed);
}
unsafe { tcsetattr_nobg(dst, TCSAFLUSH, &tt_dst) }?;
let mut wsize = MaybeUninit::<winsize>::uninit();
cerr(unsafe { ioctl(src, TIOCGWINSZ, wsize.as_mut_ptr()) })?;
cerr(unsafe { ioctl(dst, TIOCSWINSZ, wsize.as_ptr()) })?;
Ok(())
}
pub fn set_raw_mode(&mut self, with_signals: bool, preserve_oflag: bool) -> io::Result<()> {
let fd = self.tty.as_raw_fd();
let mut term = if let Some(termios) = self.original_termios {
termios
} else {
*self.original_termios.insert(unsafe {
let mut termios = MaybeUninit::uninit();
cerr(tcgetattr(fd, termios.as_mut_ptr()))?;
termios.assume_init()
})
};
let oflag = term.c_oflag;
unsafe { cfmakeraw(&mut term) };
if preserve_oflag {
term.c_oflag = oflag;
}
if with_signals {
term.c_cflag |= ISIG;
}
unsafe { tcsetattr_nobg(fd, TCSADRAIN, &term) }?;
Ok(())
}
pub fn restore(&mut self, flush: bool) -> io::Result<()> {
if let Some(termios) = self.original_termios.take() {
let fd = self.tty.as_raw_fd();
let flags = if flush { TCSAFLUSH } else { TCSADRAIN };
unsafe { tcsetattr_nobg(fd, flags, &termios) }?;
}
Ok(())
}
pub fn tcsetpgrp_nobg(&self, pgrp: ProcessId) -> io::Result<()> {
catching_sigttou(|| self.tcsetpgrp(pgrp))
}
}
impl AsFd for UserTerm {
fn as_fd(&self) -> BorrowedFd<'_> {
self.tty.as_fd()
}
}
impl Read for UserTerm {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.tty.read(buf)
}
}
impl Write for UserTerm {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.tty.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.tty.flush()
}
}