use crate::StdioLocks;
use libc::{c_int, fcntl, termios, F_GETFL, O_RDWR};
use std::ffi::{CStr, CString, OsStr};
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, stderr, stdin, stdout, IsTerminal};
use std::mem::{self, ManuallyDrop};
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd as _};
use std::os::unix::ffi::OsStrExt;
mod attr;
#[cfg(test)]
mod pty_utils;
#[cfg(test)]
mod tests;
pub(crate) fn terminal() -> io::Result<Terminal> {
None.or_else(|| reuse_tty_from_stdio(stderr).transpose())
.or_else(|| reuse_tty_from_stdio(stdout).transpose())
.or_else(|| reuse_tty_from_stdio(stdin).transpose())
.map(|r| r.and_then(Terminal::from_stdio))
.unwrap_or_else(|| Ok(Terminal::from_controlling(open_controlling_tty()?)))
}
fn reuse_tty_from_stdio<S: IsTerminal + AsFd>(
stream: impl FnOnce() -> S,
) -> io::Result<Option<TerminalFile>> {
let stream = stream();
if stream.is_terminal() {
if is_read_write(stream.as_fd())? {
let file = unsafe { File::from_raw_fd(stream.as_fd().as_raw_fd()) };
Ok(Some(TerminalFile::Borrowed(ManuallyDrop::new(file))))
} else {
reopen_tty(stream.as_fd())
.map(TerminalFile::Owned)
.map(Some)
}
} else {
Ok(None)
}
}
fn open_controlling_tty() -> io::Result<TerminalFile> {
OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.map(TerminalFile::Owned)
}
fn is_read_write(fd: BorrowedFd) -> io::Result<bool> {
let mode = to_io_result(unsafe { fcntl(fd.as_raw_fd(), F_GETFL) })?;
Ok(mode & O_RDWR == O_RDWR)
}
fn reopen_tty(fd: BorrowedFd) -> io::Result<File> {
let name = ttyname_r(fd)?;
OpenOptions::new()
.read(true)
.write(true)
.open(OsStr::from_bytes(name.as_bytes()))
}
fn is_same_file(a: BorrowedFd, b: BorrowedFd) -> io::Result<bool> {
Ok(a.as_raw_fd() == b.as_raw_fd() || {
let stat_a = fstat(a)?;
let stat_b = fstat(b)?;
stat_a.st_dev == stat_b.st_dev && stat_a.st_ino == stat_b.st_ino
})
}
fn fstat(fd: BorrowedFd) -> io::Result<libc::stat> {
let mut stat = unsafe { mem::zeroed() };
to_io_result(unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) })?;
Ok(stat)
}
#[derive(Debug)]
pub(crate) struct Terminal {
file: TerminalFile,
same_as_stdin: bool,
same_as_stdout: bool,
same_as_stderr: bool,
}
impl Terminal {
pub(crate) fn lock_stdio(&self) -> StdioLocks {
StdioLocks {
stdin_lock: self.same_as_stdin.then(|| stdin().lock()),
stdout_lock: self.same_as_stdout.then(|| stdout().lock()),
stderr_lock: self.same_as_stderr.then(|| stderr().lock()),
}
}
pub(crate) fn enable_raw_mode(&mut self) -> io::Result<RawModeGuard<'_>> {
let fd = self.file.as_fd();
let old_termios = attr::get_terminal_attr(fd)?;
if !attr::is_raw_mode_enabled(&old_termios) {
let mut termios = old_termios;
attr::enable_raw_mode(&mut termios);
attr::set_terminal_attr(fd, &termios)?;
Ok(RawModeGuard {
inner: self,
old_termios: Some(old_termios),
})
} else {
Ok(RawModeGuard {
inner: self,
old_termios: None,
})
}
}
pub(crate) fn has_connected_stdio_stream(&self) -> bool {
self.same_as_stdin || self.same_as_stdout || self.same_as_stderr
}
}
impl Terminal {
fn from_stdio(file: TerminalFile) -> io::Result<Self> {
Ok(Terminal {
same_as_stdin: is_same_file(file.as_fd(), stdin().as_fd())?,
same_as_stdout: is_same_file(file.as_fd(), stdout().as_fd())?,
same_as_stderr: is_same_file(file.as_fd(), stderr().as_fd())?,
file,
})
}
fn from_controlling(file: TerminalFile) -> Self {
Terminal {
file,
same_as_stdin: false,
same_as_stdout: false,
same_as_stderr: false,
}
}
}
#[derive(Debug)]
enum TerminalFile {
Owned(File),
Borrowed(ManuallyDrop<File>),
}
impl io::Write for Terminal {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
impl io::Read for Terminal {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file.read(buf)
}
}
impl Deref for TerminalFile {
type Target = File;
fn deref(&self) -> &Self::Target {
match self {
TerminalFile::Owned(f) => f,
TerminalFile::Borrowed(f) => f,
}
}
}
impl DerefMut for TerminalFile {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
TerminalFile::Owned(f) => f,
TerminalFile::Borrowed(f) => f,
}
}
}
impl AsFd for super::Terminal {
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
self.0.file.as_fd()
}
}
impl AsFd for super::TerminalLock<'_> {
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
self.inner.file.as_fd()
}
}
impl AsFd for super::RawModeGuard<'_> {
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
self.0.inner.file.as_fd()
}
}
impl AsRawFd for super::Terminal {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.0.file.as_raw_fd()
}
}
impl AsRawFd for super::TerminalLock<'_> {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.inner.file.as_raw_fd()
}
}
impl AsRawFd for super::RawModeGuard<'_> {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.0.inner.file.as_raw_fd()
}
}
pub(crate) struct RawModeGuard<'a> {
inner: &'a mut Terminal,
old_termios: Option<termios>,
}
impl fmt::Debug for RawModeGuard<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RawModeGuard")
.field("inner", &self.inner)
.finish_non_exhaustive()
}
}
impl io::Write for RawModeGuard<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl io::Read for RawModeGuard<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl Drop for RawModeGuard<'_> {
fn drop(&mut self) {
if let Some(old_termios) = self.old_termios {
_ = attr::set_terminal_attr(self.inner.file.as_fd(), &old_termios);
}
}
}
fn to_io_result(value: c_int) -> io::Result<c_int> {
if value == -1 {
Err(io::Error::last_os_error())
} else {
Ok(value)
}
}
#[cfg(not(target_os = "macos"))]
fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
let mut buf = Vec::with_capacity(64);
loop {
let code = unsafe { libc::ttyname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.capacity()) };
match code {
0 => return Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned()),
libc::ERANGE => buf.reserve(64),
code => return Err(io::Error::from_raw_os_error(code)),
}
}
}
#[cfg(target_os = "macos")]
fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
use libc::{F_GETPATH, PATH_MAX};
let buf: [i8; PATH_MAX as usize] = [0; PATH_MAX as usize];
unsafe {
match fcntl(fd.as_raw_fd(), F_GETPATH as c_int, &buf) {
0 => {
let res = CStr::from_ptr(buf.as_ptr()).to_owned();
Ok(res)
}
_ => Err(io::Error::last_os_error()),
}
}
}