lib/kino/shorts.ex

defmodule Kino.Shorts do
  @moduledoc """
  Shortcuts for building Kinos.

  This module provides an easy to use Kino API and is meant to
  be imported into your notebooks:

      import Kino.Shorts

  """

  ## Outputs

  @doc """
  Renders a data table output for user-provided tabular data.

  The data must implement the `Table.Reader` protocol. This
  function is a wrapper around `Kino.DataTable.new/1`.

  ## Examples

      import Kino.Shorts

      data = [
        %{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
        %{id: 2, name: "Erlang", website: "https://www.erlang.org"}
      ]

      data_table(data)

  """
  @spec data_table(Table.Reader.t(), keyword()) :: Kino.DataTable.t()
  def data_table(tabular, opts \\ []), do: Kino.DataTable.new(tabular, opts)

  @doc """
  Renders an image of any given format.

  It is a wrapper around `Kino.Image.new/2`.

  ## Examples

      import Kino.Shorts
      content = File.read!("/path/to/image.jpeg")
      image(content, "image/jpeg")

  """
  @spec image(binary(), Kino.Image.common_image_type() | Kino.Image.mime_type()) :: Kino.Image.t()
  def image(content, type), do: Kino.Image.new(content, type)

  @doc """
  Renders an audio of any given format.

  It is a wrapper around `Kino.Audio.new/2`.

  ## Examples

      import Kino.Shorts
      content = File.read!("/path/to/audio.wav")
      audio(content, :wav)

  """
  @spec audio(binary(), Kino.Audio.common_audio_type() | Kino.Audio.mime_type()) :: Kino.Audio.t()
  def audio(content, type), do: Kino.Audio.new(content, type)

  @doc """
  Renders a video of any given format.

  It is a wrapper around `Kino.Audio.new/2`.

  ## Examples

      import Kino.Shorts
      content = File.read!("/path/to/video.mp4")
      video(content, :mp4)

  """
  @spec video(binary(), Kino.Video.common_video_type() | Kino.Video.mime_type()) :: Kino.Video.t()
  def video(content, type), do: Kino.Video.new(content, type)

  @doc """
  Renders a file download button.

  The given function is invoked to generate the file content whenever
  a download is requested.

  It is a wrapper around `Kino.Download.new/2`.

  ## Options

    * `:filename` - the default filename suggested for download.
      Defaults to `"download"`

    * `:label` - the button text. Defaults to the value of `:filename`
      if present and `"Download"` otherwise

  ## Examples

      download(fn ->
        "Example text"
      end)

      download(
        fn -> Jason.encode!(%{"foo" => "bar"}) end,
        filename: "data.json"
      )

      download(
        fn -> <<0, 1>> end,
        filename: "data.bin",
        label: "Binary data"
      )

  """
  @spec download((-> binary()), keyword()) :: Kino.Download.t()
  def download(content_fun, opts \\ []), do: Kino.Download.new(content_fun, opts)

  @doc ~S'''
  Renders Markdown content, in case you need richer text.

  It is a wrapper around `Kino.Markdown.new/1`.

  ## Examples

      import Kino.Shorts

      markdown("""
      # Example

      A regular Markdown file.

      ## Code

      ```elixir
      "Elixir" |> String.graphemes() |> Enum.frequencies()
      ```

      ## Table

      | ID | Name   | Website                 |
      | -- | ------ | ----------------------- |
      | 1  | Elixir | https://elixir-lang.org |
      | 2  | Erlang | https://www.erlang.org  |
      """)
  '''
  @spec markdown(String.t()) :: Kino.Markdown.t()
  def markdown(markdown), do: Kino.Markdown.new(markdown)

  @doc """
  Renders plain text content.

  It is similar to `markdown/1`, however doesn't interpret any markup.

  It is a wrapper around `Kino.Text.new/1`.

  ## Examples

      import Kino.Shorts
      text("Hello!")

  """
  @spec text(String.t()) :: Kino.Text.t()
  def text(text), do: Kino.Text.new(text)

  @doc ~S'''
  Renders Mermaid graphs.

  It is a wrapper around `Kino.Mermaid.new/1`.

  ## Examples

      import Kino.Shorts

      mermaid("""
      graph TD;
        A-->B;
        A-->C;
        B-->D;
        C-->D;
      """)
  '''
  @spec mermaid(String.t()) :: Kino.Mermaid.t()
  def mermaid(mermaid), do: Kino.Mermaid.new(mermaid)

  @doc """
  A placeholder for static outputs that can be dynamically updated.

  The frame can be updated with the `Kino.Frame` module API.
  Also see `Kino.animate/3`.

  ## Examples

      import Kino.Shorts
      frame = frame() |> Kino.render()

      for i <- 1..100 do
        Kino.Frame.render(frame, i)
        Process.sleep(50)
      end

  """
  @spec frame(keyword()) :: Kino.Frame.t()
  def frame(opts \\ []), do: Kino.Frame.new(opts)

  @doc """
  Displays arbitrarily nested data structure as a tree view.

  It is a wrapper around `Kino.Tree.new/1`.

  ## Examples

      import Kino.Shorts
      tree(Process.info(self()))

  """
  @spec tree(term()) :: Kino.Layout.t()
  def tree(tree), do: Kino.Tree.new(tree)

  @doc ~S'''
  Displays arbitrary static HTML.

  It is a wrapper around `Kino.HTML.new/1`.

  ## Examples

      import Kino.Shorts

      html("""
      <h3>Look!</h3>

      <p>I wrote this HTML from <strong>Kino</strong>!</p>
      """)

  '''
  @spec html(String.t()) :: Kino.HTML.t()
  def html(html), do: Kino.HTML.new(html)

  ## Layout

  @doc """
  Arranges outputs into separate tabs.

  It is a wrapper around `Kino.Layout.tabs/1`.

  ## Examples

      import Kino.Shorts

      data = [
        %{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
        %{id: 2, name: "Erlang", website: "https://www.erlang.org"}
      ]

      tabs([
        Table: data_table(data),
        Raw: data
      ])

  """
  @spec tabs(list({String.t() | atom(), term()})) :: Kino.Layout.t()
  defdelegate tabs(tabs), to: Kino.Layout

  @doc """
  Arranges outputs into a grid.

  Note that the grid does not support scrolling, it always squeezes
  the content, such that it does not exceed the page width.

  It is a wrapper around `Kino.Layout.grid/2`. See the linked function
  for all supported options.

  ## Options

    * `:columns` - the number of columns in the grid. Defaults to `1`

    * `:boxed` - whether the grid should be wrapped in a bordered box.
      Defaults to `false`

    * `:gap` - the amount of spacing between grid items in pixels.
      Defaults to `8`

  ## Examples

      import Kino.Shorts

      images =
        for path <- paths do
          path |> File.read!() |> image(:jpeg)
        end

      grid(images, columns: 3)

  """
  @spec grid(list(term()), keyword()) :: Kino.Layout.t()
  defdelegate grid(terms, opts \\ []), to: Kino.Layout

  ## Inputs

  @doc """
  Renders and reads a new text input.

  ## Options

    * `:default` - the initial input value. Defaults to `""`
  """
  @spec read_text(String.t(), keyword()) :: String.t() | nil
  def read_text(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.text(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new multiline text input.

  ## Options

    * `:default` - the initial input value. Defaults to `""`

    * `:monospace` - whether to use a monospace font inside the textarea.
      Defaults to `false`
  """
  @spec read_textarea(String.t(), keyword()) :: String.t() | nil
  def read_textarea(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.textarea(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new password input.

  ## Options

    * `:default` - the initial input value. Defaults to `""`
  """
  @spec read_password(String.t(), keyword()) :: String.t() | nil
  def read_password(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.password(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new number input.

  ## Options

    * `:default` - the initial input value. Defaults to `nil`
  """
  @spec read_number(String.t(), keyword()) :: number() | nil
  def read_number(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.number(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new URL input.

  ## Options

    * `:default` - the initial input value. Defaults to `nil`
  """
  @spec read_url(String.t(), keyword()) :: String.t() | nil
  def read_url(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.url(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new select input.

  The input expects a list of options in the form `[{value, label}]`,
  where `value` is an arbitrary term and `label` is a descriptive
  string.

  ## Options

    * `:default` - the initial input value. Defaults to the first
      value from the given list of options

  ## Examples

      read_select("Language", [en: "English", fr: "Français"])

      read_select("Language", [{1, "One"}, {2, "Two"}, {3, "Three"}])
  """
  @spec read_select(String.t(), list({value :: term(), label :: String.t()}), keyword()) ::
          String.t()
  def read_select(label, options, opts \\ [])
      when is_binary(label) and is_list(options) and is_list(opts) do
    Kino.Input.select(label, options, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new checkbox.

  ## Options

    * `:default` - the initial input value. Defaults to `false`
  """
  @spec read_checkbox(String.t(), keyword()) :: boolean()
  def read_checkbox(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.checkbox(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new slider input.

  ## Options

    * `:default` - the initial input value. Defaults to the
      minimum value

    * `:min` - the minimum value

    * `:max` - the maximum value

    * `:step` - the slider increment
  """
  @spec read_range(String.t(), keyword()) :: float()
  def read_range(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.range(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new datetime input.

  ## Options

    * `:default` - the initial input value. Defaults to `nil`

    * `:min` - the minimum datetime value (in UTC)

    * `:max` - the maximum datetime value (in UTC)
  """
  @spec read_utc_datetime(String.t(), keyword()) :: NaiveDateTime.t() | nil
  def read_utc_datetime(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.utc_datetime(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new time input.

  ## Options

    * `:default` - the initial input value. Defaults to `nil`

    * `:min` - the minimum time value (in UTC)

    * `:max` - the maximum time value (in UTC)
  """
  @spec read_utc_time(String.t(), keyword()) :: Time.t() | nil
  def read_utc_time(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.utc_time(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new date input.

  ## Options

    * `:default` - the initial input value. Defaults to `nil`

    * `:min` - the minimum date value

    * `:max` - the maximum date value
  """
  @spec read_date(String.t(), keyword()) :: Date.t() | nil
  def read_date(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.date(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new color input.

  ## Options

    * `:default` - the initial input value. Defaults to `#6583FF`
  """
  @spec read_color(String.t(), keyword()) :: String.t() | nil
  def read_color(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.color(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new image input.

  See `Kino.Input.image/2` for all supported formats and options.

  > #### Warning {: .warning}
  >
  > The image input is shared by default: once you upload a image,
  > the image will be replicated to all users reading the notebook.
  > Use `Kino.Control.form/2` if you want each user to have a distinct
  > image upload with an explicit submission button.
  """
  @spec read_image(String.t(), keyword()) ::
          %{
            file_ref: term(),
            height: pos_integer(),
            width: pos_integer(),
            format: :rgb | :png | :jpeg
          }
          | nil
  def read_image(label, opts \\ []) do
    Kino.Input.image(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new audio input.

  See `Kino.Input.audio/2` for all supported formats and options.

  > #### Warning {: .warning}
  >
  > The audio input is shared by default: once you upload an audio,
  > the audio will be replicated to all users reading the notebook.
  > Use `Kino.Control.form/2` if you want each user to have a distinct
  > audio upload with an explicit submission button.
  """
  @spec read_audio(String.t(), keyword()) ::
          %{
            file_ref: term(),
            num_channels: pos_integer(),
            sampling_rate: pos_integer(),
            format: :pcm_f32 | :wav
          }
          | nil
  def read_audio(label, opts \\ []) do
    Kino.Input.audio(label, opts) |> Kino.render() |> Kino.Input.read()
  end

  @doc """
  Renders and reads a new file input.

  The file path can then be accessed using `Kino.Input.file_path/1`.
  See `Kino.Input.file/2` for additional considerations.

  > #### Warning {: .warning}
  >
  > The file input is shared by default: once you upload a file,
  > the file will be replicated to all users reading the notebook.
  > Use `Kino.Control.form/2` if you want each user to have a distinct
  > file upload with an explicit submission button.

  ## Options

    * `:accept` - the list of accepted file types (either extensions
      or MIME types) or `:any`. Defaults to `:any`

  """
  @spec read_file(String.t(), keyword()) ::
          %{
            file_ref: String.t(),
            client_name: String.t()
          }
          | nil
  def read_file(label, opts \\ []) when is_binary(label) and is_list(opts) do
    Kino.Input.file(label, opts) |> Kino.render() |> Kino.Input.read()
  end
end