use crossterm::event::{Event, poll, read};
use std::fs::File;
use std::io;
use std::time::Duration;
#[cfg(unix)]
use std::mem::MaybeUninit;
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
#[cfg(unix)]
use std::sync::OnceLock;
#[cfg(unix)]
fn stdin_is_tty() -> bool {
let stdin_fd = io::stdin().as_raw_fd();
unsafe { libc::isatty(stdin_fd) == 1 }
}
#[cfg(not(unix))]
fn stdin_is_tty() -> bool {
use std::io::IsTerminal;
io::stdin().is_terminal()
}
#[cfg(unix)]
static SAVED_TERMIOS: OnceLock<libc::termios> = OnceLock::new();
#[cfg(unix)]
pub fn enable_raw_mode() -> io::Result<()> {
if stdin_is_tty() {
crossterm::terminal::enable_raw_mode()
} else {
let tty = File::options()
.read(true)
.write(true)
.open("/dev/tty")
.map_err(|e| {
io::Error::new(
io::ErrorKind::NotFound,
format!(
"Cannot open /dev/tty: {}. Interactive mode requires a terminal.",
e
),
)
})?;
let tty_fd = tty.as_raw_fd();
if tty_fd < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid file descriptor for /dev/tty",
));
}
let mut orig_termios = MaybeUninit::<libc::termios>::uninit();
unsafe {
if libc::tcgetattr(tty_fd, orig_termios.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
let orig_termios = orig_termios.assume_init();
let _ = SAVED_TERMIOS.set(orig_termios);
let mut termios = orig_termios;
libc::cfmakeraw(&mut termios);
if libc::tcsetattr(tty_fd, libc::TCSANOW, &termios) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
}
#[cfg(not(unix))]
pub fn enable_raw_mode() -> io::Result<()> {
crossterm::terminal::enable_raw_mode()
}
#[cfg(unix)]
pub fn disable_raw_mode() -> io::Result<()> {
if stdin_is_tty() {
crossterm::terminal::disable_raw_mode()
} else {
let tty = File::options().read(true).write(true).open("/dev/tty").ok();
if let Some(tty) = tty {
let tty_fd = tty.as_raw_fd();
if tty_fd < 0 {
return Ok(()); }
unsafe {
if let Some(orig) = SAVED_TERMIOS.get() {
libc::tcsetattr(tty_fd, libc::TCSANOW, orig);
} else {
let mut termios = MaybeUninit::<libc::termios>::uninit();
if libc::tcgetattr(tty_fd, termios.as_mut_ptr()) == 0 {
let mut termios = termios.assume_init();
termios.c_lflag |= libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN;
termios.c_iflag |= libc::ICRNL | libc::IXON;
termios.c_oflag |= libc::OPOST;
libc::tcsetattr(tty_fd, libc::TCSANOW, &termios);
}
}
}
}
Ok(())
}
}
#[cfg(not(unix))]
pub fn disable_raw_mode() -> io::Result<()> {
crossterm::terminal::disable_raw_mode()
}
#[cfg(unix)]
fn with_tty_stdin<F, R>(f: F) -> io::Result<R>
where
F: FnOnce() -> io::Result<R>,
{
use std::os::unix::io::IntoRawFd;
let stdin_fd = io::stdin().as_raw_fd();
if unsafe { libc::isatty(stdin_fd) } == 1 {
return f();
}
unsafe {
let saved_stdin = libc::dup(0);
if saved_stdin < 0 {
return Err(io::Error::last_os_error());
}
let tty = match File::options().read(true).write(true).open("/dev/tty") {
Ok(f) => f,
Err(e) => {
libc::close(saved_stdin);
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Cannot open /dev/tty: {}. Interactive mode requires a terminal.",
e
),
));
}
};
let tty_fd = tty.into_raw_fd();
if tty_fd < 0 {
libc::close(saved_stdin);
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid file descriptor for /dev/tty",
));
}
if libc::dup2(tty_fd, 0) < 0 {
let err = io::Error::last_os_error();
libc::close(tty_fd);
libc::close(saved_stdin);
return Err(err);
}
libc::close(tty_fd);
let result = f();
libc::dup2(saved_stdin, 0);
libc::close(saved_stdin);
result
}
}
#[cfg(unix)]
pub fn read_event() -> io::Result<Event> {
with_tty_stdin(read)
}
#[cfg(not(unix))]
pub fn read_event() -> io::Result<Event> {
read()
}
#[cfg(unix)]
pub fn poll_event(timeout: Duration) -> io::Result<bool> {
with_tty_stdin(|| poll(timeout))
}
#[cfg(not(unix))]
pub fn poll_event(timeout: Duration) -> io::Result<bool> {
poll(timeout)
}