Skip to main content

lib/ttycast/replay.ex

defmodule TTYCast.Replay do
  @moduledoc false

  alias TTYCast.Terminal

  @spec snapshot(TTYCast.Cast.t(), keyword()) :: {:ok, binary() | map()} | {:error, term()}
  def snapshot(cast, opts \\ []) do
    format = Keyword.get(opts, :format, :plain)
    target_us = target_time_us(cast, opts)
    max_scrollback = Keyword.get(opts, :max_scrollback, 20_000)

    with {:ok, terminal} <-
           Terminal.start(cast.header.width, cast.header.height, max_scrollback: max_scrollback) do
      try do
        {start_us, replay_events} = restore_keyframe(cast, terminal, target_us)

        replay_events
        |> Enum.filter(&(TTYCast.Events.event_time(&1) > start_us))
        |> replay_until(terminal, target_us)

        Terminal.snapshot(terminal, format)
      after
        Terminal.stop(terminal)
      end
    end
  end

  defp target_time_us(cast, opts) do
    cond do
      time_us = Keyword.get(opts, :time_us) -> time_us
      time_ms = Keyword.get(opts, :time_ms) -> time_ms * 1_000
      time = Keyword.get(opts, :time) -> trunc(time * 1_000_000)
      true -> Map.get(cast.index, :duration_us, 0)
    end
  end

  defp restore_keyframe(cast, terminal, target_us) do
    case nearest_keyframe(cast, target_us) do
      nil ->
        {0, TTYCast.Events.list(cast, end_us: target_us)}

      {keyframe_us, vt} ->
        :ok = Terminal.write(terminal, vt)
        {keyframe_us, TTYCast.Events.list(cast, start_us: keyframe_us, end_us: target_us)}
    end
  end

  defp nearest_keyframe(cast, target_us) do
    cast.index.chunks
    |> Enum.take_while(&(&1.start_t_us <= target_us))
    |> Enum.reverse()
    |> Enum.find_value(fn chunk_index ->
      case TTYCast.read_chunk!(cast, chunk_index) do
        %{keyframe: %{t_us: keyframe_us, vt: vt}} when keyframe_us <= target_us ->
          {keyframe_us, vt}

        _chunk ->
          nil
      end
    end)
  end

  defp replay_until(events, terminal, target_us) do
    events
    |> Enum.take_while(&(TTYCast.Events.event_time(&1) <= target_us))
    |> Enum.each(fn
      {:output, _t_us, bytes} -> Terminal.write(terminal, bytes)
      {:resize, _t_us, columns, rows} -> Terminal.resize(terminal, columns, rows)
      _event -> :ok
    end)
  end
end