use crossterm::event::{Event, poll, read};
use std::io;
use std::time::Duration;
#[cfg(unix)]
use std::fs::File;
#[cfg(unix)]
use std::mem::MaybeUninit;
#[cfg(unix)]
use std::os::unix::io::{AsRawFd, IntoRawFd};
#[cfg(unix)]
use std::sync::{Mutex, 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)]
struct StdinRedirect {
saved_stdin: libc::c_int,
}
#[cfg(unix)]
static STDIN_REDIRECT: OnceLock<Mutex<Option<StdinRedirect>>> = OnceLock::new();
#[cfg(unix)]
fn redirect_state() -> &'static Mutex<Option<StdinRedirect>> {
STDIN_REDIRECT.get_or_init(|| Mutex::new(None))
}
#[cfg(unix)]
fn redirect_stdin_to_tty_once() -> io::Result<()> {
let state = redirect_state();
let mut guard = state.lock().expect("STDIN_REDIRECT mutex poisoned");
if guard.is_some() {
return Ok(());
}
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.into_raw_fd();
if tty_fd < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid file descriptor for /dev/tty",
));
}
unsafe {
let saved_stdin = libc::dup(0);
if saved_stdin < 0 {
libc::close(tty_fd);
return Err(io::Error::last_os_error());
}
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);
*guard = Some(StdinRedirect { saved_stdin });
}
Ok(())
}
#[cfg(unix)]
fn restore_stdin_redirect() {
let state = redirect_state();
let Ok(mut guard) = state.lock() else { return };
if let Some(redirect) = guard.take() {
unsafe {
libc::dup2(redirect.saved_stdin, 0);
libc::close(redirect.saved_stdin);
}
}
}
#[cfg(unix)]
pub fn enable_raw_mode() -> io::Result<()> {
if stdin_is_tty() {
return crossterm::terminal::enable_raw_mode();
}
redirect_stdin_to_tty_once()?;
let mut orig_termios = MaybeUninit::<libc::termios>::uninit();
unsafe {
if libc::tcgetattr(0, orig_termios.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
let orig = orig_termios.assume_init();
let _ = SAVED_TERMIOS.set(orig);
let mut termios = orig;
libc::cfmakeraw(&mut termios);
if libc::tcsetattr(0, 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() {
return crossterm::terminal::disable_raw_mode();
}
unsafe {
if let Some(orig) = SAVED_TERMIOS.get() {
libc::tcsetattr(0, libc::TCSANOW, orig);
} else {
let mut termios = MaybeUninit::<libc::termios>::uninit();
if libc::tcgetattr(0, 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(0, libc::TCSANOW, &termios);
}
}
}
restore_stdin_redirect();
Ok(())
}
#[cfg(not(unix))]
pub fn disable_raw_mode() -> io::Result<()> {
crossterm::terminal::disable_raw_mode()
}
pub fn restore() {
use crossterm::ExecutableCommand;
use crossterm::event::DisableMouseCapture;
use crossterm::terminal::LeaveAlternateScreen;
use std::io::stdout;
let _ = stdout().execute(DisableMouseCapture);
let _ = stdout().execute(LeaveAlternateScreen);
let _ = disable_raw_mode();
}
pub fn install_panic_hook() {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore();
prev(info);
}));
}
pub fn read_event() -> io::Result<Event> {
read()
}
pub fn poll_event(timeout: Duration) -> io::Result<bool> {
poll(timeout)
}