lib/harlock/terminal/caps.ex

defmodule Harlock.Terminal.Caps do
  @moduledoc false
  # Terminal capability detection. Called once as a function from the
  # AppSupervisor's init/1, result threaded into Writer/Reader as start args.
  #
  # v0.1 is env-only: $TERM, $TERM_PROGRAM, $COLORTERM. DA/DA2 escape probes
  # need an async-read-with-timeout primitive that :file.read does not give
  # us, so they wait for v0.2 when the reader switches to a port-based
  # implementation or we move probes to a stty min/time toggle.

  defstruct [
    :term,
    :term_program,
    :colorterm,
    :colors,
    :bracketed_paste,
    :mouse,
    :kitty_keyboard
  ]

  @type color_depth :: :mono | :ansi16 | :ansi256 | :truecolor

  @type t :: %__MODULE__{
          term: String.t(),
          term_program: String.t(),
          colorterm: String.t(),
          colors: color_depth(),
          bracketed_paste: boolean(),
          mouse: boolean(),
          kitty_keyboard: boolean()
        }

  @spec detect() :: t()
  def detect do
    term = System.get_env("TERM") || ""
    colorterm = System.get_env("COLORTERM") || ""
    term_program = System.get_env("TERM_PROGRAM") || ""

    %__MODULE__{
      term: term,
      term_program: term_program,
      colorterm: colorterm,
      colors: classify_colors(term, colorterm),
      bracketed_paste: supports_bracketed_paste?(term, term_program),
      mouse: supports_mouse?(term),
      kitty_keyboard: term_program == "kitty" or term == "xterm-kitty"
    }
  end

  defp classify_colors(_term, colorterm) when colorterm in ["truecolor", "24bit"], do: :truecolor

  defp classify_colors(term, _) do
    cond do
      term == "" -> :mono
      term in ["dumb"] -> :mono
      String.contains?(term, "256color") -> :ansi256
      String.contains?(term, "direct") -> :truecolor
      String.starts_with?(term, "xterm") -> :ansi16
      String.starts_with?(term, "screen") -> :ansi16
      String.starts_with?(term, "tmux") -> :ansi16
      String.starts_with?(term, "rxvt") -> :ansi16
      true -> :ansi16
    end
  end

  defp supports_bracketed_paste?(term, term_program) do
    term_program in ["iTerm.app", "WezTerm", "Alacritty", "vscode", "Apple_Terminal", "ghostty"] or
      String.starts_with?(term, "xterm") or
      String.starts_with?(term, "screen") or
      String.starts_with?(term, "tmux") or
      term == "xterm-kitty"
  end

  defp supports_mouse?(term) do
    # We enable mouse on startup regardless and tolerate non-response; this is
    # just a hint for the runtime about whether to bother.
    term != "" and term != "dumb"
  end
end