innards 0.3.0

Inline terminal tools for Rust symbol navigation, editing, and paging
Documentation
use std::io::{self, Stdout, Write};

use anyhow::Result;
use crossterm::ExecutableCommand;
use crossterm::cursor::{MoveTo, Show};
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::backend::CrosstermBackend;
use ratatui::{Terminal, TerminalOptions, Viewport};

use super::MIN_HEIGHT;

pub(super) struct TerminalGuard {
    pub(super) terminal: Terminal<CrosstermBackend<Stdout>>,
    pub(super) mode: TerminalMode,
    clear_on_drop: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum TerminalMode {
    Inline,
    Fullscreen,
}

impl TerminalGuard {
    pub(super) fn enter(height: u16) -> Result<Self> {
        enable_raw_mode()?;
        let terminal = Self::new_inline_terminal(height)?;
        Ok(Self {
            terminal,
            mode: TerminalMode::Inline,
            clear_on_drop: true,
        })
    }

    pub(super) fn new_inline_terminal(height: u16) -> Result<Terminal<CrosstermBackend<Stdout>>> {
        let stdout = io::stdout();
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::with_options(
            backend,
            TerminalOptions {
                viewport: Viewport::Inline(height.max(MIN_HEIGHT)),
            },
        )?;
        Ok(terminal)
    }

    pub(super) fn new_fullscreen_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
        let stdout = io::stdout();
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::with_options(
            backend,
            TerminalOptions {
                viewport: Viewport::Fullscreen,
            },
        )?;
        Ok(terminal)
    }

    pub(super) fn resize(&mut self, height: u16, anchor_y: u16) -> Result<()> {
        debug_assert_eq!(self.mode, TerminalMode::Inline);
        self.terminal.clear()?;
        io::stdout().execute(MoveTo(0, anchor_y))?;
        self.terminal = Self::new_inline_terminal(height)?;
        Ok(())
    }

    pub(super) fn enter_fullscreen(&mut self) -> Result<()> {
        if self.mode == TerminalMode::Fullscreen {
            return Ok(());
        }
        self.terminal.clear()?;
        io::stdout().execute(EnterAlternateScreen)?;
        self.terminal = Self::new_fullscreen_terminal()?;
        self.terminal.clear()?;
        self.mode = TerminalMode::Fullscreen;
        Ok(())
    }

    pub(super) fn leave_fullscreen(&mut self, height: u16) -> Result<()> {
        if self.mode == TerminalMode::Inline {
            return Ok(());
        }
        self.terminal.clear()?;
        io::stdout().execute(LeaveAlternateScreen)?;
        self.terminal = Self::new_inline_terminal(height)?;
        self.terminal.clear()?;
        self.mode = TerminalMode::Inline;
        Ok(())
    }

    pub(super) fn preserve_on_drop(&mut self) {
        self.clear_on_drop = false;
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        if self.clear_on_drop {
            let _ = self.terminal.clear();
        }
        if self.mode == TerminalMode::Fullscreen {
            let _ = io::stdout().execute(LeaveAlternateScreen);
        }
        let _ = disable_raw_mode();
        let _ = self.terminal.show_cursor();
        let _ = io::stdout().execute(Show);
        let _ = io::stdout().flush();
    }
}