use crate::program::MouseMode;
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, Stdout};
#[derive(Debug, Clone)]
pub struct TerminalConfig {
pub alt_screen: bool,
pub mouse_mode: MouseMode,
pub bracketed_paste: bool,
pub focus_reporting: bool,
pub headless: bool,
}
impl Default for TerminalConfig {
fn default() -> Self {
Self {
alt_screen: true,
mouse_mode: MouseMode::None,
bracketed_paste: false,
focus_reporting: false,
headless: false,
}
}
}
pub struct TerminalManager {
terminal: Option<Terminal<CrosstermBackend<Stdout>>>,
config: TerminalConfig,
alt_screen_was_active: bool,
is_released: bool,
}
impl TerminalManager {
pub fn new(config: TerminalConfig) -> io::Result<Self> {
let terminal = if !config.headless {
Some(Self::setup_terminal(&config)?)
} else {
None
};
Ok(Self {
terminal,
config,
alt_screen_was_active: false,
is_released: false,
})
}
fn setup_terminal(config: &TerminalConfig) -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
let mut stdout = io::stdout();
enable_raw_mode()?;
if config.alt_screen {
execute!(stdout, EnterAlternateScreen)?;
}
match config.mouse_mode {
MouseMode::CellMotion => {
execute!(stdout, crossterm::event::EnableMouseCapture)?;
}
MouseMode::AllMotion => {
execute!(
stdout,
crossterm::event::EnableMouseCapture,
crossterm::cursor::Show,
crossterm::cursor::Hide,
)?;
}
MouseMode::None => {}
}
if config.bracketed_paste {
execute!(stdout, crossterm::event::EnableBracketedPaste)?;
}
if config.focus_reporting {
execute!(stdout, crossterm::event::EnableFocusChange)?;
}
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
pub fn terminal(&self) -> Option<&Terminal<CrosstermBackend<Stdout>>> {
self.terminal.as_ref()
}
pub fn terminal_mut(&mut self) -> Option<&mut Terminal<CrosstermBackend<Stdout>>> {
self.terminal.as_mut()
}
pub fn release(&mut self) -> io::Result<()> {
if self.is_released || self.config.headless {
return Ok(());
}
self.alt_screen_was_active = self.config.alt_screen;
if let Some(ref mut terminal) = self.terminal {
terminal.show_cursor()?;
}
if self.config.alt_screen {
execute!(io::stdout(), LeaveAlternateScreen)?;
}
disable_raw_mode()?;
self.is_released = true;
Ok(())
}
pub fn restore(&mut self) -> io::Result<()> {
if !self.is_released || self.config.headless {
return Ok(());
}
enable_raw_mode()?;
if self.alt_screen_was_active {
execute!(io::stdout(), EnterAlternateScreen)?;
}
if let Some(ref mut terminal) = self.terminal {
terminal.hide_cursor()?;
terminal.clear()?;
}
self.is_released = false;
Ok(())
}
pub fn is_released(&self) -> bool {
self.is_released
}
pub fn cleanup(&mut self) -> io::Result<()> {
if self.config.headless {
return Ok(());
}
if let Some(ref mut terminal) = self.terminal {
let _ = terminal.show_cursor();
}
let mut stdout = io::stdout();
if self.config.focus_reporting {
let _ = execute!(stdout, crossterm::event::DisableFocusChange);
}
if self.config.bracketed_paste {
let _ = execute!(stdout, crossterm::event::DisableBracketedPaste);
}
if self.config.mouse_mode != MouseMode::None {
let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
}
if self.config.alt_screen && !self.is_released {
let _ = execute!(stdout, LeaveAlternateScreen);
}
let _ = disable_raw_mode();
Ok(())
}
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(&mut ratatui::Frame),
{
if let Some(ref mut terminal) = self.terminal {
terminal.draw(f)?;
}
Ok(())
}
pub fn size(&self) -> io::Result<ratatui::layout::Rect> {
if let Some(ref terminal) = self.terminal {
let size = terminal.size()?;
Ok(ratatui::layout::Rect::new(0, 0, size.width, size.height))
} else {
Ok(ratatui::layout::Rect::new(0, 0, 80, 24))
}
}
pub fn clear(&mut self) -> io::Result<()> {
if let Some(ref mut terminal) = self.terminal {
terminal.clear()?;
}
Ok(())
}
}
impl Drop for TerminalManager {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terminal_config_default() {
let config = TerminalConfig::default();
assert!(config.alt_screen);
assert_eq!(config.mouse_mode, MouseMode::None);
assert!(!config.bracketed_paste);
assert!(!config.focus_reporting);
assert!(!config.headless);
}
#[test]
fn test_terminal_manager_headless() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let manager = TerminalManager::new(config).unwrap();
assert!(manager.terminal().is_none());
assert!(!manager.is_released());
}
#[test]
fn test_terminal_manager_release_restore() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let mut manager = TerminalManager::new(config).unwrap();
assert!(manager.release().is_ok());
assert!(!manager.is_released());
assert!(manager.restore().is_ok());
assert!(!manager.is_released());
}
#[test]
fn test_terminal_manager_size_headless() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let manager = TerminalManager::new(config).unwrap();
let size = manager.size().unwrap();
assert_eq!(size.width, 80);
assert_eq!(size.height, 24);
}
#[test]
fn test_terminal_manager_clear_headless() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let mut manager = TerminalManager::new(config).unwrap();
assert!(manager.clear().is_ok());
}
#[test]
fn test_terminal_manager_draw_headless() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let mut manager = TerminalManager::new(config).unwrap();
assert!(manager
.draw(|_f| {
})
.is_ok());
}
#[test]
fn test_terminal_manager_cleanup() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
let mut manager = TerminalManager::new(config).unwrap();
assert!(manager.cleanup().is_ok());
}
#[test]
fn test_terminal_manager_drop() {
let config = TerminalConfig {
headless: true,
..Default::default()
};
{
let _manager = TerminalManager::new(config).unwrap();
}
}
#[test]
fn test_terminal_config_variations() {
let configs = vec![
TerminalConfig {
alt_screen: false,
mouse_mode: MouseMode::CellMotion,
bracketed_paste: true,
focus_reporting: true,
headless: true,
},
TerminalConfig {
alt_screen: true,
mouse_mode: MouseMode::AllMotion,
bracketed_paste: false,
focus_reporting: false,
headless: true,
},
];
for config in configs {
let manager = TerminalManager::new(config).unwrap();
assert!(manager.terminal().is_none()); }
}
}