defprotocol Kino.Render do
@moduledoc """
Protocol defining term formatting in the context of Livebook.
"""
@fallback_to_any true
@doc """
Transforms the given value into a Livebook-compatible output.
For detailed description of the output format see `t:Livebook.Runtime.output/0`.
When implementing the protocol for custom struct, you generally do
not need to worry about the output format. Instead, you can compose
built-in kinos and call `Kino.Render.to_livebook/1` to get the
expected representation.
For example, if we wanted to render a custom struct as a mermaid
graph, we could do this:
defimpl Kino.Render, for: Graph do
def to_livebook(graph) do
source = Graph.to_mermaid(graph)
mermaid_kino = Kino.Mermaid.new(source)
Kino.Render.to_livebook(mermaid_kino)
end
end
In many cases it is useful to show the default inspect representation
alongside our custom visual representation. For this, we can use tabs:
defimpl Kino.Render, for: Graph do
def to_livebook(graph) do
source = Graph.to_mermaid(graph)
mermaid_kino = Kino.Mermaid.new(source)
inspect_kino = Kino.Inspect.new(image)
kino = Kino.Layout.tabs(Graph: mermaid_kino, Raw: inspect_kino)
Kino.Render.to_livebook(kino)
end
end
"""
@spec to_livebook(t()) :: map()
def to_livebook(value)
end
defimpl Kino.Render, for: Any do
def to_livebook(term) do
Kino.Output.inspect(term)
end
end
defimpl Kino.Render, for: Kino.Inspect do
def to_livebook(raw) do
Kino.Output.inspect(raw.term)
end
end
defimpl Kino.Render, for: Kino.JS do
@dialyzer {:nowarn_function, {:to_livebook, 1}}
def to_livebook(kino) do
Kino.Bridge.reference_object(kino.ref, self())
%{js_view: js_view, export: export} = Kino.JS.output_attrs(kino)
%{type: :js, js_view: js_view, export: export}
end
end
defimpl Kino.Render, for: Kino.JS.Live do
@dialyzer {:nowarn_function, {:to_livebook, 1}}
def to_livebook(kino) do
Kino.Bridge.reference_object(kino.pid, self())
%{js_view: js_view, export: export} = Kino.JS.Live.output_info(kino)
%{type: :js, js_view: js_view, export: export}
end
end
defimpl Kino.Render, for: Kino.Image do
def to_livebook(image) do
%{type: :image, content: image.content, mime_type: image.mime_type}
end
end
defimpl Kino.Render, for: Kino.Text do
def to_livebook(%{terminal: true} = kino) do
%{type: :terminal_text, text: kino.text, chunk: kino.chunk}
end
def to_livebook(kino) do
%{type: :plain_text, text: kino.text, chunk: kino.chunk}
end
end
defimpl Kino.Render, for: Kino.Markdown do
def to_livebook(markdown) do
%{type: :markdown, text: markdown.text, chunk: markdown.chunk}
end
end
defimpl Kino.Render, for: Kino.Frame do
@dialyzer {:nowarn_function, {:to_livebook, 1}}
def to_livebook(kino) do
Kino.Bridge.reference_object(kino.pid, self())
outputs = Kino.Frame.get_outputs(kino)
%{type: :frame, ref: kino.ref, outputs: outputs, placeholder: kino.placeholder}
end
end
defimpl Kino.Render, for: Kino.Layout do
def to_livebook(%{type: :tabs} = kino) do
%{type: :tabs, outputs: kino.outputs, labels: kino.info.labels}
end
def to_livebook(%{type: :grid} = kino) do
%{
type: :grid,
outputs: kino.outputs,
columns: kino.info.columns,
gap: kino.info.gap,
boxed: kino.info.boxed
}
end
end
defimpl Kino.Render, for: Kino.Input do
def to_livebook(input) do
Kino.Bridge.reference_object(input.ref, self())
%{
type: :input,
ref: input.ref,
id: input.id,
destination: input.destination,
attrs: input.attrs
}
end
end
defimpl Kino.Render, for: Kino.Control do
def to_livebook(control) do
Kino.Bridge.reference_object(control.ref, self())
%{
type: :control,
ref: control.ref,
destination: control.destination,
attrs: control.attrs
}
end
end
# Elixir built-ins
defimpl Kino.Render, for: Reference do
def to_livebook(reference) do
cond do
accessible_ets_table?(reference) ->
reference |> Kino.ETS.new() |> Kino.Render.to_livebook()
true ->
Kino.Output.inspect(reference)
end
end
defp accessible_ets_table?(tid) do
try do
case :ets.info(tid, :protection) do
:undefined -> false
:private -> false
_ -> true
end
rescue
# When the tid is not a valid table identifier
ArgumentError -> false
end
end
end
defimpl Kino.Render, for: Atom do
def to_livebook(atom) do
cond do
application_with_supervisor?(atom) ->
raw = Kino.Inspect.new(atom)
tree = Kino.Process.app_tree(atom, direction: :left_right)
tabs = Kino.Layout.tabs(Raw: raw, "Application tree": tree)
Kino.Render.to_livebook(tabs)
Kino.Utils.supervisor?(atom) ->
raw = Kino.Inspect.new(atom)
tree = Kino.Process.sup_tree(atom, direction: :left_right)
tabs = Kino.Layout.tabs(Raw: raw, "Supervision tree": tree)
Kino.Render.to_livebook(tabs)
true ->
Kino.Output.inspect(atom)
end
end
defp application_with_supervisor?(name) do
with master when master != :undefined <- :application_controller.get_master(name),
{root, _application} when is_pid(root) <- :application_master.get_child(master),
do: true,
else: (_ -> false)
end
end
defimpl Kino.Render, for: PID do
def to_livebook(pid) do
cond do
Kino.Utils.supervisor?(pid) ->
raw = Kino.Inspect.new(pid)
tree = Kino.Process.sup_tree(pid, direction: :left_right)
tabs = Kino.Layout.tabs(Raw: raw, "Supervision tree": tree)
Kino.Render.to_livebook(tabs)
true ->
Kino.Output.inspect(pid)
end
end
end
defimpl Kino.Render, for: BitString do
def to_livebook(string) do
case Kino.Utils.get_image_type(string) do
nil ->
Kino.Output.inspect(string)
type ->
raw = Kino.Inspect.new(string)
image = Kino.Image.new(string, type)
tabs = Kino.Layout.tabs(Image: image, Raw: raw)
Kino.Render.to_livebook(tabs)
end
end
end
defimpl Kino.Render, for: Nx.Heatmap do
def to_livebook(heatmap) do
tensor = Kino.Inspect.new(heatmap.tensor)
heatmap = Kino.Inspect.new(heatmap)
tabs = Kino.Layout.tabs(Heatmap: heatmap, Tensor: tensor)
Kino.Render.to_livebook(tabs)
end
end