defmodule Mix.Tasks.Raxol.Replay do @moduledoc """ Replays a recorded Raxol session from an asciicast (.cast) file. ## Usage mix raxol.replay demo.cast mix raxol.replay demo.cast --speed 2.0 mix raxol.replay demo.cast --speed 0.5 mix raxol.replay demo.cast --no-interactive ## Options * `--speed` / `-s` - Playback speed multiplier (default: 1.0). 2.0 plays at double speed, 0.5 at half speed. * `--max-delay` - Maximum pause between frames in seconds (default: 5.0). Prevents long idle gaps from stalling replay. * `--no-interactive` - Disable keyboard controls (simple playback). * `--info` - Print recording info without playing. ## Controls (interactive mode) * `space` - Pause / resume * `+` / `-` - Increase / decrease speed * `>` / `<` - Skip forward / backward 5 seconds * `0`..`9` - Jump to 0%-90% of recording * `q` / `ESC` - Quit Files are asciinema v2 format, compatible with `asciinema play`. """ use Mix.Task alias Raxol.Recording.{Asciicast, Player, Session} @shortdoc "Replay a recorded .cast session" @switches [ speed: :float, max_delay: :float, info: :boolean, interactive: :boolean ] @aliases [s: :speed] @impl Mix.Task def run(args) do case OptionParser.parse(args, strict: @switches, aliases: @aliases) do {opts, [path], _} -> unless File.exists?(path) do Mix.raise("File not found: #{path}") end if Keyword.get(opts, :info, false) do print_info(path) else replay(path, opts) end _ -> print_usage() end end defp replay(path, opts) do speed = Keyword.get(opts, :speed, 1.0) max_delay = Keyword.get(opts, :max_delay, 5.0) interactive = Keyword.get(opts, :interactive, true) speed_label = if speed != 1.0, do: " (#{speed}x)", else: "" Mix.shell().info([:cyan, "Replaying #{path}#{speed_label}...", :reset]) if interactive do Mix.shell().info("Controls: space=pause +/-=speed :seek q=quit\n") else Mix.shell().info("Press Ctrl+C to stop.\n") end Process.sleep(500) case Player.play(path, speed: speed, max_delay: max_delay, interactive: interactive ) do :ok -> :ok {:error, reason} -> Mix.shell().error("Replay failed: #{inspect(reason)}") end Mix.shell().info([:green, "\nReplay complete.", :reset]) end defp print_info(path) do case Asciicast.read(path) do {:ok, session} -> do_print_info(path, session) {:error, reason} -> Mix.raise("Failed to read #{path}: #{inspect(reason)}") end end defp do_print_info(path, session) do Mix.shell().info([:bright, "Recording: ", :reset, path]) Mix.shell().info(" Size: #{session.width}x#{session.height}") Mix.shell().info( " Duration: #{Float.round(Session.duration(session), 1)}s" ) Mix.shell().info(" Events: #{Session.event_count(session)}") if session.title do Mix.shell().info(" Title: #{session.title}") end if session.command do Mix.shell().info(" Command: #{session.command}") end Mix.shell().info(" Recorded: #{DateTime.to_string(session.started_at)}") env_term = get_in(session.env, ["TERM"]) || "unknown" env_shell = get_in(session.env, ["SHELL"]) || "unknown" Mix.shell().info(" Terminal: #{env_term}") Mix.shell().info(" Shell: #{env_shell}") end defp print_usage do Mix.shell().error("Usage: mix raxol.replay FILE [options]") Mix.shell().error("") Mix.shell().error("Options:") Mix.shell().error(" --speed FLOAT Playback speed (default: 1.0)") Mix.shell().error( " --max-delay SECS Max pause between frames (default: 5.0)" ) Mix.shell().error(" --no-interactive Disable keyboard controls") Mix.shell().error( " --info Print recording info without playing" ) Mix.shell().error("") Mix.shell().error("Example: mix raxol.replay demo.cast --speed 2.0") end end