use crossterm::{
cursor::{Hide, Show},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use std::io::{self, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Once;
use crate::error::Result;
pub const MIN_TERM_WIDTH: u16 = 60;
pub const MIN_TERM_HEIGHT: u16 = 14;
static ALT_SCREEN_DEPTH: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalConstraints {
pub min_width: u16,
pub min_height: u16,
}
impl Default for TerminalConstraints {
fn default() -> Self {
Self {
min_width: MIN_TERM_WIDTH,
min_height: MIN_TERM_HEIGHT,
}
}
}
pub fn get_terminal_size() -> (u16, u16) {
size().unwrap_or((80, 24))
}
pub fn is_terminal_too_small() -> bool {
let (w, h) = get_terminal_size();
w < MIN_TERM_WIDTH || h < MIN_TERM_HEIGHT
}
pub fn clean_exit() -> ! {
restore_terminal();
std::process::exit(0);
}
pub fn restore_terminal() {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, Show);
}
#[derive(Debug)]
pub struct AltScreenGuard;
pub fn is_alt_screen_active() -> bool {
ALT_SCREEN_DEPTH.load(Ordering::SeqCst) > 0
}
impl AltScreenGuard {
pub fn enter() -> Self {
init_terminal_panic_hook();
if ALT_SCREEN_DEPTH.fetch_add(1, Ordering::SeqCst) == 0 {
let _ = execute!(io::stdout(), EnterAlternateScreen, Hide);
}
AltScreenGuard
}
}
impl Drop for AltScreenGuard {
fn drop(&mut self) {
if ALT_SCREEN_DEPTH.fetch_sub(1, Ordering::SeqCst) == 1 {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, Show);
}
}
}
#[derive(Debug)]
pub struct TerminalGuard {
_alt: AltScreenGuard,
}
impl TerminalGuard {
pub fn enter() -> Result<Self> {
let guard = AltScreenGuard::enter();
enable_raw_mode()?;
Ok(Self { _alt: guard })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
}
}
pub fn wait_for_valid_size(stdout: &mut io::Stdout) -> Result<(u16, u16)> {
wait_for_constraints(stdout, TerminalConstraints::default())
}
pub fn wait_for_constraints(
stdout: &mut io::Stdout,
constraints: TerminalConstraints,
) -> Result<(u16, u16)> {
let (mut w, mut h) = get_terminal_size();
if w >= constraints.min_width && h >= constraints.min_height {
return Ok((w, h));
}
loop {
execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
let output =
crate::frame::render_too_small(w, h, constraints.min_width, constraints.min_height);
for line in output.lines() {
print!("{}\x1B[K\r\n", line);
}
execute!(stdout, Clear(ClearType::FromCursorDown))?;
stdout.flush()?;
match crossterm::event::read()? {
crossterm::event::Event::Resize(new_w, new_h) => {
w = new_w;
h = new_h;
if w >= constraints.min_width && h >= constraints.min_height {
return Ok((w, h));
}
}
crossterm::event::Event::Key(key) => {
if key.kind == crossterm::event::KeyEventKind::Press
&& ((key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL)
&& (key.code == crossterm::event::KeyCode::Char('c')
|| key.code == crossterm::event::KeyCode::Char('C')))
|| key.code == crossterm::event::KeyCode::Char('\x03')
|| key.code == crossterm::event::KeyCode::Char('q')
|| key.code == crossterm::event::KeyCode::Char('Q'))
{
clean_exit();
}
let (cur_w, cur_h) = get_terminal_size();
w = cur_w;
h = cur_h;
if w >= constraints.min_width && h >= constraints.min_height {
return Ok((w, h));
}
}
_ => {}
}
}
}
pub fn get_content_width(max_desired: u16) -> usize {
let (term_w, _) = get_terminal_size();
let available = (term_w as usize).saturating_sub(2);
if max_desired == 0 || max_desired <= 80 {
available.max(MIN_TERM_WIDTH as usize)
} else {
available
.min(max_desired as usize)
.max(MIN_TERM_WIDTH as usize)
}
}
pub fn init_terminal_panic_hook() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
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, Show);
original_hook(panic_info);
}));
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_width_clamping() {
let width = get_content_width(80);
assert!(width >= MIN_TERM_WIDTH as usize);
}
}