use scope::error::{Result, ScopeError};
use std::io::{self, Write};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
struct Lifecycle {
held: AtomicBool,
hook: OnceLock<()>,
}
static SCREEN: Lifecycle = Lifecycle::new();
impl Lifecycle {
const fn new() -> Self {
Self {
held: AtomicBool::new(false),
hook: OnceLock::new(),
}
}
fn take(
&self,
w: &mut impl Write,
raw_mode: impl FnOnce() -> io::Result<()>,
install_hook: impl FnOnce(),
) -> io::Result<()> {
self.hook.get_or_init(install_hook);
raw_mode()?;
self.held.store(true, Ordering::SeqCst);
crossterm::execute!(w, crossterm::terminal::EnterAlternateScreen)?;
paste_mode(w, true);
crossterm::execute!(w, crossterm::event::EnableMouseCapture)?;
Ok(())
}
fn give_back(&self, w: &mut impl Write, leave: impl FnOnce()) {
if self.held.swap(false, Ordering::SeqCst) {
let _ = crossterm::execute!(w, crossterm::event::DisableMouseCapture);
paste_mode(w, false);
leave();
}
}
}
pub(crate) fn enter() -> Result<ratatui::DefaultTerminal> {
take_screen(
&SCREEN,
&mut io::stdout(),
crossterm::terminal::enable_raw_mode,
)
.and_then(|()| {
ratatui::Terminal::new(ratatui::backend::CrosstermBackend::new(io::stdout()))
.map_err(not_taken)
})
.inspect_err(|_| restore())
}
pub(crate) fn not_taken(e: io::Error) -> ScopeError {
ScopeError::io_context(e, "cannot take the terminal")
}
fn take_screen(
lifecycle: &Lifecycle,
w: &mut impl Write,
raw_mode: impl FnOnce() -> io::Result<()>,
) -> Result<()> {
lifecycle
.take(w, raw_mode, install_restore_hook)
.map_err(not_taken)
}
pub(crate) fn restore() {
SCREEN.give_back(&mut io::stdout(), ratatui::restore);
}
fn paste_mode(w: &mut impl Write, on: bool) {
let _ = if on {
crossterm::execute!(w, crossterm::event::EnableBracketedPaste)
} else {
crossterm::execute!(w, crossterm::event::DisableBracketedPaste)
};
}
fn install_restore_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore();
previous(info);
}));
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
const ALT_SCREEN_AND_PASTE: &[u8] = b"\x1b[?1049h\x1b[?2004h";
const MOUSE_ON: &[u8] = b"\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1015h\x1b[?1006h";
#[test]
fn test_paste_mode_writes_the_enable_and_disable_sequences() {
let mut on = Vec::new();
paste_mode(&mut on, true);
assert_eq!(on, b"\x1b[?2004h");
let mut off = Vec::new();
paste_mode(&mut off, false);
assert_eq!(off, b"\x1b[?2004l");
}
#[test]
fn test_tui_resumes_and_monitor_runs_install_one_restore_hook() {
let screen = Lifecycle::new();
let installs = Cell::new(0);
for _ in 0..4 {
let mut out = Vec::new();
screen
.take(&mut out, || Ok(()), || installs.set(installs.get() + 1))
.unwrap();
assert!(out.starts_with(ALT_SCREEN_AND_PASTE), "{out:?}");
assert_eq!(out.ends_with(MOUSE_ON), !cfg!(windows), "{out:?}");
screen.give_back(&mut Vec::new(), || {});
}
assert_eq!(installs.get(), 1);
}
#[test]
fn test_give_back_acts_once_per_take() {
let screen = Lifecycle::new();
let leaves = Cell::new(0);
screen.take(&mut Vec::new(), || Ok(()), || {}).unwrap();
let mut first = Vec::new();
screen.give_back(&mut first, || leaves.set(leaves.get() + 1));
assert!(
first.ends_with(b"\x1b[?2004l"),
"mouse and paste off: {first:?}"
);
let mut second = Vec::new();
screen.give_back(&mut second, || leaves.set(leaves.get() + 1));
assert!(second.is_empty(), "a second restore moves the cursor");
assert_eq!(leaves.get(), 1);
}
#[test]
fn test_a_failed_take_is_the_one_terminal_error() {
let screen = Lifecycle::new();
let e =
take_screen(&screen, &mut Vec::new(), || Err(io::Error::other("no tty"))).unwrap_err();
assert!(matches!(&e, ScopeError::IoError(io) if io.kind() == io::ErrorKind::Other));
assert_eq!(e.to_string(), "I/O error: cannot take the terminal: no tty");
}
#[test]
fn test_raw_mode_failure_takes_nothing() {
let screen = Lifecycle::new();
let mut out = Vec::new();
let e = screen
.take(&mut out, || Err(io::Error::other("no tty")), || {})
.unwrap_err();
assert_eq!(e.to_string(), "no tty");
assert!(out.is_empty());
assert!(!screen.held.load(Ordering::SeqCst));
}
struct Refusing;
impl Write for Refusing {
fn write(&mut self, _: &[u8]) -> io::Result<usize> {
Err(io::Error::other("refused"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_a_failed_take_is_an_error_and_can_be_given_back() {
let screen = Lifecycle::new();
let e = screen.take(&mut Refusing, || Ok(()), || {}).unwrap_err();
assert_eq!(e.to_string(), "refused");
let left = Cell::new(false);
screen.give_back(&mut Vec::new(), || left.set(true));
assert!(left.get(), "raw mode was on, so it is turned off");
}
}