use std::sync::{
Arc, LazyLock, Mutex, MutexGuard, PoisonError, TryLockError,
atomic::{AtomicBool, Ordering},
};
use anyhow::{Context, Result};
use crossterm::{
event::EnableBracketedPaste,
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
use crate::diagnosticln as eprintln;
use super::terminal_protocol::{ProtocolState, capture_protocol_state, write_terminal_cleanup};
use super::terminal_screen::ScreenModeTracker;
static TERMINAL_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
static SAVED_PROTOCOL_STATE: LazyLock<Mutex<Option<ProtocolState>>> =
LazyLock::new(|| Mutex::new(None));
static RAW_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
static TERMINAL_OWNER_ACTIVE: AtomicBool = AtomicBool::new(false);
fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn publish_protocol_state(state: &ProtocolState) {
*lock_recover(&SAVED_PROTOCOL_STATE) = Some(state.clone());
}
struct TerminalOwner;
impl TerminalOwner {
fn acquire() -> Result<Self> {
if TERMINAL_OWNER_ACTIVE
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
anyhow::bail!("Another terminal state guard is already active");
}
Ok(Self)
}
}
impl Drop for TerminalOwner {
fn drop(&mut self) {
TERMINAL_OWNER_ACTIVE.store(false, Ordering::SeqCst);
}
}
#[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: AtomicBool,
protocol_cleanup_needed: bool,
pending_input: Vec<u8>,
protocol_state: ProtocolState,
screen_mode_tracker: ScreenModeTracker,
_owner: TerminalOwner,
}
impl TerminalStateGuard {
pub fn new() -> Result<Self> {
let owner = TerminalOwner::acquire()?;
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);
}
let capture = capture_protocol_state(&mut std::io::stdout());
let protocol_state = capture.state.clone();
publish_protocol_state(&protocol_state);
execute!(std::io::stdout(), EnableBracketedPaste)
.with_context(|| "Failed to enable bracketed paste mode")?;
Ok(Self {
saved_state,
is_raw_mode_active,
needs_cleanup: AtomicBool::new(true),
protocol_cleanup_needed: true,
pending_input: capture.pending_input,
protocol_state,
screen_mode_tracker: ScreenModeTracker::new(),
_owner: owner,
})
}
pub fn new_without_raw_mode() -> Result<Self> {
let owner = TerminalOwner::acquire()?;
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: AtomicBool::new(false),
protocol_cleanup_needed: false,
pending_input: Vec::new(),
protocol_state: ProtocolState::default(),
screen_mode_tracker: ScreenModeTracker::new(),
_owner: owner,
})
}
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);
}
self.needs_cleanup.store(true, Ordering::Release);
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);
}
if !self.protocol_cleanup_needed {
self.needs_cleanup.store(false, Ordering::Release);
}
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
}
pub(crate) fn take_pending_input(&mut self) -> Vec<u8> {
std::mem::take(&mut self.pending_input)
}
pub(crate) fn observe_remote_output(&mut self, output: &[u8]) {
if let Some(current) = self.screen_mode_tracker.observe(output) {
self.protocol_state.current_1049 = Some(current);
publish_protocol_state(&self.protocol_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 self.protocol_cleanup_needed {
write_terminal_cleanup(&mut std::io::stdout(), &self.protocol_state);
}
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);
}
self.needs_cleanup.store(false, Ordering::Release);
Ok(())
}
}
impl Drop for TerminalStateGuard {
fn drop(&mut self) {
if self.needs_cleanup.load(Ordering::Acquire)
&& 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 state = match SAVED_PROTOCOL_STATE.try_lock() {
Ok(saved) => saved.clone(),
Err(TryLockError::Poisoned(error)) => error.into_inner().clone(),
Err(TryLockError::WouldBlock) => None,
}
.unwrap_or_default();
write_terminal_cleanup(&mut std::io::stdout(), &state);
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));
}
#[test]
#[serial_test::serial(terminal_owner)]
fn test_terminal_owner_applies_to_non_raw_guard_and_releases_on_drop() {
let guard = TerminalStateGuard::new_without_raw_mode().unwrap();
assert!(!guard.needs_cleanup.load(Ordering::Acquire));
assert!(TerminalStateGuard::new_without_raw_mode().is_err());
drop(guard);
assert!(TerminalOwner::acquire().is_ok());
}
#[test]
fn test_lock_recover_allows_guaranteed_publication_after_poison() {
let saved = Mutex::new(None);
let _ = std::panic::catch_unwind(|| {
let _guard = saved.lock().unwrap();
panic!("intentional poison");
});
let expected = ProtocolState {
main_kitty_flags: Some(7),
..ProtocolState::default()
};
*lock_recover(&saved) = Some(expected.clone());
assert_eq!(*lock_recover(&saved), Some(expected));
}
}