use std::io::{self, Stdout, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use crossterm::cursor::{SetCursorStyle, Show};
use crossterm::event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
supports_keyboard_enhancement,
};
use ratatui::Terminal;
use ratatui::prelude::CrosstermBackend;
static SESSION_LIVE: AtomicBool = AtomicBool::new(false);
pub struct TerminalSession {
terminal: Terminal<CrosstermBackend<Stdout>>,
keyboard_enhanced: bool,
}
impl TerminalSession {
pub fn enter(mouse_capture: bool) -> io::Result<Self> {
enable_raw_mode()?;
SESSION_LIVE.store(true, Ordering::SeqCst);
Self::enter_after_raw_mode(mouse_capture).inspect_err(|_| leave_if_live(&mut io::stdout()))
}
fn enter_after_raw_mode(mouse_capture: bool) -> io::Result<Self> {
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableBracketedPaste)?;
let keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false)
&& execute!(
stdout,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
)
.inspect_err(|e| tracing::warn!("keyboard enhancement push failed: {e}"))
.is_ok();
if mouse_capture {
let _ = execute!(stdout, EnableMouseCapture);
}
let terminal = Terminal::new(CrosstermBackend::new(stdout))?;
Ok(Self {
terminal,
keyboard_enhanced,
})
}
pub fn terminal_mut(&mut self) -> &mut Terminal<CrosstermBackend<Stdout>> {
&mut self.terminal
}
pub fn keyboard_enhanced(&self) -> bool {
self.keyboard_enhanced
}
}
impl Drop for TerminalSession {
fn drop(&mut self) {
leave_if_live(self.terminal.backend_mut());
}
}
pub(crate) fn leave_if_live<W: Write>(w: &mut W) {
if SESSION_LIVE.swap(false, Ordering::SeqCst) {
leave(w);
}
}
pub fn leave<W: Write>(w: &mut W) {
let _ = disable_raw_mode();
let _ = execute!(w, PopKeyboardEnhancementFlags);
let _ = execute!(
w,
LeaveAlternateScreen,
DisableMouseCapture,
DisableBracketedPaste,
SetCursorStyle::DefaultUserShape,
Show,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(windows))]
#[test]
fn leave_writes_every_teardown_sequence() {
let mut out: Vec<u8> = Vec::new();
leave(&mut out);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[?1049l"),
"LeaveAlternateScreen missing: {s:?}"
);
assert!(
s.contains("\x1b[?2004l"),
"DisableBracketedPaste missing: {s:?}"
);
assert!(
s.contains("\x1b[?1000l"),
"DisableMouseCapture missing: {s:?}"
);
assert!(s.contains("\x1b[?25h"), "Show missing: {s:?}");
assert!(
s.contains("\x1b[0 q"),
"SetCursorStyle::DefaultUserShape missing: {s:?}"
);
assert!(
s.contains("\x1b[<1u"),
"PopKeyboardEnhancementFlags missing: {s:?}"
);
}
#[test]
fn leave_is_best_effort_off_a_terminal() {
let mut out: Vec<u8> = Vec::new();
leave(&mut out);
leave(&mut out); }
#[test]
fn leave_if_live_is_a_noop_when_no_session_was_entered() {
let mut out: Vec<u8> = Vec::new();
leave_if_live(&mut out);
assert!(out.is_empty(), "wrote teardown with no session: {out:?}");
}
}