quvyta-framework 0.1.34

A Rust framework for building terminal applications
//! The bytes that draw a terminal session's screen again from nothing: its scrollback, its screen,
//! the screen beneath when a program is on the alternate one, the cursor and the modes the
//! program turned on.

use super::terminal_notice::Heard;
use super::terminal_session::TerminalSession;

/// Leaves the alternate screen, putting back the cursor saved when it was entered.
const LEAVE_ALTERNATE: &[u8] = b"\x1b[?1049l";

/// Saves the cursor and enters the alternate screen, cleared.
const ENTER_ALTERNATE: &[u8] = b"\x1b[?1049h";

/// Ends a line of scrollback: attributes off, so the next line starts plain, and down a row.
const END_OF_LINE: &[u8] = b"\x1b[m\r\n";

/// Turns on the focus reports of xterm's mode 1004.
const FOCUS_REPORTS: &[u8] = b"\x1b[?1004h";

impl TerminalSession {
    /// Bytes that, given to an empty session with the same screen size, draw what this one
    /// draws: its scrollback, oldest line first, its screen with every colour and attribute, the
    /// screen beneath when the program is on the alternate one, the cursor, and the modes the
    /// program turned on, application keys, bracketed paste, mouse reporting, focus reports and
    /// the kitty keyboard flags. Taken while the session's screen is locked, so it is one moment
    /// of it.
    ///
    /// It is what an [attached](Self::attach) session is sent first, before the bytes the program
    /// writes after it: a screen that attaches to a program running elsewhere draws it as it was
    /// when it left, then follows it. Leaving the alternate screen later shows the screen beneath
    /// as it was, since that is drawn first.
    ///
    /// The whole scrollback the session keeps is sent, which its
    /// [`scrollback`](super::TerminalBuilder::scrollback) bounds; a line that wrapped arrives as a
    /// line of its own. What the program said about itself, its title and folder, is reported by
    /// its watch as it happens and is not repeated here.
    #[must_use]
    pub fn snapshot(&self) -> Vec<u8> {
        snapshot(&self.parser())
    }
}

/// The bytes of [`TerminalSession::snapshot`] for the screen `parser` holds.
pub(super) fn snapshot(parser: &vt100::Parser<Heard>) -> Vec<u8> {
    let live = parser.screen();
    let (rows, columns) = live.size();
    let alternate = live.alternate_screen();
    let heard = parser.callbacks();
    // A copy to read from, so the screen a person may be scrolling back through is left where it is.
    let mut copy = vt100::Parser::new(rows, columns, 0);
    *copy.screen_mut() = live.clone();
    if alternate {
        // The screen beneath is not shown while the program is on the alternate one; the copy
        // leaves it to read it.
        copy.process(LEAVE_ALTERNATE);
    }
    let main = copy.screen_mut();
    let mut out = Vec::new();

    // The oldest kept line is as far back as the view goes; each one is the top row there.
    main.set_scrollback(usize::MAX);
    let kept = main.scrollback();
    for back in (1..=kept).rev() {
        main.set_scrollback(back);
        if let Some(row) = main.rows_formatted(0, columns).next() {
            out.extend_from_slice(&row);
        }
        out.extend_from_slice(END_OF_LINE);
    }
    if kept > 0 {
        // Every line above ends with a line break, and the screen scrolls a line into the
        // scrollback only for a break on its last row: as many more as the screen has rows less
        // one push all of them up, and the screen is drawn over the blank rows left.
        for _ in 1..rows {
            out.extend_from_slice(b"\r\n");
        }
    }
    main.set_scrollback(0);
    out.extend_from_slice(&main.contents_formatted());
    push_keyboard(&mut out, heard.keyboard.pushed(false));
    if alternate {
        // Saved on entering, so leaving later puts the cursor back where the screen beneath had it.
        out.extend_from_slice(&main.cursor_state_formatted());
        out.extend_from_slice(ENTER_ALTERNATE);
        out.extend_from_slice(&live.contents_formatted());
        push_keyboard(&mut out, heard.keyboard.pushed(true));
    }
    out.extend_from_slice(&live.input_mode_formatted());
    if heard.focus.reporting() {
        out.extend_from_slice(FOCUS_REPORTS);
    }
    out.extend_from_slice(&live.cursor_state_formatted());
    out
}

/// Pushes the kitty keyboard flags `flags`, oldest first, on the screen in use.
fn push_keyboard(out: &mut Vec<u8>, flags: &[u16]) {
    for flags in flags {
        out.extend_from_slice(format!("\x1b[>{flags}u").as_bytes());
    }
}