use std::io::{self, Stdout};
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use crossterm::event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::prelude::{CrosstermBackend, Terminal};
const RESTORE: &[u8] =
b"\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?1049l\x1b[?25h";
pub(crate) struct TerminalGuard {
pub(crate) terminal: Terminal<CrosstermBackend<Stdout>>,
mouse: bool,
}
impl TerminalGuard {
pub(crate) fn enter(mouse: bool) -> io::Result<Self> {
capture();
enable_raw_mode()?;
let mut stdout = io::stdout();
if let Err(e) = execute!(stdout, EnterAlternateScreen) {
let _ = disable_raw_mode();
return Err(e);
}
if mouse {
let _ = execute!(stdout, EnableMouseCapture);
}
let _ = execute!(stdout, EnableBracketedPaste);
match Terminal::new(CrosstermBackend::new(stdout)) {
Ok(terminal) => {
arm();
Ok(TerminalGuard { terminal, mouse })
}
Err(e) => {
let _ = execute!(
io::stdout(),
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
);
let _ = disable_raw_mode();
Err(e)
}
}
}
pub(crate) fn suspend(&mut self) {
self.release();
disarm();
}
pub(crate) fn resume(&mut self) {
let _ = enable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen);
if self.mouse {
let _ = execute!(self.terminal.backend_mut(), EnableMouseCapture);
}
let _ = execute!(self.terminal.backend_mut(), EnableBracketedPaste);
let _ = self.terminal.clear();
arm();
}
fn release(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), DisableBracketedPaste);
if self.mouse {
let _ = execute!(self.terminal.backend_mut(), DisableMouseCapture);
}
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
self.release();
let _ = self.terminal.show_cursor();
disarm();
}
}
const TRAPPED: [libc::c_int; 3] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP];
static ARMED: AtomicBool = AtomicBool::new(false);
static ORIGINAL: AtomicPtr<libc::termios> = AtomicPtr::new(std::ptr::null_mut());
pub(crate) fn capture() {
if !ORIGINAL.load(Ordering::SeqCst).is_null() {
return;
}
let mut modes: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut modes) } == 0 {
ORIGINAL.store(Box::into_raw(Box::new(modes)), Ordering::SeqCst);
}
}
pub(crate) fn arm() {
ARMED.store(true, Ordering::SeqCst);
}
pub(crate) fn disarm() {
ARMED.store(false, Ordering::SeqCst);
}
pub(crate) fn restore() -> bool {
if !ARMED.swap(false, Ordering::SeqCst) {
return false;
}
reset();
true
}
fn reset() {
let original = ORIGINAL.load(Ordering::SeqCst);
if !original.is_null() {
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, original) };
}
unsafe { libc::write(libc::STDOUT_FILENO, RESTORE.as_ptr().cast(), RESTORE.len()) };
}
fn exit_code(signal: libc::c_int) -> libc::c_int {
128 + signal
}
extern "C" fn on_signal(signal: libc::c_int) {
if ARMED.swap(false, Ordering::SeqCst) {
reset();
}
unsafe { libc::_exit(exit_code(signal)) };
}
pub(crate) fn trap_signals() {
for signal in TRAPPED {
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = on_signal as *const () as libc::sighandler_t;
libc::sigemptyset(&mut action.sa_mask);
action.sa_flags = libc::SA_RESTART;
libc::sigaction(signal, &action, std::ptr::null_mut());
}
}
}
pub(crate) fn restore_on_panic() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore();
previous(info);
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_first_teardown_restores() {
disarm();
assert!(!restore(), "a disarmed terminal needs no restoring");
arm();
assert!(restore(), "the first teardown after arming does the work");
assert!(!restore(), "the second finds nothing left to do");
arm();
disarm();
assert!(
!restore(),
"a guard that restored itself leaves nothing behind"
);
}
#[test]
fn restore_bytes_disable_mouse_and_bracketed_paste() {
let s = std::str::from_utf8(RESTORE).unwrap();
assert!(s.contains("\x1b[?1049l"), "leave alternate screen");
assert!(s.contains("\x1b[?25h"), "show cursor");
assert!(s.contains("\x1b[?1006l"), "disable SGR mouse encoding");
assert!(
s.contains("\x1b[?1003l"),
"disable any-motion mouse tracking"
);
assert!(
s.contains("\x1b[?1000l"),
"disable button-event mouse tracking"
);
assert!(s.contains("\x1b[?2004l"), "disable bracketed paste");
}
#[test]
fn one_guard_serves_both_tuis() {
let src = concat!(include_str!("tui.rs"), include_str!("watch.rs"));
assert!(
!src.contains(concat!("struct Terminal", "Guard")),
"the guard lives in term.rs"
);
}
#[test]
fn signals_report_the_shell_convention() {
assert_eq!(exit_code(libc::SIGINT), 130);
assert_eq!(exit_code(libc::SIGTERM), 143);
assert_eq!(exit_code(libc::SIGHUP), 129);
}
#[test]
fn a_trapped_signal_exits_instead_of_killing_us() {
for signal in TRAPPED {
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
trap_signals();
arm();
unsafe { libc::raise(signal) };
unsafe { libc::_exit(1) };
}
let mut status = 0;
assert!(unsafe { libc::waitpid(child, &mut status, 0) } > 0);
assert!(
libc::WIFEXITED(status),
"signal {signal} killed the process instead of being handled"
);
assert_eq!(libc::WEXITSTATUS(status), exit_code(signal));
}
}
}