tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use std::io::{self, BufWriter, Stdout};

use crate::app::context::SurfaceMode;
use crate::style::query_keyboard_enhancement_support;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::execute;
use crossterm::style::Print;
use ratatui::backend::CrosstermBackend;
use ratatui::{TerminalOptions, Viewport};

use super::terminal_handoff::reset_handoff_state_for_terminal_restore;
use super::terminal_transition::{
    CrosstermTransitionExecutor, enter_plan, execute_plan, execute_plan_with_rollback, exit_plan,
};

#[cfg(feature = "image")]
use super::image_support;

type TerminalWriter = BufWriter<Stdout>;
const TERMINAL_BUFFER_CAPACITY: usize = 64 * 1024;

pub(crate) type Terminal = ratatui::Terminal<CrosstermBackend<TerminalWriter>>;

fn buffered_stdout() -> TerminalWriter {
    BufWriter::with_capacity(TERMINAL_BUFFER_CAPACITY, io::stdout())
}

pub(crate) fn create_inline_terminal(height: u16) -> io::Result<Terminal> {
    let backend = CrosstermBackend::new(buffered_stdout());
    let options = TerminalOptions {
        viewport: Viewport::Inline(height.max(1)),
    };
    ratatui::Terminal::with_options(backend, options)
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct SurfaceTerminalPolicy {
    pub(crate) uses_alternate_screen: bool,
    pub(crate) disable_auto_wrap: bool,
    pub(crate) clear_on_start: bool,
}

pub(crate) fn surface_terminal_policy(surface_mode: SurfaceMode) -> SurfaceTerminalPolicy {
    let inline_disable_auto_wrap = matches!(surface_mode, SurfaceMode::InlineEphemeral { .. });
    SurfaceTerminalPolicy {
        uses_alternate_screen: !surface_mode.is_inline(),
        disable_auto_wrap: inline_disable_auto_wrap,
        clear_on_start: surface_mode.clear_on_start(),
    }
}

pub(crate) struct DisableMouseAllMotion;

impl crossterm::Command for DisableMouseAllMotion {
    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
        f.write_str("\x1b[?1003l\x1b[?1006l")
    }

    #[cfg(windows)]
    fn execute_winapi(&self) -> io::Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn is_ansi_code_supported(&self) -> bool {
        true
    }
}

pub(crate) struct DisableAutoWrap;

impl crossterm::Command for DisableAutoWrap {
    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
        f.write_str("\x1b[?7l")
    }

    #[cfg(windows)]
    fn execute_winapi(&self) -> io::Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn is_ansi_code_supported(&self) -> bool {
        true
    }
}

pub(crate) struct EnableAutoWrap;

impl crossterm::Command for EnableAutoWrap {
    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
        f.write_str("\x1b[?7h")
    }

    #[cfg(windows)]
    fn execute_winapi(&self) -> io::Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn is_ansi_code_supported(&self) -> bool {
        true
    }
}

struct EnableMouseMotionTracking;

impl crossterm::Command for EnableMouseMotionTracking {
    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
        f.write_str("\x1b[?1003h")
    }

    #[cfg(windows)]
    fn execute_winapi(&self) -> io::Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn is_ansi_code_supported(&self) -> bool {
        true
    }
}

struct DisableMouseMotionTracking;

impl crossterm::Command for DisableMouseMotionTracking {
    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
        f.write_str("\x1b[?1003l")
    }

    #[cfg(windows)]
    fn execute_winapi(&self) -> io::Result<()> {
        Ok(())
    }

    #[cfg(windows)]
    fn is_ansi_code_supported(&self) -> bool {
        true
    }
}

pub(crate) fn set_mouse_all_motion_enabled(
    writer: &mut impl std::io::Write,
    enabled: bool,
) -> io::Result<()> {
    if enabled {
        execute!(writer, EnableMouseMotionTracking)?;
    } else {
        execute!(writer, DisableMouseMotionTracking)?;
        execute!(
            writer,
            Print("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h")
        )?;
    }
    Ok(())
}

pub(crate) fn set_mouse_capture_enabled(
    writer: &mut impl std::io::Write,
    enabled: bool,
) -> io::Result<()> {
    if enabled {
        execute!(writer, EnableMouseCapture)?;
    } else {
        execute!(writer, DisableMouseAllMotion, DisableMouseCapture)?;
    }
    Ok(())
}

pub(crate) struct TerminalGuard {
    stdout: Stdout,
    policy: SurfaceTerminalPolicy,
    keyboard_enhancement: bool,
}

impl TerminalGuard {
    pub(crate) fn enter(
        surface_mode: SurfaceMode,
        mouse_enabled: bool,
    ) -> io::Result<(Terminal, Self)> {
        let policy = surface_terminal_policy(surface_mode);
        let mut stdout = io::stdout();
        let keyboard_enhancement = query_keyboard_enhancement_support().unwrap_or(false);
        let plan = enter_plan(policy, mouse_enabled, keyboard_enhancement);
        let mut executor = CrosstermTransitionExecutor::new(stdout);
        execute_plan_with_rollback(&mut executor, &plan)?;
        stdout = executor.into_inner();

        #[cfg(feature = "image")]
        image_support::init_image_picker();
        let terminal = if policy.uses_alternate_screen {
            let backend =
                CrosstermBackend::new(BufWriter::with_capacity(TERMINAL_BUFFER_CAPACITY, stdout));
            match ratatui::Terminal::new(backend) {
                Ok(terminal) => terminal,
                Err(err) => {
                    rollback_entered_terminal(policy, keyboard_enhancement);
                    return Err(err);
                }
            }
        } else {
            let height = match surface_mode {
                SurfaceMode::InlineEphemeral { height }
                | SurfaceMode::InlineTranscript { height, .. } => height,
                SurfaceMode::Fullscreen => 1,
            };
            match create_inline_terminal(height) {
                Ok(terminal) => terminal,
                Err(err) => {
                    rollback_entered_terminal(policy, keyboard_enhancement);
                    return Err(err);
                }
            }
        };

        let guard = Self {
            stdout: io::stdout(),
            policy,
            keyboard_enhancement,
        };
        Ok((terminal, guard))
    }
}

fn rollback_entered_terminal(policy: SurfaceTerminalPolicy, keyboard_enhancement: bool) {
    let mut stdout = io::stdout();
    let plan = exit_plan(policy, keyboard_enhancement);
    let mut executor = CrosstermTransitionExecutor::new(&mut stdout);
    let _ = execute_plan(&mut executor, &plan);
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let plan = exit_plan(self.policy, self.keyboard_enhancement);
        let mut executor = CrosstermTransitionExecutor::new(&mut self.stdout);
        let _ = execute_plan(&mut executor, &plan);
    }
}

pub(crate) fn restore_terminal_on_panic(surface_mode: SurfaceMode) {
    let policy = surface_terminal_policy(surface_mode);
    let mut stdout = io::stdout();
    let keyboard_enhancement = query_keyboard_enhancement_support().unwrap_or(false);
    let plan = exit_plan(policy, keyboard_enhancement);
    let mut executor = CrosstermTransitionExecutor::new(&mut stdout);
    let _ = execute_plan(&mut executor, &plan);
    reset_handoff_state_for_terminal_restore();
}

#[cfg(test)]
pub(crate) fn assert_inline_surface_internal_wrap_policy_is_opaque() {
    use crate::app::context::{InlineStartupPolicy, SurfaceMode};

    let fullscreen = surface_terminal_policy(SurfaceMode::Fullscreen);
    assert!(fullscreen.uses_alternate_screen);
    assert!(!fullscreen.disable_auto_wrap);

    let ephemeral = surface_terminal_policy(SurfaceMode::InlineEphemeral { height: 3 });
    assert!(!ephemeral.uses_alternate_screen);
    assert!(ephemeral.disable_auto_wrap);

    let transcript = surface_terminal_policy(SurfaceMode::InlineTranscript {
        height: 3,
        startup: InlineStartupPolicy::PreserveHost,
    });
    assert!(!transcript.uses_alternate_screen);
    assert!(!transcript.disable_auto_wrap);
}