termcinema-engine 0.1.0

🧠 Core typewriter-style terminal animation engine (SVG renderer) for termcinema
Documentation
use crate::core::ScriptBlock;
use crate::parser::patterns::match_prompt_line;

#[derive(Debug)]
enum ParseState {
    Start,
    ExpectOutput,
}

/// Smartly analyzes the structure of shell transcripts: Prompt + Command (same line) + Output
pub(crate) fn parse_script_blocks(input: &str) -> Vec<ScriptBlock> {
    let mut blocks = Vec::new();
    let mut current_output: Vec<String> = Vec::new();
    let mut state = ParseState::Start;
    let mut has_seen_prompt = false; // 🆕 Tracks whether a prompt has been encountered

    for line in input.lines() {
        let trimmed = line.trim_end();

        if trimmed.is_empty() {
            // Retain empty lines as part of output if inside output context or before first prompt
            if matches!(state, ParseState::ExpectOutput) || !has_seen_prompt {
                current_output.push("".to_string());
            }
            continue;
        }

        if let Some((prompt, command)) = match_prompt_line(trimmed) {
            // 🧠 If we encounter a prompt for the first time and have buffered output → push it
            if !has_seen_prompt && !current_output.is_empty() {
                blocks.push(ScriptBlock::Output(current_output.clone()));
                current_output.clear();
            }

            // Flush any pending output before a new prompt
            if !current_output.is_empty() {
                blocks.push(ScriptBlock::Output(current_output.clone()));
                current_output.clear();
            }

            blocks.push(ScriptBlock::Prompt(prompt.to_string()));

            if !command.is_empty() {
                blocks.push(ScriptBlock::Command(command.to_string()));
                state = ParseState::ExpectOutput;
            } else {
                state = ParseState::Start;
            }

            has_seen_prompt = true;
            continue;
        }

        // Standard output or pre-prompt connection logs
        if matches!(state, ParseState::ExpectOutput) || !has_seen_prompt {
            current_output.push(trimmed.to_string());
        }
    }

    // Push trailing output if present
    if !current_output.is_empty() {
        blocks.push(ScriptBlock::Output(current_output));
    }

    blocks
}