[content] replay_file.py (python)
import pandas as pd
import curses
import time
import argparse
import math

def apply_change(content, offset, length, new_text):
    """Applies a text change to the content string."""
    # Ensure all components are strings
    content = str(content)
    new_text = str(new_text) if not pd.isna(new_text) else ""
    
    # Ensure offset and length are valid integers
    offset = int(offset)
    length = int(length)
    
    if offset > len(content):
        # Pad with spaces if offset is out of bounds
        content += ' ' * (offset - len(content))
        
    return content[:offset] + new_text + content[offset + length:]

def replay_trace(stdscr, filepath, speed_factor):
    """
    Main function to replay the coding trace in the terminal.
    """
    # --- Curses Setup ---
    curses.curs_set(0)  # Hide the actual cursor
    stdscr.nodelay(1)   # Make getch non-blocking
    
    # Use terminal default colors
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, curses.COLOR_WHITE, -1) # Status bar: white on default bg
    
    # --- Data Loading ---
    try:
        df = pd.read_csv(filepath).sort_values('Time').reset_index(drop=True)
    except FileNotFoundError:
        print(f"Error: The file '{filepath}' was not found.")
        return

    file_states = {}
    active_file = None
    paused = False
    
    # --- Main Replay Loop ---
    for i in range(len(df)):
        # --- Handle User Input ---
        key = stdscr.getch()
        if key == ord('q'):
            break
        elif key == ord(' '):
            paused = not paused
        elif key == curses.KEY_UP:
            speed_factor *= 1.5
        elif key == curses.KEY_DOWN:
            speed_factor /= 1.5

        if paused:
            height, width = stdscr.getmaxyx()
            pause_msg = "PAUSED"
            stdscr.addstr(height - 1, 0, pause_msg, curses.A_REVERSE)
            stdscr.refresh()
            while paused:
                time.sleep(0.1)
                key = stdscr.getch()
                if key == ord(' '):
                    paused = False
                elif key == ord('q'):
                    return

        # --- Process Event ---
        event = df.iloc[i]
        
        active_file = event['File']
        if active_file not in file_states:
            file_states[active_file] = ""
            
        # Apply the content change
        file_states[active_file] = apply_change(
            file_states[active_file],
            event['RangeOffset'],
            event['RangeLength'],
            event['Text']
        )
        
        # --- Render to Screen ---
        stdscr.clear()
        height, width = stdscr.getmaxyx()
        
        # Display file content
        content_to_show = file_states.get(active_file, "")
        lines = content_to_show.split('
')
        
        for row_idx, line in enumerate(lines):
            if row_idx < height - 2:  # Leave space for status bar and help
                stdscr.addstr(row_idx, 0, line[:width - 1])

        # Status Bar
        status_bar_text = (
            f"File: {active_file} | "
            f"Time: {event['Time']/1000:.1f}s | "
            f"Event: {event['Type']} | "
            f"Speed: {speed_factor:.1f}x"
        )
        stdscr.attron(curses.color_pair(1) | curses.A_REVERSE)
        stdscr.addstr(height - 2, 0, status_bar_text.ljust(width - 1))
        stdscr.attroff(curses.color_pair(1) | curses.A_REVERSE)

        # Help Text
        help_text = "PAUSE/PLAY [space] | FASTER [↑] | SLOWER [↓] | QUIT [q]"
        stdscr.addstr(height - 1, 0, help_text)

        stdscr.refresh()
        
        # --- Wait for Next Event ---
        if i + 1 < len(df):
            time_delta_ms = df.iloc[i+1]['Time'] - event['Time']
            sleep_duration_s = max(0, time_delta_ms / 1000.0)
            time.sleep(sleep_duration_s / speed_factor)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Replay coding traces from a CSV file in the terminal.",
        formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument("filepath", help="The path to the source CSV file.")
    parser.add_argument(
        "--speed", 
        type=float, 
        default=2.0, 
        help="Initial playback speed multiplier (e.g., 2.0 for 2x speed)."
    )
    args = parser.parse_args()

    # The curses.wrapper function handles terminal initialization and restoration
    curses.wrapper(replay_trace, args.filepath, args.speed)