lib/ex_term/router.ex

defmodule ExTerm.Router do
  @moduledoc """
  Plug that marshals ExTerm options to be put in the session store.

  by placing options in the session, it's possible for the initial html render
  and the socket-connected liveview to share common parameters.

  This module also provides the helper function `live_term/3` which wraps
  calling the plug (with options) and calling the `Phoenix.LiveView.Router.live/4`
  macro into one directive.
  """

  @behaviour Plug

  alias ExTerm.CSS
  require CSS

  @css_builtins CSS.builtins()

  @doc false
  def init([module | opts]) do
    new_opts =
      Keyword.update(opts, :css, :default, fn
        key when key in @css_builtins ->
          key

        result = {:css, {:priv, app, file_path}} ->
          css_content =
            app
            |> :code.priv_dir()
            |> Path.join(file_path)
            |> File.read!()

          old_content_map = Application.get_env(:ex_term, app, [])

          Application.put_env(:ex_term, app, [{file_path, css_content} | old_content_map],
            persistent: true
          )

          result

        other ->
          raise "unsupported CSS option #{inspect(other)}, must be a default atom (one of #{inspect(@css_builtins)}) or `{:priv, app, path}`"
      end)

    [module | new_opts]
  end

  @doc false
  def call(conn, [module | opts]) do
    Plug.Conn.put_session(conn, "exterm-backend", {module, opts})
  end

  @doc """
  creates an ExTerm live terminal route, inside of a Phoenix Router

  You must supply a path, and optionally a backend module.

  ### Basic Usage

  The following code creates a router with the default `ExTerm.TerminalBackend`
  backend:

  ```elixir
  defmodule MyAppWeb.Router do
    use MyAppWeb, :router
    import ExTerm.Router

    pipeline :browser do
      plug :accepts, ["html"]
      plug :fetch_session
      plug :fetch_live_flash
      plug :put_root_layout, {MyAppWeb.LayoutView, :root}
      plug :protect_from_forgery
      plug :put_secure_browser_headers
    end

    import ExTerm.Router

    scope "/live_term" do
      pipe_through :browser

      live_term "/terminal", pubsub_server: MyAppWeb.PubSub
    end
  end
  ```

  ### Alternative backends

  If you would like to specify an alternative backend, you may provide this as a
  second parameter, which must be a module.

  > #### Other backends {: .info}
  >
  > There are currently no other backends shipped with `:ex_term` but backends
  > to interface with other CLIs may be forthcoming.

  ```elixir
  scope "/live_term", MyBackend, pubsub_server: MyAppWeb.PubSub
  ```

  ### Options

  #### Required options

  - `:pubsub_server` A `Phoenix.Pubsub` server that will be used for Terminal backends to
    communicate back to the LiveView.

  #### Optional options

  - `:layout` A twople of postive integers which specify the dimensions of the
    viewable console space.
  - `:css` may be an atom or `{:priv, app, file_path}`.
    - if an atom, may be `:default` or `:bw`, which are the builtin css files.
    - if a `:priv` tuple, at compile-time it will search the priv directory of
      the supplied application at the specified `file_path` to obtain the css
      file.

  For documentation for backend-specific options, see `ExTerm.TerminalBackend`
  """
  defmacro live_term(route, module_or_opts, opts \\ [])

  defmacro live_term(route, module, opts) when is_atom(module) do
    id = :erlang.phash2(Map.take(__CALLER__, [:file, :line]))
    pipeline_name = :"exterm-pipeline-#{id}"

    quote do
      pipeline unquote(pipeline_name) do
        plug ExTerm.Router, [unquote(module) | unquote(opts)]
      end

      pipe_through unquote(pipeline_name)

      live unquote(route), ExTerm
    end
  end

  defmacro live_term(route, opts, []) when is_list(opts) do
    id = :erlang.phash2(Map.take(__CALLER__, [:file, :line]))
    pipeline_name = :"exterm-pipeline-#{id}"

    quote do
      pipeline unquote(pipeline_name) do
        plug ExTerm.Router, [ExTerm.TerminalBackend | unquote(opts)]
      end

      pipe_through unquote(pipeline_name)

      live unquote(route), ExTerm
    end
  end
end