use crossterm::{
cursor::{Hide, Show},
execute, queue,
terminal::{
disable_raw_mode, enable_raw_mode, size as term_size, Clear, ClearType,
EnterAlternateScreen, LeaveAlternateScreen,
},
tty::IsTty,
};
use std::io::{stdout, StdoutLock, Write};
use super::error::RendererError;
#[derive(Debug)]
pub struct TerminalState {
term_size: (u16, u16),
colors_enabled: bool,
alternate_screen: bool,
raw_mode: bool,
cursor_hidden: bool,
is_tty: bool,
}
impl TerminalState {
pub fn new() -> Result<Self, RendererError> {
let term_size = if Self::is_test_env() {
(80, 24) } else {
term_size().map_err(|e| {
RendererError::TerminalError(format!("Failed to get terminal size: {}", e))
})?
};
let is_tty = !Self::is_test_env() && stdout().is_tty();
let colors_enabled = is_tty;
Ok(Self {
term_size,
colors_enabled,
alternate_screen: false,
raw_mode: false,
cursor_hidden: false,
is_tty,
})
}
pub fn setup(&mut self) -> Result<(), RendererError> {
if !self.is_tty {
return Ok(());
}
if !self.raw_mode {
enable_raw_mode().map_err(|e| {
RendererError::TerminalError(format!("Failed to enable raw mode: {}", e))
})?;
self.raw_mode = true;
}
if !self.cursor_hidden {
execute!(stdout(), Hide)?;
self.cursor_hidden = true;
}
Ok(())
}
pub fn cleanup(&mut self) -> Result<(), RendererError> {
if !self.is_tty {
return Ok(());
}
let mut stdout = stdout();
if self.cursor_hidden {
execute!(stdout, Show)?;
self.cursor_hidden = false;
}
if self.raw_mode {
disable_raw_mode().map_err(|e| {
RendererError::TerminalError(format!("Failed to disable raw mode: {}", e))
})?;
self.raw_mode = false;
}
if self.alternate_screen {
execute!(stdout, LeaveAlternateScreen)?;
self.alternate_screen = false;
}
stdout.flush()?;
Ok(())
}
pub fn enter_alternate_screen(&mut self) -> Result<(), RendererError> {
if !self.is_tty {
return Ok(());
}
if !self.alternate_screen {
execute!(stdout(), EnterAlternateScreen)?;
self.alternate_screen = true;
}
self.setup()?;
Ok(())
}
pub fn clear_screen(&mut self) -> Result<(), RendererError> {
if self.is_tty {
queue!(stdout(), Clear(ClearType::All))?;
}
Ok(())
}
pub fn flush(&self) -> Result<(), RendererError> {
stdout().flush()?;
Ok(())
}
pub fn stdout(&self) -> StdoutLock {
stdout().lock()
}
#[inline]
pub fn size(&self) -> (u16, u16) {
self.term_size
}
pub fn resize(&mut self, width: u16, height: u16) -> Result<(), RendererError> {
self.term_size = (width, height);
if self.is_tty {
let mut stdout = stdout();
queue!(stdout, Clear(ClearType::All))?;
stdout.flush()?;
}
Ok(())
}
#[inline]
pub fn colors_enabled(&self) -> bool {
self.colors_enabled
}
pub fn set_colors_enabled(&mut self, enabled: bool) {
self.colors_enabled = enabled;
}
#[inline]
pub fn is_tty(&self) -> bool {
self.is_tty
}
pub fn show_cursor(&mut self) -> Result<(), RendererError> {
if self.is_tty && self.cursor_hidden {
execute!(stdout(), Show)?;
self.cursor_hidden = false;
}
Ok(())
}
pub fn hide_cursor(&mut self) -> Result<(), RendererError> {
if self.is_tty && !self.cursor_hidden {
execute!(stdout(), Hide)?;
self.cursor_hidden = true;
}
Ok(())
}
pub fn try_recover(&mut self) -> Result<(), RendererError> {
if self.is_tty {
let _ = execute!(stdout(), Show);
let _ = disable_raw_mode();
let _ = execute!(stdout(), LeaveAlternateScreen);
self.setup()?;
}
Ok(())
}
#[inline]
fn is_test_env() -> bool {
std::env::var("RUST_TEST").is_ok()
|| std::env::var("CARGO_TARGET_DIR").is_ok()
|| std::env::var("CI").is_ok()
|| std::env::var("TERM").map(|v| v == "dumb").unwrap_or(false)
}
}
impl Drop for TerminalState {
fn drop(&mut self) {
if let Err(e) = self.cleanup() {
eprintln!("Error cleaning up terminal state: {}", e);
}
}
}