vtcode-tui 0.98.6

Reusable TUI primitives and session API for VT Code-style terminal interfaces
use std::io::{self, Write};

use anyhow::{Context, Result};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::crossterm::cursor::{
    MoveToColumn, RestorePosition, SavePosition, SetCursorStyle, Show,
};
use ratatui::crossterm::event;
use ratatui::crossterm::execute;
use ratatui::crossterm::terminal::{
    Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};

pub(super) struct TerminalModeGuard {
    label: String,
    raw_mode_enabled: bool,
    alternate_screen: bool,
    cursor_hidden: bool,
    cursor_position_saved: bool,
}

impl TerminalModeGuard {
    pub(super) fn new(label: &str) -> Self {
        Self {
            label: label.to_string(),
            raw_mode_enabled: false,
            alternate_screen: false,
            cursor_hidden: false,
            cursor_position_saved: false,
        }
    }

    pub(super) fn save_cursor_position(&mut self, stderr: &mut io::Stderr) {
        match execute!(stderr, SavePosition) {
            Ok(_) => {
                self.cursor_position_saved = true;
            }
            Err(error) => {
                tracing::debug!(%error, selector = %self.label, "failed to save cursor position");
            }
        }
    }

    pub(super) fn enable_raw_mode(&mut self) -> Result<()> {
        enable_raw_mode()
            .with_context(|| format!("Failed to enable raw mode for {} selector", self.label))?;
        self.raw_mode_enabled = true;
        Ok(())
    }

    pub(super) fn enter_alternate_screen(&mut self, stderr: &mut io::Stderr) -> Result<()> {
        execute!(stderr, EnterAlternateScreen).with_context(|| {
            format!(
                "Failed to enter alternate screen for {} selector",
                self.label
            )
        })?;
        self.alternate_screen = true;
        Ok(())
    }

    pub(super) fn hide_cursor(
        &mut self,
        terminal: &mut Terminal<CrosstermBackend<io::Stderr>>,
    ) -> Result<()> {
        terminal
            .hide_cursor()
            .with_context(|| format!("Failed to hide cursor for {} selector", self.label))?;
        self.cursor_hidden = true;
        Ok(())
    }

    pub(super) fn restore_with_terminal(
        &mut self,
        terminal: &mut Terminal<CrosstermBackend<io::Stderr>>,
    ) -> Result<()> {
        while let Ok(true) = event::poll(std::time::Duration::from_millis(0)) {
            let _ = event::read();
        }

        let _ = execute!(io::stderr(), MoveToColumn(0), Clear(ClearType::CurrentLine));

        if self.alternate_screen {
            execute!(terminal.backend_mut(), LeaveAlternateScreen).with_context(|| {
                format!(
                    "Failed to leave alternate screen after {} selector",
                    self.label
                )
            })?;
            self.alternate_screen = false;
        }

        if self.raw_mode_enabled {
            disable_raw_mode().with_context(|| {
                format!("Failed to disable raw mode after {} selector", self.label)
            })?;
            self.raw_mode_enabled = false;
        }

        if self.cursor_hidden {
            terminal
                .show_cursor()
                .with_context(|| format!("Failed to show cursor after {} selector", self.label))?;
            self.cursor_hidden = false;
        }

        let _ = execute!(terminal.backend_mut(), SetCursorStyle::DefaultUserShape);
        if self.cursor_position_saved {
            let _ = execute!(terminal.backend_mut(), RestorePosition);
            self.cursor_position_saved = false;
        }

        terminal.backend_mut().flush().ok();
        io::stderr().flush().ok();
        Ok(())
    }
}

impl Drop for TerminalModeGuard {
    fn drop(&mut self) {
        while let Ok(true) = event::poll(std::time::Duration::from_millis(0)) {
            let _ = event::read();
        }

        let _ = execute!(io::stderr(), MoveToColumn(0), Clear(ClearType::CurrentLine));

        if self.alternate_screen {
            let mut stderr = io::stderr();
            let _ = execute!(stderr, LeaveAlternateScreen);
            self.alternate_screen = false;
        }

        if self.raw_mode_enabled {
            let _ = disable_raw_mode();
            self.raw_mode_enabled = false;
        }

        if self.cursor_hidden {
            let mut stderr = io::stderr();
            let _ = execute!(stderr, SetCursorStyle::DefaultUserShape, Show);
            let _ = stderr.flush();
            self.cursor_hidden = false;
        }

        if self.cursor_position_saved {
            let mut stderr = io::stderr();
            let _ = execute!(stderr, RestorePosition);
            let _ = stderr.flush();
            self.cursor_position_saved = false;
        }

        let _ = io::stderr().flush();
    }
}