rnk 0.19.3

A React-like declarative terminal UI framework for Rust, inspired by Ink and Bubbletea
Documentation
//! Terminal control helpers extracted from App orchestration.

use std::sync::Arc;

use super::Terminal;
use super::registry::{AppRuntime, ModeSwitch, Printable};
use super::render_to_string::render_to_string;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ResizeAction {
    repaint: bool,
    clear_screen: bool,
}

fn resize_action(
    last_width: u16,
    last_height: u16,
    new_width: u16,
    new_height: u16,
    alt_screen: bool,
) -> ResizeAction {
    let changed = new_width != last_width || new_height != last_height;

    ResizeAction {
        repaint: changed,
        clear_screen: changed && alt_screen,
    }
}

/// Handles terminal mode switching, terminal commands, and inline println rendering.
pub(crate) struct TerminalController;

impl TerminalController {
    pub(crate) fn handle_mode_switch(
        terminal: &mut Terminal,
        runtime: &Arc<AppRuntime>,
        mode_switch: ModeSwitch,
    ) -> std::io::Result<()> {
        match mode_switch {
            ModeSwitch::EnterAltScreen => {
                if !terminal.is_alt_screen() {
                    terminal.switch_to_alt_screen()?;
                    runtime.set_alt_screen_state(true);
                    terminal.repaint();
                }
            }
            ModeSwitch::ExitAltScreen => {
                if terminal.is_alt_screen() {
                    terminal.switch_to_inline()?;
                    runtime.set_alt_screen_state(false);
                    terminal.repaint();
                }
            }
        }
        Ok(())
    }

    pub(crate) fn handle_terminal_cmd(
        terminal: &mut Terminal,
        runtime: &Arc<AppRuntime>,
        cmd: crate::cmd::TerminalCmd,
        last_width: &mut u16,
        last_height: &mut u16,
    ) -> std::io::Result<()> {
        use crate::cmd::TerminalCmd;
        use crossterm::{cursor, execute, terminal as ct};
        use std::io::stdout;

        match cmd {
            TerminalCmd::ClearScreen => {
                if terminal.is_alt_screen() {
                    execute!(
                        stdout(),
                        ct::Clear(ct::ClearType::All),
                        cursor::MoveTo(0, 0)
                    )?;
                } else {
                    terminal.clear()?;
                }
            }
            TerminalCmd::HideCursor => {
                execute!(stdout(), cursor::Hide)?;
            }
            TerminalCmd::ShowCursor => {
                execute!(stdout(), cursor::Show)?;
            }
            TerminalCmd::SetWindowTitle(title) => {
                execute!(stdout(), ct::SetTitle(&title))?;
            }
            TerminalCmd::WindowSize => {
                // This triggers a resize check on next render.
                *last_width = 0;
                *last_height = 0;
            }
            TerminalCmd::EnterAltScreen => {
                if !terminal.is_alt_screen() {
                    terminal.switch_to_alt_screen()?;
                    runtime.set_alt_screen_state(true);
                    terminal.repaint();
                }
            }
            TerminalCmd::ExitAltScreen => {
                if terminal.is_alt_screen() {
                    terminal.switch_to_inline()?;
                    runtime.set_alt_screen_state(false);
                    terminal.repaint();
                }
            }
            TerminalCmd::EnableMouse => {
                crate::hooks::use_mouse::set_mouse_enabled(true);
                execute!(stdout(), crossterm::event::EnableMouseCapture)?;
            }
            TerminalCmd::DisableMouse => {
                crate::hooks::use_mouse::set_mouse_enabled(false);
                execute!(stdout(), crossterm::event::DisableMouseCapture)?;
            }
            TerminalCmd::EnableBracketedPaste => {
                execute!(stdout(), crossterm::event::EnableBracketedPaste)?;
            }
            TerminalCmd::DisableBracketedPaste => {
                execute!(stdout(), crossterm::event::DisableBracketedPaste)?;
            }
        }
        Ok(())
    }

    pub(crate) fn handle_println_messages(
        terminal: &mut Terminal,
        messages: &[Printable],
    ) -> std::io::Result<()> {
        // Println only works in inline mode.
        if terminal.is_alt_screen() {
            return Ok(());
        }

        // Get terminal width for rendering elements.
        let (width, _) = Terminal::size().unwrap_or((80, 24));

        for message in messages {
            match message {
                Printable::Text(text) => {
                    terminal.println(text)?;
                }
                Printable::Element(element) => {
                    let rendered = render_to_string(element, width);
                    terminal.println(&rendered)?;
                }
            }
        }

        terminal.repaint();
        Ok(())
    }

    pub(crate) fn handle_resize(
        terminal: &mut Terminal,
        last_width: &mut u16,
        last_height: &mut u16,
        new_width: u16,
        new_height: u16,
    ) {
        use crossterm::cursor::MoveTo;
        use crossterm::execute;
        use crossterm::terminal::{Clear, ClearType};
        use std::io::stdout;

        let action = resize_action(
            *last_width,
            *last_height,
            new_width,
            new_height,
            terminal.is_alt_screen(),
        );

        if action.repaint {
            if action.clear_screen {
                let _ = execute!(stdout(), MoveTo(0, 0), Clear(ClearType::All));
            }
            // Inline mode: repaint only to avoid clearing scrollback.
            terminal.repaint();
        }

        *last_width = new_width;
        *last_height = new_height;
    }
}

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

    #[test]
    fn test_resize_action_no_change_is_noop() {
        assert_eq!(
            resize_action(80, 24, 80, 24, true),
            ResizeAction {
                repaint: false,
                clear_screen: false
            }
        );
    }

    #[test]
    fn test_resize_action_width_shrink_repaints_inline_without_clear() {
        assert_eq!(
            resize_action(100, 24, 80, 24, false),
            ResizeAction {
                repaint: true,
                clear_screen: false
            }
        );
    }

    #[test]
    fn test_resize_action_width_shrink_clears_alt_screen() {
        assert_eq!(
            resize_action(100, 24, 80, 24, true),
            ResizeAction {
                repaint: true,
                clear_screen: true
            }
        );
    }
}