use std::io::Write;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use anyhow::{Context, Result};
use crossterm::{
event::{DisableBracketedPaste, EnableBracketedPaste},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
use once_cell::sync::Lazy;
static TERMINAL_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static RAW_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone)]
pub struct TerminalState {
pub was_raw_mode: bool,
pub size: (u32, u32),
pub was_alternate_screen: bool,
pub was_mouse_enabled: bool,
}
impl Default for TerminalState {
fn default() -> Self {
Self {
was_raw_mode: false,
size: (80, 24),
was_alternate_screen: false,
was_mouse_enabled: false,
}
}
}
pub struct TerminalStateGuard {
saved_state: TerminalState,
is_raw_mode_active: Arc<AtomicBool>,
_needs_cleanup: bool,
}
impl TerminalStateGuard {
pub fn new() -> Result<Self> {
let saved_state = Self::save_terminal_state()?;
let is_raw_mode_active = Arc::new(AtomicBool::new(false));
let _guard = TERMINAL_MUTEX.lock().unwrap();
if !RAW_MODE_ACTIVE.load(Ordering::SeqCst) {
enable_raw_mode().with_context(|| "Failed to enable raw mode")?;
RAW_MODE_ACTIVE.store(true, Ordering::SeqCst);
is_raw_mode_active.store(true, Ordering::Relaxed);
}
execute!(std::io::stdout(), EnableBracketedPaste)
.with_context(|| "Failed to enable bracketed paste mode")?;
Ok(Self {
saved_state,
is_raw_mode_active,
_needs_cleanup: true,
})
}
pub fn new_without_raw_mode() -> Result<Self> {
let saved_state = Self::save_terminal_state()?;
let is_raw_mode_active = Arc::new(AtomicBool::new(false));
Ok(Self {
saved_state,
is_raw_mode_active,
_needs_cleanup: false,
})
}
pub fn enter_raw_mode(&self) -> Result<()> {
let _guard = TERMINAL_MUTEX.lock().unwrap();
if !RAW_MODE_ACTIVE.load(Ordering::SeqCst) {
enable_raw_mode().with_context(|| "Failed to enable raw mode")?;
RAW_MODE_ACTIVE.store(true, Ordering::SeqCst);
self.is_raw_mode_active.store(true, Ordering::Relaxed);
}
Ok(())
}
pub fn exit_raw_mode(&self) -> Result<()> {
let _guard = TERMINAL_MUTEX.lock().unwrap();
if RAW_MODE_ACTIVE.load(Ordering::SeqCst) {
disable_raw_mode().with_context(|| "Failed to disable raw mode")?;
RAW_MODE_ACTIVE.store(false, Ordering::SeqCst);
self.is_raw_mode_active.store(false, Ordering::Relaxed);
}
Ok(())
}
pub fn is_raw_mode_active(&self) -> bool {
self.is_raw_mode_active.load(Ordering::Relaxed)
}
pub fn saved_state(&self) -> &TerminalState {
&self.saved_state
}
fn save_terminal_state() -> Result<TerminalState> {
let size = if let Some((terminal_size::Width(w), terminal_size::Height(h))) =
terminal_size::terminal_size()
{
(u32::from(w), u32::from(h))
} else {
(80, 24) };
Ok(TerminalState {
was_raw_mode: false,
size,
was_alternate_screen: false,
was_mouse_enabled: false,
})
}
fn restore_terminal_state(&self) -> Result<()> {
let _guard = TERMINAL_MUTEX.lock().unwrap();
if let Err(e) = execute!(std::io::stdout(), DisableBracketedPaste) {
eprintln!("Warning: Failed to disable bracketed paste mode during cleanup: {e}");
}
let _ = std::io::stdout().write_all(
b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1049l\x1b[?25h",
);
let _ = std::io::stdout().flush();
if RAW_MODE_ACTIVE.load(Ordering::SeqCst) {
if let Err(e) = disable_raw_mode() {
eprintln!("Warning: Failed to disable raw mode during cleanup: {e}");
} else {
RAW_MODE_ACTIVE.store(false, Ordering::SeqCst);
}
}
if self.is_raw_mode_active.load(Ordering::Relaxed) {
self.is_raw_mode_active.store(false, Ordering::Relaxed);
}
Ok(())
}
}
impl Drop for TerminalStateGuard {
fn drop(&mut self) {
if let Err(e) = self.restore_terminal_state() {
eprintln!("Warning: Failed to restore terminal state: {e}");
}
}
}
pub fn force_terminal_cleanup() {
let _guard = TERMINAL_MUTEX.try_lock().ok();
let _ = std::io::stdout()
.write_all(b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1049l\x1b[?25h");
let _ = std::io::stdout().flush();
if RAW_MODE_ACTIVE.load(Ordering::SeqCst) {
let _ = disable_raw_mode();
RAW_MODE_ACTIVE.store(false, Ordering::SeqCst);
}
}
pub struct TerminalOps;
impl TerminalOps {
pub fn enable_mouse() -> Result<()> {
use crossterm::event::EnableMouseCapture;
use crossterm::execute;
execute!(std::io::stdout(), EnableMouseCapture)
.with_context(|| "Failed to enable mouse capture")?;
Ok(())
}
pub fn disable_mouse() -> Result<()> {
use crossterm::event::DisableMouseCapture;
use crossterm::execute;
execute!(std::io::stdout(), DisableMouseCapture)
.with_context(|| "Failed to disable mouse capture")?;
Ok(())
}
pub fn enable_alternate_screen() -> Result<()> {
use crossterm::execute;
use crossterm::terminal::EnterAlternateScreen;
execute!(std::io::stdout(), EnterAlternateScreen)
.with_context(|| "Failed to enter alternate screen")?;
Ok(())
}
pub fn disable_alternate_screen() -> Result<()> {
use crossterm::execute;
use crossterm::terminal::LeaveAlternateScreen;
execute!(std::io::stdout(), LeaveAlternateScreen)
.with_context(|| "Failed to leave alternate screen")?;
Ok(())
}
pub fn clear_screen() -> Result<()> {
use crossterm::execute;
use crossterm::terminal::{Clear, ClearType};
execute!(std::io::stdout(), Clear(ClearType::All))
.with_context(|| "Failed to clear screen")?;
Ok(())
}
pub fn cursor_home() -> Result<()> {
use crossterm::cursor::MoveTo;
use crossterm::execute;
execute!(std::io::stdout(), MoveTo(0, 0))
.with_context(|| "Failed to move cursor to home")?;
Ok(())
}
pub fn set_title(title: &str) -> Result<()> {
use crossterm::execute;
use crossterm::terminal::SetTitle;
execute!(std::io::stdout(), SetTitle(title))
.with_context(|| "Failed to set terminal title")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_force_terminal_cleanup_idempotent() {
force_terminal_cleanup();
force_terminal_cleanup();
}
#[test]
fn test_try_lock_ok_survives_poisoned_mutex() {
let m = Mutex::new(());
let _ = std::panic::catch_unwind(|| {
let _guard = m.lock().unwrap();
panic!("intentional poison");
});
assert!(m.is_poisoned(), "mutex should be poisoned after the above");
let guard = m.try_lock().ok();
assert!(guard.is_none(), "expected None for a poisoned mutex");
}
#[test]
fn test_try_lock_ok_does_not_block_when_held() {
let m = Mutex::new(());
let _held = m.lock().unwrap();
let guard = m.try_lock().ok();
assert!(guard.is_none(), "expected None when lock is already held");
}
#[test]
fn test_terminal_state_default() {
let state = TerminalState::default();
assert!(!state.was_raw_mode);
assert!(!state.was_alternate_screen);
assert!(!state.was_mouse_enabled);
assert_eq!(state.size, (80, 24));
}
}