tslime 0.1.2

A lightweight terminal screensaver simulating slime mold growth patterns
Documentation
//! Terminal screen state and raw mode management.
//!
//! This module handles entering/exiting alternate screen buffers, enabling raw mode,
//! and managing signal handlers for window resizing and interrupts.

#[cfg(unix)]
use crate::terminal::signal::request_shutdown;
use crossterm::{
    cursor, execute,
    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
#[cfg(unix)]
use signal_hook::low_level::{register, unregister};
#[cfg(unix)]
use signal_hook::SigId;
use std::io::{self, Stdout};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

/// Manages the terminal screen state.
///
/// Handles entering/leaving the alternate screen, managing raw mode,
/// and cleaning up on exit.
pub struct TerminalScreen {
    stdout: Stdout,
    is_active: bool,
    resize_flag: Arc<AtomicBool>,
    #[cfg(unix)]
    sigwinch_id: Option<SigId>,
    #[cfg(unix)]
    sigint_id: Option<SigId>,
    #[cfg(unix)]
    sigterm_id: Option<SigId>,
}

impl TerminalScreen {
    /// Create a new terminal screen manager.
    pub fn new() -> Self {
        let resize_flag = Arc::new(AtomicBool::new(false));
        Self {
            stdout: io::stdout(),
            is_active: false,
            resize_flag,
            #[cfg(unix)]
            sigwinch_id: None,
            #[cfg(unix)]
            sigint_id: None,
            #[cfg(unix)]
            sigterm_id: None,
        }
    }

    /// Enter the alternate screen and enable raw mode.
    ///
    /// Registers signal handlers for SIGWINCH, SIGINT, and SIGTERM (on Unix).
    pub fn setup(&mut self) -> io::Result<()> {
        if self.is_active {
            return Ok(());
        }

        execute!(self.stdout, EnterAlternateScreen, cursor::Hide)?;
        terminal::enable_raw_mode()?;
        self.is_active = true;

        #[cfg(unix)]
        {
            use signal_hook::consts::SIGINT;

            // SAFETY: request_shutdown() is signal-safe as it only sets an atomic flag.
            let id = unsafe {
                register(SIGINT, || {
                    request_shutdown();
                })
            }
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("Failed to register SIGINT handler: {}", e),
                )
            })?;
            self.sigint_id = Some(id);
        }

        #[cfg(unix)]
        {
            use signal_hook::consts::SIGTERM;

            // SAFETY: request_shutdown() is signal-safe as it only sets an atomic flag.
            let id = unsafe {
                register(SIGTERM, || {
                    request_shutdown();
                })
            }
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("Failed to register SIGTERM handler: {}", e),
                )
            })?;
            self.sigterm_id = Some(id);
        }

        #[cfg(unix)]
        {
            use signal_hook::consts::SIGWINCH;

            let flag = Arc::clone(&self.resize_flag);
            // SAFETY: Storing to an AtomicBool is signal-safe.
            let id = unsafe {
                register(SIGWINCH, move || {
                    flag.store(true, Ordering::SeqCst);
                })
            }
            .map_err(|e| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("Failed to register SIGWINCH handler: {}", e),
                )
            })?;
            self.sigwinch_id = Some(id);
        }

        Ok(())
    }

    /// Get the current size of the terminal.
    pub fn get_size(&self) -> io::Result<(u16, u16)> {
        terminal::size()
    }

    /// Check if a resize event has occurred since the last check.
    ///
    /// Resets the flag to false.
    pub fn check_resize(&self) -> bool {
        self.resize_flag.swap(false, Ordering::SeqCst)
    }

    /// Leave the alternate screen and disable raw mode.
    ///
    /// Unregisters signal handlers.
    pub fn teardown(&mut self) -> io::Result<()> {
        if !self.is_active {
            return Ok(());
        }

        #[cfg(unix)]
        {
            if let Some(id) = self.sigwinch_id.take() {
                unregister(id);
            }
            if let Some(id) = self.sigint_id.take() {
                unregister(id);
            }
            if let Some(id) = self.sigterm_id.take() {
                unregister(id);
            }
        }

        terminal::disable_raw_mode()?;
        execute!(self.stdout, LeaveAlternateScreen, cursor::Show)?;
        self.is_active = false;
        Ok(())
    }

    /// Check if the screen is currently active (raw mode enabled).
    pub fn is_active(&self) -> bool {
        self.is_active
    }

    /// Clear the screen content.
    pub fn clear(&mut self) -> io::Result<()> {
        if !self.is_active {
            return Ok(());
        }
        execute!(self.stdout, terminal::Clear(terminal::ClearType::All))
    }
}

impl Default for TerminalScreen {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for TerminalScreen {
    fn drop(&mut self) {
        let _ = self.teardown();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_terminal_screen_creation() {
        let screen = TerminalScreen::new();
        assert!(!screen.is_active());
        #[cfg(unix)]
        {
            assert!(screen.sigwinch_id.is_none());
            assert!(screen.sigint_id.is_none());
            assert!(screen.sigterm_id.is_none());
        }
    }

    #[test]
    fn test_terminal_screen_default() {
        let screen = TerminalScreen::default();
        assert!(!screen.is_active());
    }

    #[test]
    fn test_resize_flag_initial() {
        let screen = TerminalScreen::new();
        assert!(!screen.check_resize());
    }

    #[test]
    fn test_setup_idempotent() {
        let mut screen = TerminalScreen::new();
        let result1 = screen.setup();
        if result1.is_ok() {
            assert!(screen.is_active());
            let result2 = screen.setup();
            assert!(result2.is_ok());
            assert!(screen.is_active());
        } else {
            assert!(!screen.is_active());
        }
    }

    #[test]
    fn test_teardown_idempotent() {
        let mut screen = TerminalScreen::new();
        let result1 = screen.teardown();
        assert!(result1.is_ok());
        assert!(!screen.is_active());
        let result2 = screen.teardown();
        assert!(result2.is_ok());
        assert!(!screen.is_active());
    }
}