rusty_repl 0.3.0

REPL library with customisable prompts and clean terminal management.
Documentation
//! Terminal lifecycle management for REPL sessions.
//!
//! [`TerminalManager`] handles switching the terminal into an alternate screen buffer,
//! hiding the cursor, and setting a temporary window title. When the REPL session ends,
//! it restores the terminal to its original state, ensuring a clean and consistent user experience.
//!
//! This abstraction isolates the details of terminal control so higher-level REPL components
//! can focus on input handling and evaluation logic.
//!
//! # Behavior
//! - On creation, the terminal enters an alternate screen and clears its contents.
//! - The cursor is hidden for a cleaner UI.
//! - The window title reflects the active REPL session.
//! - On drop or explicit `restore`, the terminal reverts to its previous state.
//!
//! # Example
//! ```no_run
//! use rusty_repl::{ReplConfig, TerminalManager};
//! use std::sync::Arc;
//!
//! fn main() -> std::io::Result<()> {
//!     let config = Arc::new(ReplConfig::default().with_title("Rusty REPL"));
//!     let mut term = TerminalManager::new(config.clone())?;
//!
//!     // perform REPL operations here
//!
//!     term.restore()?; // optional, automatically called on drop
//!     Ok(())
//! }
//! ```
//!
//! # Notes
//! - Uses [`crossterm`] for cross-platform terminal control.
//! - Restoration is guaranteed on drop, protecting against early exits or panics.
//! - Cleanup errors are logged to `stderr` but do not panic.
//!
//! # Design Rationale
//! Using the alternate screen buffer prevents the REPL from polluting the user’s main terminal
//! scrollback. This mirrors full-screen TUI programs (editors, pagers) while keeping the implementation lightweight.

use crossterm::{
    cursor::{Hide, MoveTo, Show},
    execute, queue,
    terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle},
};
use std::{
    io::{self, Stdout, Write, stdout},
    sync::Arc,
};

use crate::repl::config::ReplConfig;

/// Manages a terminal session in an alternate screen buffer.
///
/// Switches the terminal into an alternate screen, hides the cursor,
/// and sets a window title. Ensures restoration to the original state
/// when the session ends. Intended for REPL environments to provide
/// a clean, isolated terminal view.
pub struct TerminalManager {
    stdout: Stdout,
}

impl TerminalManager {
    /// Creates a new [`TerminalManager`] and enters the alternate screen.
    ///
    /// # Arguments
    ///
    /// * `config` - Shared REPL configuration containing the window title and other settings.
    ///
    /// # Errors
    ///
    /// Returns an `io::Error` if entering the alternate screen, hiding the cursor,
    /// clearing the screen, or setting the title fails.
    pub fn new(config: Arc<ReplConfig>) -> io::Result<Self> {
        let mut stdout = stdout();
        // Enter alternate screen
        execute!(
            &mut stdout,
            EnterAlternateScreen,
            Hide,                  // hide cursor
            Clear(ClearType::All), // clear alternate screen
            MoveTo(0, 0),          // move cursor to top-left
            SetTitle(&config.title())
        )?;
        stdout.flush()?;

        Ok(Self { stdout })
    }

    /// Restores the terminal to its original state.
    ///
    /// Leaves the alternate screen, shows the cursor, and flushes pending output.
    /// Normally called automatically on drop, but can be invoked manually.
    ///
    /// # Errors
    ///
    /// Returns an `io::Error` if flushing or sending terminal commands fails.
    pub fn restore(&mut self) -> io::Result<()> {
        queue!(
            &mut self.stdout,
            Show, // show cursor
            LeaveAlternateScreen,
        )?;

        self.stdout.flush()?;

        Ok(())
    }
}

impl Drop for TerminalManager {
    /// Ensures the terminal is restored when the `TerminalManager` is dropped.
    ///
    /// Any errors during cleanup are logged to `stderr` as a best-effort measure.
    fn drop(&mut self) {
        if let Err(err) = self.restore() {
            eprintln!("Failed to restore terminal on drop: {err}");
        }
    }
}