quvyta-framework 0.1.35

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, lock};

/// 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.
    ///
    /// A process that relays the program's output through a
    /// [`tee`](super::TerminalBuilder::tee) listener to screens that attach takes the snapshot with
    /// [`snapshot_then`](Self::snapshot_then) instead, so no chunk falls between the snapshot and
    /// the relay.
    #[must_use]
    pub fn snapshot(&self) -> Vec<u8> {
        snapshot(&self.parser())
    }

    /// Takes the [`snapshot`](Self::snapshot) and hands it to `attach` while no chunk of the
    /// program's output is on its way from the [`tee`](super::TerminalBuilder::tee) listener to
    /// this session's screen, and returns what `attach` returns.
    ///
    /// The listener hears each chunk before the screen has it. A screen that attaches with a plain
    /// `snapshot` in that moment finds the chunk in neither the snapshot nor its relay, and lacks
    /// it until the program draws that part again. Here every chunk is either in the snapshot or
    /// heard after `attach` returns, so a relay that `attach` starts misses nothing:
    ///
    /// ```no_run
    /// use std::io::{self, Write};
    /// use std::sync::{Arc, Mutex};
    /// use qframe::widgets::TerminalSession;
    ///
    /// let relay: Arc<Mutex<Option<io::PipeWriter>>> = Arc::default();
    /// let heard = Arc::clone(&relay);
    /// let session = TerminalSession::builder("htop")
    ///     .tee(move |bytes| {
    ///         if let Some(pipe) = heard.lock().unwrap().as_mut() {
    ///             let _ = pipe.write_all(bytes);
    ///         }
    ///     })
    ///     .spawn()?;
    ///
    /// // A screen attaches: it reads from a pipe that starts with the snapshot and goes on with
    /// // what follows. It reads before the snapshot is written, which can be more than a pipe holds.
    /// let (output, mut pipe) = io::pipe()?;
    /// # struct Back;
    /// # impl qframe::widgets::TerminalCarrier for Back {
    /// #     fn write(&mut self, _: &[u8]) -> io::Result<()> { Ok(()) }
    /// #     fn resize(&mut self, _: u16, _: u16) -> io::Result<()> { Ok(()) }
    /// #     fn end(&mut self) {}
    /// # }
    /// # let back_to_the_program = Back;
    /// let screen = TerminalSession::attached(output, back_to_the_program);
    /// session.snapshot_then(|snapshot| {
    ///     pipe.write_all(snapshot)?;
    ///     *relay.lock().unwrap() = Some(pipe);
    ///     io::Result::Ok(())
    /// })?;
    /// # Ok::<(), io::Error>(())
    /// ```
    ///
    /// The program's output waits while `attach` runs, so it only writes the snapshot out and sets
    /// the relay. Take any lock the listener takes inside `attach`, never around this call: the
    /// reading thread holds the wait while the listener runs, and a lock held around the call
    /// would have each wait for the other. The screen itself stays free, so a widget showing this
    /// session keeps drawing meanwhile.
    pub fn snapshot_then<T>(&self, attach: impl FnOnce(&[u8]) -> T) -> T {
        let _between = lock(&self.shared.feed);
        let bytes = self.snapshot();
        attach(&bytes)
    }
}

/// 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());
    }
}