Skip to main content

lib/ttycast/interactive.ex

defmodule TTYCast.Interactive do
  @moduledoc """
  Interactive current-terminal recorder.
  """

  alias Ghostty.{PTY, TTY}
  alias TTYCast.Writer

  @spec record([String.t()], keyword()) :: {:ok, map()} | {:error, term()}
  def record([cmd | args], opts) do
    path = Keyword.fetch!(opts, :path)
    {width, height} = TTY.size()
    width = Keyword.get(opts, :width, width)
    height = Keyword.get(opts, :height, height)

    input_policy = Keyword.get(opts, :input_policy, :redacted)

    with {:ok, writer} <-
           TTYCast.start_writer(
             path: path,
             width: width,
             height: height,
             input_policy: input_policy
           ),
         {:ok, pty} <- PTY.start_link(cmd: cmd, args: args, cols: width, rows: height),
         {:ok, tty} <-
           TTY.start_link(owner: self(), raw: true, backend: Keyword.get(opts, :backend, :auto)) do
      Writer.marker(writer, :process_started, %{command: [cmd | args], interactive?: true})
      loop(%{writer: writer, pty: pty, tty: tty, path: path, bytes: 0})
    end
  end

  def record([], _opts), do: {:error, :missing_command}

  defp loop(state) do
    receive do
      {:data, data} ->
        TTY.write(state.tty, data)
        Writer.output(state.writer, data)
        loop(%{state | bytes: state.bytes + byte_size(data)})

      {TTY, _tty, {:data, data}} ->
        PTY.write(state.pty, data)
        Writer.input(state.writer, data)
        loop(state)

      {TTY, _tty, {:resize, cols, rows}} ->
        PTY.resize(state.pty, cols, rows)
        Writer.resize(state.writer, cols, rows)
        loop(state)

      {TTY, _tty, :eof} ->
        PTY.close(state.pty)
        finish(state, 0)

      {:exit, status} ->
        finish(state, status)
    end
  end

  defp finish(state, status) do
    close_pty(state.pty)
    Writer.marker(state.writer, :process_exited, %{status: status})
    Writer.close(state.writer)
    GenServer.stop(state.tty)
    {:ok, %{path: state.path, status: status, bytes: state.bytes}}
  end

  defp close_pty(pty) do
    PTY.close(pty)
  catch
    :exit, _reason -> :ok
  end
end