use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use crossterm::ExecutableCommand;
use crossterm::cursor::Hide;
use crossterm::event::{EnableBracketedPaste, EnableMouseCapture};
use crossterm::terminal::{self, Clear, ClearType, EnterAlternateScreen};
pub(crate) static TTY_FD_PATH: OnceLock<bool> = OnceLock::new();
static LOG_PATH: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
pub fn set_log_path(path: Option<std::path::PathBuf>) {
let _ = LOG_PATH.set(path);
}
pub(crate) static EVENT_READER_SHUTDOWN: AtomicBool = AtomicBool::new(false);
pub(crate) static EVENT_READER_EXITED: AtomicBool = AtomicBool::new(false);
pub(crate) static READER_HANDLE: Mutex<Option<std::thread::JoinHandle<()>>> = Mutex::new(None);
pub struct TerminalGuard {
#[cfg(unix)]
saved_stdout_fd: Option<libc::c_int>,
#[cfg(unix)]
saved_stderr_fd: Option<libc::c_int>,
}
impl TerminalGuard {
pub fn new() -> std::io::Result<Self> {
EVENT_READER_SHUTDOWN.store(false, Ordering::Relaxed);
EVENT_READER_EXITED.store(false, Ordering::Relaxed);
let mut tty_writer: Box<dyn std::io::Write> = match open_tty_for_write() {
Some(f) => Box::new(f),
None => Box::new(std::io::stdout()),
};
tty_writer.execute(EnterAlternateScreen)?;
tty_writer.execute(Clear(ClearType::All))?;
tty_writer.execute(EnableBracketedPaste)?;
tty_writer.execute(EnableMouseCapture)?;
tty_writer.execute(Hide)?;
terminal::enable_raw_mode()?;
let _ = tty_writer.flush();
drop(tty_writer);
#[cfg(unix)]
let (saved_stdout_fd, saved_stderr_fd) = redirect_stdout_stderr_to_log();
#[cfg(not(unix))]
let _ = ();
let _ = TTY_FD_PATH.set(true);
#[cfg(unix)]
return Ok(TerminalGuard {
saved_stdout_fd,
saved_stderr_fd,
});
#[cfg(not(unix))]
return Ok(TerminalGuard {});
}
}
pub(crate) fn open_tty_for_write() -> Option<std::fs::File> {
std::fs::OpenOptions::new()
.read(false)
.write(true)
.open("/dev/tty")
.ok()
}
pub(crate) fn tty_size() -> (u16, u16) {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
let f = match std::fs::OpenOptions::new()
.read(true)
.write(false)
.open("/dev/tty")
{
Ok(f) => f,
Err(_) => return (80, 24),
};
let fd = f.as_raw_fd();
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) };
if rc < 0 || ws.ws_col == 0 || ws.ws_row == 0 {
return (80, 24);
}
(ws.ws_col, ws.ws_row)
}
#[cfg(not(unix))]
{
crossterm::terminal::size().unwrap_or((80, 24))
}
}
#[cfg(unix)]
fn redirect_stdout_stderr_to_log() -> (Option<libc::c_int>, Option<libc::c_int>) {
let configured = LOG_PATH
.get()
.and_then(|opt| opt.clone())
.unwrap_or_else(|| std::path::PathBuf::from("/dev/null"));
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&configured)
.or_else(|_| std::fs::OpenOptions::new().write(true).open("/dev/null"));
let file = match file {
Ok(f) => f,
Err(_) => return (None, None),
};
use std::os::fd::AsRawFd;
let target_fd = file.as_raw_fd();
let saved_stdout_fd = unsafe { libc::dup(1) };
let saved_stderr_fd = unsafe { libc::dup(2) };
unsafe {
libc::dup2(target_fd, 1);
libc::dup2(target_fd, 2);
}
drop(file);
(
if saved_stdout_fd >= 0 {
Some(saved_stdout_fd)
} else {
None
},
if saved_stderr_fd >= 0 {
Some(saved_stderr_fd)
} else {
None
},
)
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
EVENT_READER_SHUTDOWN.store(true, Ordering::Relaxed);
wait_for_reader_exit(Duration::from_millis(50));
let mut tty_writer: Box<dyn std::io::Write> = match open_tty_for_write() {
Some(f) => Box::new(f),
None => Box::new(std::io::stdout()),
};
let stdout = &mut tty_writer;
let _ = stdout.write_all(
b"\x1b[0m\
\x1b[?25h\
\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l\x1b[?1006l\x1b[?1015l\
\x1b[?2004l\
\x1b]0;\x1b\\\
\x1b[?1049l",
);
let _ = stdout.flush();
#[cfg(unix)]
sync_and_drain_via_sentinel(stdout, Duration::from_millis(100));
let _ = terminal::disable_raw_mode();
let _ = stdout.write_all(b"\x1b[?25h");
let _ = stdout.flush();
drop(tty_writer);
#[cfg(unix)]
unsafe {
if let Some(orig) = self.saved_stdout_fd {
libc::dup2(orig, 1);
libc::close(orig);
}
if let Some(orig) = self.saved_stderr_fd {
libc::dup2(orig, 2);
libc::close(orig);
}
}
}
}
pub(crate) fn wait_for_reader_exit(budget: Duration) {
let deadline = std::time::Instant::now() + budget;
while !EVENT_READER_EXITED.load(Ordering::Acquire) {
if std::time::Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(2));
}
}
#[cfg(unix)]
pub(crate) fn join_reader(budget: Duration) {
let handle = match READER_HANDLE.lock() {
Ok(mut guard) => guard.take(),
Err(_) => return,
};
let Some(handle) = handle else {
return;
};
let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let done2 = std::sync::Arc::clone(&done);
std::thread::spawn(move || {
std::thread::sleep(budget);
done2.store(true, std::sync::atomic::Ordering::Relaxed);
});
while !done.load(std::sync::atomic::Ordering::Relaxed) {
if handle.is_finished() {
let _ = handle.join();
return;
}
std::thread::sleep(Duration::from_millis(2));
}
if let Ok(mut guard) = READER_HANDLE.lock() {
*guard = Some(handle);
}
}
#[cfg(unix)]
pub(crate) fn drain_stdin_nonblocking() -> Vec<u8> {
let fd_in: libc::c_int = 0;
let original_flags = unsafe { libc::fcntl(fd_in, libc::F_GETFL) };
if original_flags < 0 {
return Vec::new();
}
let nb_flags = original_flags | libc::O_NONBLOCK;
if unsafe { libc::fcntl(fd_in, libc::F_SETFL, nb_flags) } < 0 {
return Vec::new();
}
let mut drained = Vec::with_capacity(256);
let mut buf = [0u8; 1024];
loop {
let n = unsafe { libc::read(fd_in, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n > 0 {
drained.extend_from_slice(&buf[..n as usize]);
continue;
}
if n == 0 {
break;
}
let err = std::io::Error::last_os_error().raw_os_error();
match err {
Some(e) if e == libc::EAGAIN || e == libc::EWOULDBLOCK => break,
Some(libc::EINTR) => continue,
_ => break,
}
}
let _ = unsafe { libc::fcntl(fd_in, libc::F_SETFL, original_flags) };
drained
}
#[cfg(unix)]
pub(crate) fn sync_and_drain_via_sentinel(stdout: &mut dyn std::io::Write, budget: Duration) {
let fd_in: libc::c_int = 0;
let original_flags = unsafe { libc::fcntl(fd_in, libc::F_GETFL) };
if original_flags < 0 {
return;
}
let nb_flags = original_flags | libc::O_NONBLOCK;
if unsafe { libc::fcntl(fd_in, libc::F_SETFL, nb_flags) } < 0 {
return;
}
if stdout.write_all(b"\x1b[5n").is_err() {
let _ = unsafe { libc::fcntl(fd_in, libc::F_SETFL, original_flags) };
return;
}
let _ = stdout.flush();
let deadline = std::time::Instant::now() + budget;
let mut buf = [0u8; 1024];
let mut match_state: u8 = 0;
let mut got_reply = false;
while !got_reply && std::time::Instant::now() < deadline {
let n = unsafe { libc::read(fd_in, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n > 0 {
for &b in &buf[..n as usize] {
match (match_state, b) {
(0, 0x1b) => match_state = 1,
(1, b'[') => match_state = 2,
(2, b'0') => match_state = 3,
(3, b'n') => {
got_reply = true;
break;
}
(_, 0x1b) => match_state = 1,
_ => match_state = 0,
}
}
continue;
}
if n == 0 {
break;
}
let err = std::io::Error::last_os_error().raw_os_error();
match err {
Some(e) if e == libc::EAGAIN || e == libc::EWOULDBLOCK => {
std::thread::sleep(Duration::from_millis(4));
}
Some(libc::EINTR) => continue,
_ => break,
}
}
let _ = unsafe { libc::fcntl(fd_in, libc::F_SETFL, original_flags) };
}