use std::io::{self, Stdout};
use crossterm::{
cursor,
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use crate::error::TuiError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSupport {
NoColor,
Basic,
Colors256,
TrueColor,
}
#[derive(Debug, Clone)]
pub struct TermCaps {
pub color_support: ColorSupport,
pub unicode: bool,
pub width: u16,
pub height: u16,
}
impl Default for TermCaps {
fn default() -> Self {
Self {
color_support: ColorSupport::TrueColor,
unicode: true,
width: 80,
height: 24,
}
}
}
impl TermCaps {
pub fn is_small(&self) -> bool {
self.width < 80 || self.height < 24
}
}
pub fn detect_terminal_caps() -> TermCaps {
let term = std::env::var("TERM").unwrap_or_default();
let colorterm = std::env::var("COLORTERM").unwrap_or_default();
let lang = std::env::var("LANG").unwrap_or_default();
let color_support = detect_color_support(&term, &colorterm);
let unicode = detect_unicode(&term, &lang);
let (width, height) = crossterm::terminal::size().unwrap_or((80, 24));
TermCaps {
color_support,
unicode,
width,
height,
}
}
fn detect_color_support(term: &str, colorterm: &str) -> ColorSupport {
let ct_lower = colorterm.to_lowercase();
if ct_lower == "truecolor" || ct_lower == "24bit" {
return ColorSupport::TrueColor;
}
let term_lower = term.to_lowercase();
if term_lower == "dumb" || term_lower.is_empty() {
return ColorSupport::NoColor;
}
if term_lower.ends_with("-256color") || term_lower.contains("256color") {
return ColorSupport::Colors256;
}
ColorSupport::Basic
}
fn detect_unicode(term: &str, lang: &str) -> bool {
let term_lower = term.to_lowercase();
if term_lower == "dumb" {
return false;
}
let lang_lower = lang.to_lowercase();
if lang_lower.contains("utf-8") || lang_lower.contains("utf8") {
return true;
}
true
}
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
pub struct TerminalGuard(pub Tui);
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = restore_terminal(&mut self.0);
}
}
pub fn init_terminal() -> Result<TerminalGuard, TuiError> {
enable_raw_mode()?;
let mut stdout = io::stdout();
if let Err(e) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) {
let _ = disable_raw_mode();
return Err(TuiError::Terminal(e));
}
let backend = CrosstermBackend::new(stdout);
match Terminal::new(backend) {
Ok(terminal) => Ok(TerminalGuard(terminal)),
Err(e) => {
let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
let _ = disable_raw_mode();
Err(TuiError::Terminal(e))
}
}
}
pub fn restore_terminal(terminal: &mut Tui) -> Result<(), TuiError> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
pub fn install_panic_hook() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = disable_raw_mode();
let _ = execute!(
io::stdout(),
LeaveAlternateScreen,
DisableMouseCapture,
cursor::Show
);
original_hook(panic_info);
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_panic_hook_install() {
install_panic_hook();
}
#[test]
fn test_restore_is_idempotent() {
let _ = disable_raw_mode();
}
#[test]
fn test_terminal_guard_is_send() {
fn assert_send<T: Send>() {}
assert_send::<TerminalGuard>();
}
#[test]
fn test_detect_truecolor() {
let result = detect_color_support("xterm-256color", "truecolor");
assert_eq!(result, ColorSupport::TrueColor);
}
#[test]
fn test_detect_truecolor_24bit() {
let result = detect_color_support("xterm", "24bit");
assert_eq!(result, ColorSupport::TrueColor);
}
#[test]
fn test_detect_256color() {
let result = detect_color_support("xterm-256color", "");
assert_eq!(result, ColorSupport::Colors256);
}
#[test]
fn test_detect_basic_color() {
let result = detect_color_support("xterm", "");
assert_eq!(result, ColorSupport::Basic);
}
#[test]
fn test_detect_no_color_dumb() {
let result = detect_color_support("dumb", "");
assert_eq!(result, ColorSupport::NoColor);
}
#[test]
fn test_detect_no_color_empty() {
let result = detect_color_support("", "");
assert_eq!(result, ColorSupport::NoColor);
}
#[test]
fn test_detect_unicode_utf8_lang() {
assert!(detect_unicode("xterm", "en_US.UTF-8"));
}
#[test]
fn test_detect_unicode_dumb_term() {
assert!(!detect_unicode("dumb", "en_US.UTF-8"));
}
#[test]
fn test_ascii_fallback_no_unicode() {
assert!(!detect_unicode("dumb", ""));
}
#[test]
fn test_term_caps_default() {
let caps = TermCaps::default();
assert_eq!(caps.color_support, ColorSupport::TrueColor);
assert!(caps.unicode);
assert_eq!(caps.width, 80);
assert_eq!(caps.height, 24);
}
#[test]
fn test_term_caps_is_small() {
let mut caps = TermCaps::default();
assert!(!caps.is_small()); caps.width = 79;
assert!(caps.is_small());
caps.width = 80;
caps.height = 23;
assert!(caps.is_small());
}
}