use anyhow::{Context, Result};
use crossterm::{
event::{DisableBracketedPaste, EnableBracketedPaste},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
use once_cell::sync::Lazy;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
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}");
}
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.lock().unwrap();
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(())
}
}