tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
//! Byte-frame builders for input that must reach the child PTY as a single
//! atomic write.
//!
//! The helpers here ([`bracketed_paste`](crate::frame::bracketed_paste) and
//! [`focus_report`](crate::frame::focus_report)) produce one
//! contiguous `Vec<u8>` / `&[u8]` per call. Correctness depends on that buffer
//! being delivered to the child's parser without being interleaved with other
//! writes. An `\x1b[200~` start marker followed by bytes from a different
//! paste corrupts parser state. Callers must deliver each returned frame as one
//! write.

/// CSI sequence that opens a bracketed-paste block (`ESC [ 2 0 0 ~`).
pub const BRACKETED_PASTE_START: &[u8] = b"\x1b[200~";

/// CSI sequence that closes a bracketed-paste block (`ESC [ 2 0 1 ~`).
pub const BRACKETED_PASTE_END: &[u8] = b"\x1b[201~";

/// Build the byte frame to send for a paste.
///
/// When `bracketed` is true, wraps the sanitized payload between
/// [`BRACKETED_PASTE_START`] and [`BRACKETED_PASTE_END`] so the child's
/// parser can distinguish pasted content from typed keystrokes. When false,
/// returns the sanitized payload bytes alone.
///
/// The payload is sanitized in both modes before assembly: any embedded
/// [`BRACKETED_PASTE_START`] or [`BRACKETED_PASTE_END`] sequence in `text`
/// is stripped, and C0 control bytes (`0x00..=0x1F`) other than `\t`, `\n`,
/// and `\r` are dropped. This keeps a hostile or malformed payload from
/// closing the bracketed region early or smuggling raw escape sequences
/// into the receiver, regardless of which mode the child requested.
///
/// When `bracketed` is false, every `\n` byte is rewritten to `\r`. Modern
/// shells treat the receiving end of a paste as typed input and only commit
/// a line on `\r`; an `\n` would be inserted as an extra blank line instead.
/// This matches the [xterm rule][xterm-ctlseqs] and applies unconditionally,
/// so `\r\n` is rewritten to `\r\r` and `\n\n` to `\r\r`. Inside a bracketed
/// block the receiver knows the content is pasted text and treats `\n` as
/// part of the payload, so the LF passes through unchanged.
///
/// [xterm-ctlseqs]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
///
/// The returned `Vec<u8>` must be delivered to the PTY in a single write to
/// preserve parser state; see the module docs.
pub fn bracketed_paste(text: &str, bracketed: bool) -> Vec<u8> {
    let bytes = text.as_bytes();
    let mut frame =
        Vec::with_capacity(BRACKETED_PASTE_START.len() + bytes.len() + BRACKETED_PASTE_END.len());
    if bracketed {
        frame.extend_from_slice(BRACKETED_PASTE_START);
    }
    let mut i = 0;
    while i < bytes.len() {
        let rem = &bytes[i..];
        if rem.starts_with(BRACKETED_PASTE_START) {
            i += BRACKETED_PASTE_START.len();
            continue;
        }
        if rem.starts_with(BRACKETED_PASTE_END) {
            i += BRACKETED_PASTE_END.len();
            continue;
        }
        let b = bytes[i];
        if b >= 0x20 || matches!(b, b'\t' | b'\n' | b'\r') {
            let out = if !bracketed && b == b'\n' { b'\r' } else { b };
            frame.push(out);
        }
        i += 1;
    }
    if bracketed {
        frame.extend_from_slice(BRACKETED_PASTE_END);
    }
    frame
}

/// Build the focus-report byte sequence.
///
/// Returns `ESC [ I` when `gained` is true (focus gained) or `ESC [ O`
/// otherwise (focus lost). The child must have requested focus reporting
/// ([DECSET 1004][xterm-ctlseqs]) for these bytes to be meaningful.
///
/// [xterm-ctlseqs]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
pub fn focus_report(gained: bool) -> &'static [u8] {
    if gained { b"\x1b[I" } else { b"\x1b[O" }
}