#![allow(unsafe_code)]
#![allow(clippy::borrow_as_ptr)]
use std::io;
use std::os::unix::io::{AsRawFd, RawFd};
#[derive(Debug)]
pub struct RawModeGuard {
fd: RawFd,
original: libc::termios,
}
impl RawModeGuard {
pub fn new<F: AsRawFd>(fd: &F) -> io::Result<Self> {
let fd = fd.as_raw_fd();
let original = get_termios(fd)?;
let mut raw = original;
raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
raw.c_oflag &= !libc::OPOST;
raw.c_cflag |= libc::CS8;
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG);
raw.c_cc[libc::VMIN] = 0;
raw.c_cc[libc::VTIME] = 1;
set_termios(fd, &raw)?;
Ok(Self { fd, original })
}
fn restore(&self) -> io::Result<()> {
set_termios(self.fd, &self.original)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let _ = self.restore();
}
}
pub fn enable_raw_mode() -> io::Result<RawModeGuard> {
RawModeGuard::new(&io::stdin())
}
#[must_use]
pub fn is_tty<F: AsRawFd>(fd: &F) -> bool {
unsafe { libc::isatty(fd.as_raw_fd()) == 1 }
}
pub fn terminal_size() -> io::Result<(u16, u16)> {
let mut size: libc::winsize = unsafe { std::mem::zeroed() };
let result = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) };
if result == -1 {
Err(io::Error::last_os_error())
} else if size.ws_col == 0 || size.ws_row == 0 {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"terminal reported zero dimensions",
))
} else {
Ok((size.ws_col, size.ws_row))
}
}
fn get_termios(fd: RawFd) -> io::Result<libc::termios> {
let mut termios: libc::termios = unsafe { std::mem::zeroed() };
let result = unsafe { libc::tcgetattr(fd, &mut termios) };
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(termios)
}
}
fn set_termios(fd: RawFd, termios: &libc::termios) -> io::Result<()> {
let result = unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, termios) };
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::os::unix::io::FromRawFd;
#[test]
fn test_is_tty_stdin() {
let _ = is_tty(&io::stdin());
}
#[test]
fn test_is_tty_stdout() {
let _ = is_tty(&io::stdout());
}
#[test]
fn test_is_tty_stderr() {
let _ = is_tty(&io::stderr());
}
#[test]
fn test_is_tty_pipe_returns_false() {
let (read_fd, write_fd) = create_pipe().expect("Failed to create pipe");
assert!(!is_tty(&read_fd), "Read end of pipe should not be TTY");
assert!(!is_tty(&write_fd), "Write end of pipe should not be TTY");
drop(read_fd);
drop(write_fd);
}
#[test]
fn test_is_tty_file_returns_false() {
let file = tempfile::tempfile().expect("Failed to create temp file");
assert!(!is_tty(&file), "Regular file should not be TTY");
}
#[test]
fn test_terminal_size_does_not_panic() {
let _ = terminal_size();
}
#[test]
fn test_terminal_size_valid_dimensions() {
if let Ok((cols, rows)) = terminal_size() {
assert!(cols > 0, "Columns should be positive");
assert!(rows > 0, "Rows should be positive");
assert!(cols < 10000, "Columns should be reasonable");
assert!(rows < 10000, "Rows should be reasonable");
}
}
#[test]
fn test_termios_input_flags_disabled() {
let input_flags_to_disable =
libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON;
assert_ne!(input_flags_to_disable & libc::BRKINT, 0);
assert_ne!(input_flags_to_disable & libc::ICRNL, 0);
assert_ne!(input_flags_to_disable & libc::INPCK, 0);
assert_ne!(input_flags_to_disable & libc::ISTRIP, 0);
assert_ne!(input_flags_to_disable & libc::IXON, 0);
}
#[test]
fn test_termios_output_flags_disabled() {
assert_ne!(libc::OPOST, 0, "OPOST flag should be defined");
}
#[test]
fn test_termios_control_flags_enabled() {
assert_ne!(libc::CS8, 0, "CS8 flag should be defined");
}
#[test]
fn test_termios_local_flags_disabled() {
let local_flags_to_disable = libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG;
assert_ne!(local_flags_to_disable & libc::ECHO, 0);
assert_ne!(local_flags_to_disable & libc::ICANON, 0);
assert_ne!(local_flags_to_disable & libc::IEXTEN, 0);
assert_ne!(local_flags_to_disable & libc::ISIG, 0);
}
#[test]
fn test_termios_control_chars() {
const { assert!(libc::VMIN < libc::NCCS) };
const { assert!(libc::VTIME < libc::NCCS) };
}
#[test]
fn test_raw_mode_guard_debug() {
fn assert_debug<T: std::fmt::Debug>() {}
assert_debug::<RawModeGuard>();
}
#[test]
fn test_enable_raw_mode_returns_error_on_non_tty() {
let result = enable_raw_mode();
let _ = result;
}
#[test]
fn test_raw_mode_guard_new_on_pipe_fails() {
let (read_fd, _write_fd) = create_pipe().expect("Failed to create pipe");
let result = RawModeGuard::new(&read_fd);
assert!(result.is_err(), "RawModeGuard should fail on pipe");
}
#[test]
fn test_get_termios_on_pipe_fails() {
let (read_fd, _write_fd) = create_pipe().expect("Failed to create pipe");
let result = get_termios(read_fd.as_raw_fd());
assert!(result.is_err(), "get_termios should fail on pipe");
}
#[test]
fn test_set_termios_on_pipe_fails() {
let (read_fd, _write_fd) = create_pipe().expect("Failed to create pipe");
let termios: libc::termios = unsafe { std::mem::zeroed() };
let result = set_termios(read_fd.as_raw_fd(), &termios);
assert!(result.is_err(), "set_termios should fail on pipe");
}
#[test]
fn test_is_tty_with_invalid_fd() {
struct InvalidFd;
impl AsRawFd for InvalidFd {
fn as_raw_fd(&self) -> RawFd {
-1 }
}
assert!(!is_tty(&InvalidFd), "Invalid fd should not be TTY");
}
#[test]
fn test_get_termios_with_invalid_fd_fails() {
let result = get_termios(-1);
assert!(result.is_err(), "get_termios should fail on invalid fd");
}
#[test]
fn test_set_termios_with_invalid_fd_fails() {
let termios: libc::termios = unsafe { std::mem::zeroed() };
let result = set_termios(-1, &termios);
assert!(result.is_err(), "set_termios should fail on invalid fd");
}
fn create_pipe() -> io::Result<(File, File)> {
let mut fds = [0i32; 2];
let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
if result == -1 {
return Err(io::Error::last_os_error());
}
let read_file = unsafe { File::from_raw_fd(fds[0]) };
let write_file = unsafe { File::from_raw_fd(fds[1]) };
Ok((read_file, write_file))
}
}