use std::io::{self, Write};
use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Frame, Terminal, backend::CrosstermBackend};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TerminalFeatures {
mouse_enabled: bool,
keyboard_enhancements_supported: bool,
}
impl TerminalFeatures {
pub fn new() -> Self {
Self::default()
}
pub fn mouse_enabled(mut self, enabled: bool) -> Self {
self.mouse_enabled = enabled;
self
}
pub fn keyboard_enhancements_supported(mut self, supported: bool) -> Self {
self.keyboard_enhancements_supported = supported;
self
}
pub fn enter<W: Write>(self, writer: W) -> anyhow::Result<TerminalSession<W>> {
TerminalSession::enter(writer, self)
}
}
pub struct TerminalSession<W: Write> {
terminal: Terminal<CrosstermBackend<W>>,
features: TerminalFeatures,
active: bool,
}
impl<W: Write> TerminalSession<W> {
fn enter(writer: W, features: TerminalFeatures) -> anyhow::Result<Self> {
let backend = CrosstermBackend::new(writer);
let mut terminal = Terminal::new(backend)?;
if let Err(err) = activate_writer(terminal.backend_mut(), features) {
deactivate_writer_best_effort(terminal.backend_mut(), features.mouse_enabled);
return Err(err);
}
Ok(Self {
terminal,
features,
active: true,
})
}
pub fn draw<F>(&mut self, render_callback: F) -> std::io::Result<()>
where
F: FnOnce(&mut Frame),
{
self.terminal.draw(render_callback).map(|_| ())
}
pub fn backend_mut(&mut self) -> &mut CrosstermBackend<W> {
self.terminal.backend_mut()
}
pub fn restore(&mut self) -> anyhow::Result<()> {
if !self.active {
return Ok(());
}
self.deactivate()?;
Ok(())
}
pub fn suspend(&mut self) -> anyhow::Result<TerminalSuspension<'_, W>> {
self.deactivate()?;
Ok(TerminalSuspension {
session: Some(self),
})
}
fn activate(&mut self) -> anyhow::Result<()> {
if self.active {
return Ok(());
}
if let Err(err) = activate_writer(self.terminal.backend_mut(), self.features) {
deactivate_writer_best_effort(self.terminal.backend_mut(), self.features.mouse_enabled);
return Err(err);
}
self.active = true;
Ok(())
}
fn deactivate(&mut self) -> anyhow::Result<()> {
if !self.active {
return Ok(());
}
deactivate_writer(self.terminal.backend_mut(), self.features.mouse_enabled)?;
self.active = false;
Ok(())
}
}
impl<W: Write> Drop for TerminalSession<W> {
fn drop(&mut self) {
if self.active {
deactivate_writer_best_effort(self.terminal.backend_mut(), self.features.mouse_enabled);
self.active = false;
}
}
}
pub struct TerminalSuspension<'a, W: Write> {
session: Option<&'a mut TerminalSession<W>>,
}
impl<W: Write> TerminalSuspension<'_, W> {
pub fn resume(mut self) -> anyhow::Result<()> {
let Some(session) = self.session.take() else {
return Ok(());
};
session.activate()?;
session.terminal.clear()?;
Ok(())
}
}
impl<W: Write> Drop for TerminalSuspension<'_, W> {
fn drop(&mut self) {
let Some(session) = self.session.take() else {
return;
};
if session.activate().is_ok() {
let _ = session.terminal.clear();
}
}
}
pub fn restore_stdio_best_effort() {
let mut stdout = io::stdout();
deactivate_writer_best_effort(&mut stdout, true);
}
fn activate_writer<W: Write>(writer: &mut W, features: TerminalFeatures) -> anyhow::Result<()> {
enable_raw_mode()?;
execute!(writer, EnterAlternateScreen)?;
if features.mouse_enabled {
execute!(writer, EnableMouseCapture)?;
}
execute!(writer, EnableBracketedPaste)?;
if features.keyboard_enhancements_supported {
let _ = execute!(
writer,
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES,
)
);
}
Ok(())
}
fn deactivate_writer<W: Write>(writer: &mut W, mouse_enabled: bool) -> anyhow::Result<()> {
let _ = execute!(writer, PopKeyboardEnhancementFlags);
let _ = execute!(writer, DisableBracketedPaste);
if mouse_enabled {
let _ = execute!(writer, DisableMouseCapture);
}
disable_raw_mode()?;
execute!(writer, LeaveAlternateScreen)?;
Ok(())
}
fn deactivate_writer_best_effort<W: Write>(writer: &mut W, mouse_enabled: bool) {
let _ = deactivate_writer(writer, mouse_enabled);
}