use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use crossterm::event::{DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use crossterm::{execute, queue};
use super::{TerminalAdapter, TerminalResult};
use crate::ratatui::backend::CrosstermBackend;
use crate::ratatui::{Terminal, TerminalOptions};
use crate::terminal::TerminalError;
bitflags::bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Modes: u8 {
const NONE = 0b0000_0000;
const RAW = 0b0000_0001;
const ALTERNATE = 0b0000_0010;
const MOUSE = 0b0000_0100;
}
}
#[derive(Debug)]
pub struct CrosstermTerminalAdapter {
terminal: Terminal<CrosstermBackend<std::io::Stdout>>,
modes: Arc<AtomicU8>,
}
impl CrosstermTerminalAdapter {
pub fn new() -> TerminalResult<Self> {
Self::new_with_options(TerminalOptions::default())
}
pub fn new_with_options(options: TerminalOptions) -> TerminalResult<Self> {
let backend = CrosstermBackend::new(std::io::stdout());
let terminal = Terminal::with_options(backend, options)
.map_err(|_| TerminalError::CannotConnectStdout)?;
let modes = Arc::new(AtomicU8::new(Modes::NONE.bits()));
let modes_c = modes.clone();
Self::panic_handler(modes_c);
Ok(Self { terminal, modes })
}
pub fn restore(&mut self) -> std::io::Result<()> {
let writer = self.terminal.backend_mut();
let modes =
Modes::from_bits_truncate(self.modes.swap(Modes::NONE.bits(), Ordering::AcqRel));
if modes.contains(Modes::MOUSE) {
queue!(writer, DisableMouseCapture)?;
}
if modes.contains(Modes::ALTERNATE) {
queue!(writer, LeaveAlternateScreen)?;
}
if modes.contains(Modes::RAW) {
disable_raw_mode()?;
}
writer.flush()
}
pub fn enable_bracketed_paste(&mut self) -> TerminalResult<()> {
execute!(self.terminal.backend_mut(), EnableBracketedPaste)
.map_err(|_| TerminalError::Other("Cannot enable Bracketed Paste"))
}
fn panic_handler(modes: Arc<AtomicU8>) {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let mut stdout = std::io::stdout();
let modes = Modes::from_bits_truncate(modes.swap(Modes::NONE.bits(), Ordering::AcqRel));
if modes.contains(Modes::MOUSE) {
let _ = queue!(stdout, DisableMouseCapture);
}
if modes.contains(Modes::ALTERNATE) {
let _ = queue!(stdout, LeaveAlternateScreen);
}
if modes.contains(Modes::RAW) {
let _ = disable_raw_mode();
}
let _ = stdout.flush();
hook(info);
}));
}
fn set_mode(&self, mode: Modes) {
let active = self.modes.load(Ordering::SeqCst);
self.modes.store(active | mode.bits(), Ordering::SeqCst);
}
fn unset_mode(&self, mode: Modes) {
let active = self.modes.load(Ordering::SeqCst);
self.modes.store(active ^ mode.bits(), Ordering::SeqCst);
}
}
impl TerminalAdapter for CrosstermTerminalAdapter {
type Backend = CrosstermBackend<std::io::Stdout>;
fn clear_screen(&mut self) -> TerminalResult<()> {
self.terminal
.clear()
.map_err(|_| TerminalError::CannotClear)
}
fn enable_raw_mode(&mut self) -> TerminalResult<()> {
enable_raw_mode()
.map_err(|_| TerminalError::CannotToggleRawMode)
.inspect(|_| {
self.set_mode(Modes::RAW);
})
}
fn disable_raw_mode(&mut self) -> TerminalResult<()> {
disable_raw_mode()
.map_err(|_| TerminalError::CannotToggleRawMode)
.inspect(|_| {
self.unset_mode(Modes::RAW);
})
}
fn enter_alternate_screen(&mut self) -> TerminalResult<()> {
execute!(self.terminal.backend_mut(), EnterAlternateScreen)
.map_err(|_| TerminalError::CannotEnterAlternateMode)
.inspect(|_| {
self.set_mode(Modes::ALTERNATE);
})
}
fn leave_alternate_screen(&mut self) -> TerminalResult<()> {
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)
.map_err(|_| TerminalError::CannotLeaveAlternateMode)
.inspect(|_| {
self.unset_mode(Modes::ALTERNATE);
})
}
fn enable_mouse_capture(&mut self) -> TerminalResult<()> {
execute!(self.raw_mut().backend_mut(), EnableMouseCapture)
.map_err(|_| TerminalError::CannotToggleMouseCapture)
.inspect(|_| {
self.set_mode(Modes::MOUSE);
})
}
fn disable_mouse_capture(&mut self) -> TerminalResult<()> {
execute!(self.raw_mut().backend_mut(), DisableMouseCapture)
.map_err(|_| TerminalError::CannotToggleMouseCapture)
.inspect(|_| {
self.unset_mode(Modes::MOUSE);
})
}
fn raw_mut(&mut self) -> &mut Terminal<CrosstermBackend<std::io::Stdout>> {
&mut self.terminal
}
fn raw(&self) -> &Terminal<CrosstermBackend<std::io::Stdout>> {
&self.terminal
}
}
impl Drop for CrosstermTerminalAdapter {
fn drop(&mut self) {
let _ = self.restore();
}
}