use crate::error::{Result, RustlineError};
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal};
use nix::sys::termios::{self, Termios};
use nix::unistd;
use std::os::unix::io::{BorrowedFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false);
static GOT_SIGCONT: AtomicBool = AtomicBool::new(false);
const UNSUPPORTED_TERMS: &[&str] = &["dumb", "cons25", "emacs"];
const DEFAULT_ROWS: u16 = 24;
const DEFAULT_COLS: u16 = 80;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WinSize {
pub rows: u16,
pub cols: u16,
}
impl Default for WinSize {
fn default() -> Self {
WinSize {
rows: DEFAULT_ROWS,
cols: DEFAULT_COLS,
}
}
}
impl WinSize {
#[must_use]
fn clamped(self) -> Self {
WinSize {
rows: if self.rows == 0 {
DEFAULT_ROWS
} else {
self.rows
},
cols: if self.cols == 0 {
DEFAULT_COLS
} else {
self.cols
},
}
}
#[must_use]
pub fn cols(self) -> usize {
self.cols as usize
}
#[must_use]
pub fn rows(self) -> usize {
self.rows as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Wait {
Ready,
Timeout,
Interrupted,
}
pub struct RawMode {
fd: RawFd,
original: Termios,
prev_winch: Option<SigAction>,
prev_cont: Option<SigAction>,
active: bool,
}
impl RawMode {
pub fn enable(fd: RawFd) -> Result<Self> {
let borrowed = borrow(fd);
let original = termios::tcgetattr(borrowed)
.map_err(|e| RustlineError::Terminal(format!("cannot read attributes: {e}")))?;
let mut guard = RawMode {
fd,
original,
prev_winch: None,
prev_cont: None,
active: false,
};
guard.apply()?;
let action = SigAction::new(
SigHandler::Handler(on_sigwinch),
SaFlags::empty(),
SigSet::empty(),
);
guard.prev_winch = Some(unsafe { signal::sigaction(Signal::SIGWINCH, &action)? });
let action = SigAction::new(
SigHandler::Handler(on_sigcont),
SaFlags::empty(),
SigSet::empty(),
);
guard.prev_cont = Some(unsafe { signal::sigaction(Signal::SIGCONT, &action)? });
GOT_SIGWINCH.store(false, Ordering::SeqCst);
GOT_SIGCONT.store(false, Ordering::SeqCst);
Ok(guard)
}
fn apply(&mut self) -> Result<()> {
let mut raw = self.original.clone();
raw.input_flags.remove(
termios::InputFlags::BRKINT
| termios::InputFlags::ICRNL
| termios::InputFlags::INPCK
| termios::InputFlags::ISTRIP
| termios::InputFlags::IXON,
);
raw.input_flags.insert(termios::InputFlags::IUTF8);
raw.local_flags.remove(
termios::LocalFlags::ECHO
| termios::LocalFlags::ICANON
| termios::LocalFlags::IEXTEN
| termios::LocalFlags::ISIG,
);
raw.control_flags |= termios::ControlFlags::CS8;
raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
termios::tcsetattr(borrow(self.fd), termios::SetArg::TCSANOW, &raw)
.map_err(|e| RustlineError::Terminal(format!("cannot enter raw mode: {e}")))?;
self.active = true;
Ok(())
}
pub fn disable(&mut self) -> Result<()> {
if self.active {
termios::tcsetattr(borrow(self.fd), termios::SetArg::TCSANOW, &self.original)
.map_err(|e| RustlineError::Terminal(format!("cannot leave raw mode: {e}")))?;
self.active = false;
}
Ok(())
}
pub fn reenable(&mut self) -> Result<()> {
if !self.active {
self.apply()?;
}
Ok(())
}
}
impl Drop for RawMode {
fn drop(&mut self) {
let _ = self.disable();
if let Some(prev) = self.prev_winch.take() {
let _ = unsafe { signal::sigaction(Signal::SIGWINCH, &prev) };
}
if let Some(prev) = self.prev_cont.take() {
let _ = unsafe { signal::sigaction(Signal::SIGCONT, &prev) };
}
}
}
extern "C" fn on_sigwinch(_: i32) {
GOT_SIGWINCH.store(true, Ordering::SeqCst);
}
extern "C" fn on_sigcont(_: i32) {
GOT_SIGCONT.store(true, Ordering::SeqCst);
}
pub fn take_sigwinch() -> bool {
GOT_SIGWINCH.swap(false, Ordering::SeqCst)
}
pub fn take_sigcont() -> bool {
GOT_SIGCONT.swap(false, Ordering::SeqCst)
}
fn borrow(fd: RawFd) -> BorrowedFd<'static> {
unsafe { BorrowedFd::borrow_raw(fd) }
}
#[must_use]
fn is_unsupported_term_name(term: &str) -> bool {
UNSUPPORTED_TERMS
.iter()
.any(|t| t.eq_ignore_ascii_case(term))
}
#[must_use]
pub fn is_unsupported_term() -> bool {
std::env::var("TERM").is_ok_and(|term| is_unsupported_term_name(&term))
}
#[must_use]
pub fn is_tty(fd: RawFd) -> bool {
unistd::isatty(borrow(fd)).unwrap_or(false)
}
#[must_use]
pub fn terminal_size(ifd: RawFd, ofd: RawFd) -> WinSize {
let mut size = ioctl_size(ofd).unwrap_or(WinSize { rows: 0, cols: 0 });
if size.rows == 0 {
if let Some(rows) = env_dimension("ROWS") {
size.rows = rows;
}
}
if size.cols == 0 {
if let Some(cols) = env_dimension("COLUMNS") {
size.cols = cols;
}
}
if (size.rows == 0 || size.cols == 0) && is_tty(ifd) {
if let Some(reported) = query_size(ifd, ofd) {
if size.rows == 0 {
size.rows = reported.rows;
}
if size.cols == 0 {
size.cols = reported.cols;
}
}
}
size.clamped()
}
fn ioctl_size(fd: RawFd) -> Option<WinSize> {
use nix::libc::{TIOCGWINSZ, ioctl, winsize};
let mut ws = winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let rc = unsafe { ioctl(fd, TIOCGWINSZ, &raw mut ws) };
if rc == 0 {
Some(WinSize {
rows: ws.ws_row,
cols: ws.ws_col,
})
} else {
None
}
}
fn env_dimension(name: &str) -> Option<u16> {
std::env::var(name)
.ok()?
.trim()
.parse()
.ok()
.filter(|n| *n > 0)
}
fn query_size(ifd: RawFd, ofd: RawFd) -> Option<WinSize> {
const QUERY: &[u8] = b"\x1b7\x1b[9979;9979H\x1b[6n\x1b8";
write_all(ofd, QUERY).ok()?;
let mut buf = [0u8; 32];
let mut len = 0;
while len < buf.len() {
match wait_for_input(ifd, Some(100)) {
Ok(Wait::Ready) => {}
Ok(Wait::Interrupted) => continue,
_ => break,
}
match read_input(ifd, &mut buf[len..]) {
Ok(Some(0)) | Err(_) => break,
Ok(Some(n)) => len += n,
Ok(None) => continue,
}
if buf[..len].contains(&b'R') {
break;
}
}
let reply = std::str::from_utf8(&buf[..len]).ok()?;
let body = reply.strip_prefix("\x1b[")?;
let body = body.split('R').next()?;
let mut parts = body.split(';');
let rows = parts.next()?.trim().parse().ok()?;
let cols = parts.next()?.trim().parse().ok()?;
Some(WinSize { rows, cols })
}
pub fn wait_for_input(fd: RawFd, timeout_ms: Option<u16>) -> Result<Wait> {
let borrowed = borrow(fd);
let mut fds = [PollFd::new(borrowed, PollFlags::POLLIN)];
let timeout = timeout_ms.map_or(PollTimeout::NONE, PollTimeout::from);
match poll(&mut fds, timeout) {
Ok(0) => Ok(Wait::Timeout),
Ok(_) => {
let revents = fds[0].revents().unwrap_or_else(PollFlags::empty);
if revents.intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR) {
Ok(Wait::Ready)
} else {
Ok(Wait::Timeout)
}
}
Err(nix::errno::Errno::EINTR) => Ok(Wait::Interrupted),
Err(e) => Err(e.into()),
}
}
pub fn read_input(fd: RawFd, buf: &mut [u8]) -> Result<Option<usize>> {
match unistd::read(borrow(fd), buf) {
Ok(n) => Ok(Some(n)),
Err(nix::errno::Errno::EINTR) => Ok(None),
Err(nix::errno::Errno::EAGAIN) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn write_all(fd: RawFd, buf: &[u8]) -> Result<()> {
let mut written = 0;
while written < buf.len() {
match unistd::write(borrow(fd), &buf[written..]) {
Ok(0) => {
return Err(std::io::Error::from(std::io::ErrorKind::WriteZero).into());
}
Ok(n) => written += n,
Err(nix::errno::Errno::EINTR) => {}
Err(nix::errno::Errno::EAGAIN) => {
let borrowed = borrow(fd);
let mut fds = [PollFd::new(borrowed, PollFlags::POLLOUT)];
let _ = poll(&mut fds, PollTimeout::from(100u16));
}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
pub fn clear_screen(fd: RawFd) -> Result<()> {
write_all(fd, b"\x1b[H\x1b[2J")
}
pub fn pause_output(fd: RawFd) -> Result<()> {
termios::tcflow(borrow(fd), termios::FlowArg::TCOOFF)?;
Ok(())
}
pub fn resume_output(fd: RawFd) -> Result<()> {
termios::tcflow(borrow(fd), termios::FlowArg::TCOON)?;
Ok(())
}
pub fn suspend() -> Result<()> {
signal::raise(Signal::SIGTSTP)?;
Ok(())
}
pub fn raise_default(sig: Signal) -> Result<()> {
unsafe {
signal::signal(sig, SigHandler::SigDfl)?;
}
signal::raise(sig)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn winsize_never_reports_zero() {
let clamped = WinSize { rows: 0, cols: 0 }.clamped();
assert_eq!(clamped.rows, DEFAULT_ROWS);
assert_eq!(clamped.cols, DEFAULT_COLS);
assert!(clamped.cols() > 0);
assert!(clamped.rows() > 0);
let partial = WinSize { rows: 0, cols: 100 }.clamped();
assert_eq!(partial.rows, DEFAULT_ROWS);
assert_eq!(partial.cols, 100);
}
#[test]
fn default_winsize_is_usable() {
let d = WinSize::default();
assert_eq!((d.rows, d.cols), (DEFAULT_ROWS, DEFAULT_COLS));
}
#[test]
fn terminal_size_on_a_pipe_falls_back_without_hanging() {
let (r, w) = nix::unistd::pipe().unwrap();
use std::os::fd::AsRawFd;
let size = terminal_size(r.as_raw_fd(), w.as_raw_fd());
assert!(size.cols > 0 && size.rows > 0);
}
#[test]
fn unsupported_terminals_are_recognized() {
assert!(is_unsupported_term_name("dumb"));
assert!(is_unsupported_term_name("DUMB"));
assert!(is_unsupported_term_name("cons25"));
assert!(is_unsupported_term_name("emacs"));
assert!(!is_unsupported_term_name("xterm-256color"));
assert!(!is_unsupported_term_name("dumb-but-not-really"));
assert!(!is_unsupported_term_name(""));
let _ = is_unsupported_term();
}
#[test]
fn raw_mode_on_a_pipe_is_a_terminal_error() {
use std::os::fd::AsRawFd;
let (r, _w) = nix::unistd::pipe().unwrap();
let Err(err) = RawMode::enable(r.as_raw_fd()) else {
panic!("a pipe was accepted as a terminal");
};
assert!(
matches!(err, RustlineError::Terminal(_)),
"expected a terminal error, got {err:?}",
);
assert!(err.to_string().starts_with("terminal error:"));
}
#[test]
fn write_all_handles_a_pipe() {
use std::io::Read;
use std::os::fd::AsRawFd;
let (r, w) = nix::unistd::pipe().unwrap();
write_all(w.as_raw_fd(), b"hello").unwrap();
drop(w);
let mut out = String::new();
std::fs::File::from(r).read_to_string(&mut out).unwrap();
assert_eq!(out, "hello");
}
#[test]
fn signal_flags_latch_and_clear() {
GOT_SIGWINCH.store(true, Ordering::SeqCst);
assert!(take_sigwinch());
assert!(!take_sigwinch());
GOT_SIGCONT.store(true, Ordering::SeqCst);
assert!(take_sigcont());
assert!(!take_sigcont());
}
#[test]
fn wait_reports_ready_and_timeout() {
use std::os::fd::AsRawFd;
let (r, w) = nix::unistd::pipe().unwrap();
assert_eq!(
wait_for_input(r.as_raw_fd(), Some(1)).unwrap(),
Wait::Timeout
);
write_all(w.as_raw_fd(), b"x").unwrap();
assert_eq!(
wait_for_input(r.as_raw_fd(), Some(50)).unwrap(),
Wait::Ready
);
let mut buf = [0u8; 4];
assert_eq!(read_input(r.as_raw_fd(), &mut buf).unwrap(), Some(1));
assert_eq!(&buf[..1], b"x");
}
}