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