termcinema-engine 0.1.0

🧠 Core typewriter-style terminal animation engine (SVG renderer) for termcinema
Documentation
use crate::CommandGroup;
use crate::core::ScriptBlock::{self, Command, Output, Prompt};

/// Groups a flat list of [`ScriptBlock`]s into structured [`CommandGroup`]s.
///
/// A `CommandGroup` typically consists of:
/// - a `Prompt`
/// - an optional `Command`
/// - an optional `Output`
///
/// The grouping rules follow a shell-like REPL structure:
/// - If the first block is an `Output`, treat it as initial banner output.
/// - Then, group every `Prompt` followed by a `Command`, and possibly an `Output`.
/// - If a `Prompt` appears without a `Command`, still create a group.
///
/// This function ensures the final data structure reflects
/// how terminal sessions typically unfold visually.
pub(crate) fn group_blocks(blocks: &[ScriptBlock]) -> Vec<CommandGroup> {
    let mut result = Vec::new();
    let mut i = 0;

    // 🆕 Handle initial output block before any prompt appears
    if !blocks.is_empty() {
        if let Output(initial_output) = &blocks[0] {
            result.push(CommandGroup {
                prompt: String::new(),
                command: String::new(),
                output: initial_output.clone(),
            });
            i = 1; // Skip the already-handled output block
        }
    }

    // Main grouping loop
    while i < blocks.len() {
        match &blocks[i] {
            Prompt(prompt) => {
                // Check if next is a Command
                if i + 1 < blocks.len() {
                    if let Command(cmd) = &blocks[i + 1] {
                        // Prompt + Command 组合
                        let mut output = vec![];

                        // Check if a third block is Output
                        if i + 2 < blocks.len() {
                            if let Output(out) = &blocks[i + 2] {
                                output = out.clone();
                                i += 3; // Prompt + Command + Output
                            } else {
                                i += 2; // Prompt + Command only
                            }
                        } else {
                            i += 2; // Prompt + Command only
                        }

                        result.push(CommandGroup {
                            prompt: prompt.clone(),
                            command: cmd.clone(),
                            output,
                        });
                    } else {
                        // Prompt-only (no command)
                        result.push(CommandGroup {
                            prompt: prompt.clone(),
                            command: String::new(),
                            output: vec![],
                        });
                        i += 1;
                    }
                } else {
                    // Last block is a Prompt without anything after
                    result.push(CommandGroup {
                        prompt: prompt.clone(),
                        command: String::new(),
                        output: vec![],
                    });
                    i += 1;
                }
            }
            _ => {
                // Skip other types (should not occur at top level)
                i += 1;
            }
        }
    }

    result
}