From 430b8bf3e97f35b928c248aaa58d618610a4bfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Wed, 23 Aug 2023 22:17:13 +0200 Subject: [PATCH] Update outputs format --- lib/kino.ex | 4 +- lib/kino/control.ex | 22 +- lib/kino/frame.ex | 2 +- lib/kino/image.ex | 15 +- lib/kino/input.ex | 20 +- lib/kino/js.ex | 4 +- lib/kino/js/data_store.ex | 2 +- lib/kino/js/live.ex | 4 +- lib/kino/output.ex | 499 +------------------------------------ lib/kino/render.ex | 93 +++++-- mix.exs | 1 - test/kino/control_test.exs | 16 +- test/kino/debug_test.exs | 48 ++-- test/kino/frame_test.exs | 28 ++- test/kino/output_test.exs | 2 +- test/kino/text_test.exs | 6 +- test/kino/tree_test.exs | 6 +- test/kino_test.exs | 91 +++---- 18 files changed, 234 insertions(+), 629 deletions(-) diff --git a/lib/kino.ex b/lib/kino.ex index 6b4eb0d1..0b2ae7ef 100644 --- a/lib/kino.ex +++ b/lib/kino.ex @@ -90,8 +90,8 @@ defmodule Kino do def inspect(term, opts \\ []) do label = if label = opts[:label], do: "#{label}: ", else: "" - {:terminal_text, text, info} = Kino.Output.inspect(term, opts) - output = {:terminal_text, label <> text, info} + output = Kino.Output.inspect(term, opts) + output = update_in(output.text, &(label <> &1)) Kino.Bridge.put_output(output) term diff --git a/lib/kino/control.ex b/lib/kino/control.ex index c4cab7b1..c887a96b 100644 --- a/lib/kino/control.ex +++ b/lib/kino/control.ex @@ -34,9 +34,13 @@ defmodule Kino.Control do #=> {:hello, %{origin: "client1"}} """ - defstruct [:attrs] + defstruct [:ref, :destination, :attrs] - @opaque t :: %__MODULE__{attrs: Kino.Output.control_attrs()} + @opaque t :: %__MODULE__{ + ref: Kino.Output.ref(), + destination: Process.dest(), + attrs: map() + } @opaque interval :: {:interval, milliseconds :: non_neg_integer()} @@ -46,12 +50,10 @@ defmodule Kino.Control do ref = Kino.Output.random_ref() subscription_manager = Kino.SubscriptionManager.cross_node_name() - attrs = Map.merge(attrs, %{ref: ref, destination: subscription_manager}) - Kino.Bridge.reference_object(ref, self()) Kino.Bridge.monitor_object(ref, subscription_manager, {:clear_topic, ref}) - %__MODULE__{attrs: attrs} + %__MODULE__{ref: ref, destination: subscription_manager, attrs: attrs} end @doc """ @@ -268,7 +270,7 @@ defmodule Kino.Control do Enum.map(fields, fn {field, input} -> # Make sure we use this input only in the form and nowhere else input = Kino.Input.duplicate(input) - {field, input.attrs} + {field, Kino.Render.to_livebook(input)} end) submit = Keyword.get(opts, :submit, nil) @@ -309,7 +311,7 @@ defmodule Kino.Control do @spec subscribe(t() | Kino.Input.t(), term()) :: :ok def subscribe(source, tag) when is_struct(source, Kino.Control) or is_struct(source, Kino.Input) do - Kino.SubscriptionManager.subscribe(source.attrs.ref, self(), tag) + Kino.SubscriptionManager.subscribe(source.ref, self(), tag) end @doc """ @@ -318,7 +320,7 @@ defmodule Kino.Control do @spec unsubscribe(t() | Kino.Input.t()) :: :ok def unsubscribe(source) when is_struct(source, Kino.Control) or is_struct(source, Kino.Input) do - Kino.SubscriptionManager.unsubscribe(source.attrs.ref, self()) + Kino.SubscriptionManager.unsubscribe(source.ref, self()) end @doc """ @@ -377,7 +379,7 @@ defmodule Kino.Control do assert_stream_source!(source) case source do - %struct{attrs: %{ref: ref}} when struct in [Kino.Control, Kino.Input] -> + %struct{ref: ref} when struct in [Kino.Control, Kino.Input] -> {[{nil, ref} | tagged_topics], tagged_intervals} %Kino.JS.Live{ref: ref} -> @@ -428,7 +430,7 @@ defmodule Kino.Control do {tag, source} = entry case source do - %struct{attrs: %{ref: ref}} when struct in [Kino.Control, Kino.Input] -> + %struct{ref: ref} when struct in [Kino.Control, Kino.Input] -> {[{tag, ref} | tagged_topics], tagged_intervals} %Kino.JS.Live{ref: ref} -> diff --git a/lib/kino/frame.ex b/lib/kino/frame.ex index aaf438cf..6909734d 100644 --- a/lib/kino/frame.ex +++ b/lib/kino/frame.ex @@ -200,7 +200,7 @@ defmodule Kino.Frame do defp update_outputs(state, _destination, _update_fun), do: state defp put_update(destination, ref, outputs, type) do - output = Kino.Output.frame(outputs, %{ref: ref, type: type}) + output = %{type: :frame_update, ref: ref, update: {type, outputs}} case destination do :default -> Kino.Bridge.put_output(output) diff --git a/lib/kino/image.ex b/lib/kino/image.ex index 543bc778..dc4959f1 100644 --- a/lib/kino/image.ex +++ b/lib/kino/image.ex @@ -29,8 +29,19 @@ defmodule Kino.Image do The given type be either `:jpeg`/`:jpg`, `:png`, `:gif`, `:svg`, `:pixel` or a string with image MIME type. - Note that a special `:pixel` format is supported, see `t:Kino.Output.image/0` - for the specification. + ## Pixel data + + Note that a special `image/x-pixel` MIME type is supported. The + binary consists of the following consecutive parts: + + * height - 32 bits (unsigned big-endian integer) + * width - 32 bits (unsigned big-endian integer) + * channels - 8 bits (unsigned integer) + * data - pixel data in HWC order + + Pixel data consists of 8-bit unsigned integers. The number of channels + can be either: 1 (grayscale), 2 (grayscale + alpha), 3 (RGB), or 4 + (RGB + alpha). """ @spec new(binary(), common_image_type() | mime_type()) :: t() def new(content, type) do diff --git a/lib/kino/input.ex b/lib/kino/input.ex index 43822b86..086f8724 100644 --- a/lib/kino/input.ex +++ b/lib/kino/input.ex @@ -26,9 +26,14 @@ defmodule Kino.Input do more details. """ - defstruct [:attrs] + defstruct [:ref, :id, :destination, :attrs] - @opaque t :: %__MODULE__{attrs: Kino.Output.input_attrs()} + @opaque t :: %__MODULE__{ + ref: Kino.Output.ref(), + id: String.t(), + destination: Process.dest(), + attrs: map() + } defp new(attrs) do token = Kino.Bridge.generate_token() @@ -37,17 +42,10 @@ defmodule Kino.Input do ref = Kino.Output.random_ref() subscription_manager = Kino.SubscriptionManager.cross_node_name() - attrs = - Map.merge(attrs, %{ - ref: ref, - id: persistent_id, - destination: subscription_manager - }) - Kino.Bridge.reference_object(ref, self()) Kino.Bridge.monitor_object(ref, subscription_manager, {:clear_topic, ref}) - %__MODULE__{attrs: attrs} + %__MODULE__{ref: ref, id: persistent_id, destination: subscription_manager, attrs: attrs} end @doc false @@ -662,7 +660,7 @@ defmodule Kino.Input do """ @spec read(t()) :: term() def read(%Kino.Input{} = input) do - case Kino.Bridge.get_input_value(input.attrs.id) do + case Kino.Bridge.get_input_value(input.id) do {:ok, value} -> value diff --git a/lib/kino/js.ex b/lib/kino/js.ex index 40051d27..a3a047ce 100644 --- a/lib/kino/js.ex +++ b/lib/kino/js.ex @@ -491,8 +491,8 @@ defmodule Kino.JS do end @doc false - @spec js_info(t()) :: Kino.Output.js_info() - def js_info(%__MODULE__{} = kino) do + @spec output_attrs(t()) :: map() + def output_attrs(%__MODULE__{} = kino) do %{ js_view: %{ ref: kino.ref, diff --git a/lib/kino/js/data_store.ex b/lib/kino/js/data_store.ex index 89fec5af..22fe6566 100644 --- a/lib/kino/js/data_store.ex +++ b/lib/kino/js/data_store.ex @@ -24,7 +24,7 @@ defmodule Kino.JS.DataStore do @doc """ Stores output data under the given ref. """ - @spec store(Kino.Output.js_output_ref(), term()) :: :ok + @spec store(Kino.Output.ref(), term()) :: :ok def store(ref, data) do GenServer.cast(@name, {:store, ref, data}) end diff --git a/lib/kino/js/live.ex b/lib/kino/js/live.ex index dcf2e21f..6c707470 100644 --- a/lib/kino/js/live.ex +++ b/lib/kino/js/live.ex @@ -340,8 +340,8 @@ defmodule Kino.JS.Live do end @doc false - @spec js_info(t()) :: Kino.Output.js_info() - def js_info(%__MODULE__{} = kino) do + @spec output_info(t()) :: map() + def output_info(%__MODULE__{} = kino) do %{ js_view: %{ ref: kino.ref, diff --git a/lib/kino/output.ex b/lib/kino/output.ex index 5cc69620..6cb90beb 100644 --- a/lib/kino/output.ex +++ b/lib/kino/output.ex @@ -1,512 +1,25 @@ defmodule Kino.Output do - @moduledoc """ - A number of output formats supported by Livebook. - """ + @moduledoc false import Kernel, except: [inspect: 2] @typedoc """ - Livebook cell output may be one of these values and gets rendered accordingly. - """ - @type t :: - ignored() - | terminal_text() - | plain_text() - | markdown() - | image() - | js() - | frame() - | tabs() - | grid() - | input() - | control() - - @typedoc """ - An empty output that should be ignored whenever encountered. - """ - @type ignored :: :ignored - - @typedoc ~S""" - Terminal text content. - - Supports ANSI escape codes and overwriting lines with `\r`. - """ - @type terminal_text :: {:terminal_text, String.t(), info :: %{chunk: boolean()}} - - @typedoc """ - Plain text content. - - Adjacent outputs with `:chunk` set to `true` are merged and rendered - as a whole. + Livebook cell output may be one of these values and gets rendered + accordingly. - Similar to `t:markdown/0`, but with no special markup. + See `t:Livebook.Runtime.output/0` for the detailed format. """ - @type plain_text :: {:plain_text, String.t(), info :: %{chunk: boolean()}} - - @typedoc """ - Markdown content. - - Adjacent outputs with `:chunk` set to `true` are merged and rendered - as a whole. - """ - @type markdown :: {:markdown, String.t(), info :: %{chunk: boolean()}} - - @typedoc """ - A raw image in the given format. - - ## Pixel data - - Note that a special `image/x-pixel` MIME type is supported. The - binary consists of the following consecutive parts: - - * height - 32 bits (unsigned big-endian integer) - * width - 32 bits (unsigned big-endian integer) - * channels - 8 bits (unsigned integer) - * data - pixel data in HWC order - - Pixel data consists of 8-bit unsigned integers. The number of channels - can be either: 1 (grayscale), 2 (grayscale + alpha), 3 (RGB), or 4 - (RGB + alpha). - """ - @type image :: {:image, content :: binary(), mime_type :: binary()} - - @typedoc """ - JavaScript powered output with dynamic data and events. - - See `Kino.JS` and `Kino.JS.Live` for more details. - """ - @type js() :: {:js, info :: js_info()} - - @typedoc """ - Data describing a JS output. - - ## Export - - The `:export` map describes how the output should be persisted. - The output data is put in a Markdown fenced code block. - - * `:info_string` - used as the info string for the Markdown - code block - - * `:key` - in case the data is a map and only a specific part - should be exported - """ - @type js_info :: %{ - js_view: js_view(), - export: - nil - | %{ - info_string: String.t(), - key: nil | term() - } - } - - @typedoc """ - A JavaScript view definition. - - JS view is a component rendered on the client side and possibly - interacting with a server process within the runtime. - - * `:ref` - unique identifier - - * `:pid` - the server process holding the data and handling - interactions - - ## Assets - - The `:assets` map includes information about the relevant files. - - * `:archive_path` - an absolute path to a `.tar.gz` archive with - all the assets - - * `:hash` - a checksum of all assets in the archive - - * `:js_path` - a relative asset path pointing to the JavaScript - entrypoint module - - * `:cdn_url` - an absolute URL to a CDN directory where the asset - files can be accessed from. Note that this URL is not guaranteed - to be accessible, since it may be pointing to a private package - - ## Communication protocol - - A client process should connect to the server process by sending: - - {:connect, pid(), info :: %{ref: ref(), origin: term()}} - - And expect the following reply: - - {:connect_reply, payload, info :: %{ref: ref()}} - - The server process may then keep sending one of the following events: - - {:event, event :: String.t(), payload, info :: %{ref: ref()}} - - The client process may keep sending one of the following events: - - {:event, event :: String.t(), payload, info :: %{ref: ref(), origin: term()}} - - The client can also send a ping message: - - {:ping, pid(), metadata :: term(), info :: %{ref: ref()}} - - And the server should respond with: - - {:pong, metadata :: term(), info :: %{ref: ref()}} - """ - @type js_view :: %{ - ref: ref(), - pid: Process.dest(), - assets: %{ - archive_path: String.t(), - hash: String.t(), - js_path: String.t(), - cdn_url: String.t() | nil - } - } - - @typedoc """ - Outputs placeholder. - - Frame with type `:default` includes the initial list of outputs. - Other types can be used to update outputs within the given frame. - - In all cases the outputs order is reversed, that is, most recent - outputs are at the top of the stack. - """ - @type frame :: {:frame, outputs :: list(t()), frame_info()} - - @type frame_info :: - %{ - ref: frame_ref(), - type: :default, - placeholder: boolean() - } - | %{ - ref: frame_ref(), - type: :replace | :append - } - - @type frame_ref :: String.t() - - @typedoc """ - Multiple outputs arranged into tabs. - """ - @type tabs :: {:tabs, outputs :: list(t()), tabs_info()} - - @type tabs_info :: %{ - labels: list(String.t()) - } - - @typedoc """ - Multiple outputs arranged in a grid. - """ - @type grid :: {:grid, outputs :: list(t()), grid_info()} - - @type grid_info :: %{ - columns: pos_integer(), - boxed: boolean() - } - - @typedoc """ - An input field. - - All inputs have the following properties: - - * `:type` - one of the recognised input types - - * `:ref` - a unique identifier - - * `:id` - a persistent input identifier, the same on every reevaluation - - * `:label` - an arbitrary text used as the input caption - - * `:default` - the initial input value - - * `:destination` - the process to send event messages to - - On top of that, each input type may have additional attributes. - """ - @type input :: {:input, attrs :: input_attrs()} - - @type input_id :: String.t() - - @type input_attrs :: - %{ - type: :text, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t(), - destination: Process.dest() - } - | %{ - type: :textarea, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t(), - monospace: boolean(), - destination: Process.dest() - } - | %{ - type: :password, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t(), - destination: Process.dest() - } - | %{ - type: :number, - ref: ref(), - id: input_id(), - label: String.t(), - default: number() | nil, - destination: Process.dest() - } - | %{ - type: :url, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t() | nil, - destination: Process.dest() - } - | %{ - type: :select, - ref: ref(), - id: input_id(), - label: String.t(), - default: term(), - destination: Process.dest(), - options: list({value :: term(), label :: String.t()}) - } - | %{ - type: :checkbox, - ref: ref(), - id: input_id(), - label: String.t(), - default: boolean(), - destination: Process.dest() - } - | %{ - type: :range, - ref: ref(), - id: input_id(), - label: String.t(), - default: number(), - destination: Process.dest(), - min: number(), - max: number(), - step: number() - } - | %{ - type: :utc_datetime, - ref: ref(), - id: input_id(), - label: String.t(), - default: NaiveDateTime.t() | nil, - destination: Process.dest(), - min: NaiveDateTime.t() | nil, - max: NaiveDateTime.t() | nil - } - | %{ - type: :utc_time, - ref: ref(), - id: input_id(), - label: String.t(), - default: Time.t() | nil, - destination: Process.dest(), - min: Time.t() | nil, - max: Time.t() | nil - } - | %{ - type: :date, - ref: ref(), - id: input_id(), - label: String.t(), - default: Date.t(), - destination: Process.dest(), - min: Date.t(), - max: Date.t() - } - | %{ - type: :color, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t(), - destination: Process.dest() - } - | %{ - type: :image, - ref: ref(), - id: input_id(), - label: String.t(), - default: nil, - destination: Process.dest(), - format: :rgb | :png | :jpeg, - size: {pos_integer(), pos_integer()} | nil, - fit: :match | :contain | :pad | :crop - } - | %{ - type: :audio, - ref: ref(), - id: input_id(), - label: String.t(), - default: nil, - destination: Process.dest(), - format: :pcm_f32 | :wav, - sampling_rate: pos_integer() - } - | %{ - type: :file, - ref: ref(), - id: input_id(), - label: String.t(), - default: String.t(), - destination: Process.dest(), - accept: list(String.t()) | :any - } - - @typedoc """ - A control widget. - - All controls have the following properties: - - * `:type` - one of the recognised control types - - * `:ref` - a unique identifier - - * `:destination` - the process to send event messages to - - On top of that, each control type may have additional attributes. - - ## Events - - All control events are sent to `:destination` as `{:event, id, info}`, - where info is a map including additional details. In particular, it - always includes `:origin`, which is an opaque identifier of the client - that triggered the event. - """ - @type control :: {:control, attrs :: control_attrs()} - - @type control_attrs :: - %{ - type: :keyboard, - ref: ref(), - destination: Process.dest(), - events: list(:keyup | :keydown | :status), - default_handlers: :off | :on | :disable_only - } - | %{ - type: :button, - ref: ref(), - destination: Process.dest(), - label: String.t() - } - | %{ - type: :form, - ref: ref(), - destination: Process.dest(), - fields: list({field :: atom(), input_attrs()}), - submit: String.t() | nil, - # Currently we always use true, but we can support - # other tracking modes in the future - report_changes: %{(field :: atom()) => true}, - reset_on_submit: list(field :: atom()) - } + @type t :: map() @type ref :: String.t() - @doc """ - See `t:terminal_text/0`. - """ - @spec terminal_text(binary(), boolean()) :: t() - def terminal_text(text, chunk \\ false) when is_binary(text) do - {:terminal_text, text, %{chunk: chunk}} - end - - @doc """ - See `t:plain_text/0`. - """ - @spec plain_text(binary(), boolean()) :: t() - def plain_text(text, chunk \\ false) do - {:plain_text, text, %{chunk: chunk}} - end - - @doc """ - See `t:markdown/0`. - """ - @spec markdown(binary(), boolean()) :: t() - def markdown(content, chunk \\ false) when is_binary(content) do - {:markdown, content, %{chunk: chunk}} - end - - @doc """ - See `t:image/0`. - """ - @spec image(binary(), binary()) :: t() - def image(content, mime_type) when is_binary(content) and is_binary(mime_type) do - {:image, content, mime_type} - end - - @doc """ - See `t:js/0`. - """ - @spec js(js_info()) :: t() - def js(info) when is_map(info) do - {:js, info} - end - - @doc """ - See `t:frame/0`. - """ - @spec frame(list(t()), frame_info()) :: t() - def frame(outputs, info) when is_list(outputs) and is_map(info) do - {:frame, outputs, info} - end - - @doc """ - See `t:tabs/0`. - """ - @spec tabs(list(t()), tabs_info()) :: t() - def tabs(outputs, info) when is_list(outputs) and is_map(info) do - {:tabs, outputs, info} - end - - @doc """ - See `t:grid/0`. - """ - @spec grid(list(t()), grid_info()) :: t() - def grid(outputs, info) when is_list(outputs) and is_map(info) do - {:grid, outputs, info} - end - - @doc """ - See `t:input/0`. - """ - @spec input(input_attrs()) :: t() - def input(attrs) when is_map(attrs) do - {:input, attrs} - end - - @doc """ - See `t:control/0`. - """ - @spec control(control_attrs()) :: t() - def control(attrs) when is_map(attrs) do - {:control, attrs} - end - @doc """ Returns `t:text/0` with the inspected term. """ @spec inspect(term(), keyword()) :: t() def inspect(term, opts \\ []) do inspected = Kernel.inspect(term, inspect_opts(opts)) - terminal_text(inspected) + %{type: :terminal_text, text: inspected, chunk: false} end defp inspect_opts(opts) do diff --git a/lib/kino/render.ex b/lib/kino/render.ex index 32b03964..e155a15b 100644 --- a/lib/kino/render.ex +++ b/lib/kino/render.ex @@ -7,8 +7,40 @@ defprotocol Kino.Render do @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()) :: Kino.Output.t() + @spec to_livebook(t()) :: map() def to_livebook(value) end @@ -25,72 +57,97 @@ defimpl Kino.Render, for: Kino.Inspect do end defimpl Kino.Render, for: Kino.JS do + @dialyzer {:nowarn_function, {:to_livebook, 1}} + def to_livebook(kino) do - info = Kino.JS.js_info(kino) Kino.Bridge.reference_object(kino.ref, self()) - Kino.Output.js(info) + %{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()) - info = Kino.JS.Live.js_info(kino) - Kino.Output.js(info) + %{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 - Kino.Output.image(image.content, image.mime_type) + %{type: :image, content: image.content, mime_type: image.mime_type} end end defimpl Kino.Render, for: Kino.Text do - def to_livebook(%{terminal: true} = text) do - Kino.Output.terminal_text(text.text, text.chunk) + def to_livebook(%{terminal: true} = kino) do + %{type: :terminal_text, text: kino.text, chunk: kino.chunk} end - def to_livebook(text) do - Kino.Output.plain_text(text.text, text.chunk) + 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 - Kino.Output.markdown(markdown.text, markdown.chunk) + %{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) - Kino.Output.frame(outputs, %{ref: kino.ref, type: :default, placeholder: kino.placeholder}) + %{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 - Kino.Output.tabs(kino.outputs, kino.info) + %{type: :tabs, outputs: kino.outputs, labels: kino.info.labels} end def to_livebook(%{type: :grid} = kino) do - Kino.Output.grid(kino.outputs, kino.info) + %{ + 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.attrs.ref, self()) - Kino.Output.input(input.attrs) + 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.attrs.ref, self()) - Kino.Output.control(control.attrs) + Kino.Bridge.reference_object(control.ref, self()) + + %{ + type: :control, + ref: control.ref, + destination: control.destination, + attrs: control.attrs + } end end diff --git a/mix.exs b/mix.exs index fa901c2e..1db39a9f 100644 --- a/mix.exs +++ b/mix.exs @@ -59,7 +59,6 @@ defmodule Kino.MixProject do ], "Protocols and Behaviours": [ Kino.Render, - Kino.Output, Kino.Inspect, Kino.Table ], diff --git a/test/kino/control_test.exs b/test/kino/control_test.exs index d12637ab..56b2ac53 100644 --- a/test/kino/control_test.exs +++ b/test/kino/control_test.exs @@ -69,7 +69,7 @@ defmodule Kino.ControlTest do Kino.Control.subscribe(button, :name) info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) assert_receive {:name, ^info} end @@ -89,7 +89,7 @@ defmodule Kino.ControlTest do background_tick(fn -> info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) end) events = button |> Kino.Control.stream() |> Enum.take(2) @@ -110,14 +110,14 @@ defmodule Kino.ControlTest do background_tick(fn -> info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) end) events = button |> Kino.Control.stream() |> Enum.map(fn event -> - send(button.attrs.destination, {:clear_topic, button.attrs.ref}) + send(button.destination, {:clear_topic, button.ref}) event end) @@ -153,8 +153,8 @@ defmodule Kino.ControlTest do input = Kino.Input.text("Name") background_tick(fn -> - send(button.attrs.destination, {:event, button.attrs.ref, %{origin: "client1"}}) - send(button.attrs.destination, {:event, input.attrs.ref, %{origin: "client2"}}) + send(button.destination, {:event, button.ref, %{origin: "client1"}}) + send(button.destination, {:event, input.ref, %{origin: "client2"}}) end) events = [button, input] |> Kino.Control.stream() |> Enum.take(2) @@ -181,8 +181,8 @@ defmodule Kino.ControlTest do input = Kino.Input.text("Name") background_tick(fn -> - send(button.attrs.destination, {:event, button.attrs.ref, %{origin: "client1"}}) - send(input.attrs.destination, {:event, input.attrs.ref, %{origin: "client2"}}) + send(button.destination, {:event, button.ref, %{origin: "client1"}}) + send(input.destination, {:event, input.ref, %{origin: "client2"}}) end) events = diff --git a/test/kino/debug_test.exs b/test/kino/debug_test.exs index 7f083aea..bebd7e71 100644 --- a/test/kino/debug_test.exs +++ b/test/kino/debug_test.exs @@ -9,13 +9,13 @@ defmodule Kino.Debug.Test do describe "dbg with a pipeline expression" do defp assert_dbg_pipeline_render() do - assert_output( - {:grid, - [ - {:js, %{js_view: js_view}}, - {:frame, [output], %{ref: frame_ref}} - ], %{}} - ) + assert_output(%{ + type: :grid, + outputs: [ + %{type: :js, js_view: js_view}, + %{type: :frame, ref: frame_ref, outputs: [output]} + ] + }) kino = %Kino.JS.Live{ref: js_view.ref, pid: js_view.pid} @@ -33,7 +33,7 @@ defmodule Kino.Debug.Test do {kino, output, _frame_ref} = assert_dbg_pipeline_render() - assert output == {:terminal_text, "\e[34m13\e[0m", %{chunk: false}} + assert output == %{type: :terminal_text, text: "\e[34m13\e[0m", chunk: false} %{ dbg_line: dbg_line, @@ -76,10 +76,11 @@ defmodule Kino.Debug.Test do "changed" => true }) - assert_output( - {:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}], - %{ref: ^frame_ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^frame_ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m31\e[0m", chunk: false}]} + }) end test "updates result when a pipeline step is moved" do @@ -103,10 +104,11 @@ defmodule Kino.Debug.Test do "changed" => true }) - assert_output( - {:frame, [{:terminal_text, "\e[34m31\e[0m", %{chunk: false}}], - %{ref: ^frame_ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^frame_ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m31\e[0m", chunk: false}]} + }) end test "handles evaluation error" do @@ -129,10 +131,12 @@ defmodule Kino.Debug.Test do "selected_id" => 0 }) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m..\e[34m5\e[0m", %{chunk: false}}], - %{ref: ^frame_ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^frame_ref, + update: + {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m..\e[34m5\e[0m", chunk: false}]} + }) end test "groups multiple calls to the same dbg" do @@ -160,7 +164,7 @@ defmodule Kino.Debug.Test do describe "dbg with a non-pipeline expression" do defp assert_dbg_default_render() do - assert_output({:grid, [{:js, %{js_view: js_view}}, output], %{}}) + assert_output(%{type: :grid, outputs: [%{type: :js, js_view: js_view}, output]}) kino = %Kino.JS.Live{ref: js_view.ref, pid: js_view.pid} {kino, output} end @@ -170,7 +174,7 @@ defmodule Kino.Debug.Test do {kino, output} = assert_dbg_default_render() - assert output == {:terminal_text, "\e[34m15\e[0m", %{chunk: false}} + assert output == %{type: :terminal_text, text: "\e[34m15\e[0m", chunk: false} %{ dbg_line: dbg_line, diff --git a/test/kino/frame_test.exs b/test/kino/frame_test.exs index e13e316a..da988b58 100644 --- a/test/kino/frame_test.exs +++ b/test/kino/frame_test.exs @@ -6,12 +6,17 @@ defmodule Kino.FrameTest do Kino.Frame.render(frame, 1) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}} - ) + assert_output(%{ + type: :frame_update, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) Kino.Frame.render(frame, Kino.Markdown.new("_hey_")) - assert_output({:frame, [{:markdown, "_hey_", %{chunk: false}}], %{type: :replace}}) + + assert_output(%{ + type: :frame_update, + update: {:replace, [%{type: :markdown, text: "_hey_", chunk: false}]} + }) end test "render/2 sends output to a specific client when the :to is given" do @@ -21,7 +26,7 @@ defmodule Kino.FrameTest do assert_output_to( "client1", - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}} + %{type: :frame_update, update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]}} ) assert Kino.Frame.get_outputs(frame) == [] @@ -32,9 +37,10 @@ defmodule Kino.FrameTest do Kino.Frame.render(frame, 1, temporary: true) - assert_output_to_clients( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :replace}} - ) + assert_output_to_clients(%{ + type: :frame_update, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) assert Kino.Frame.get_outputs(frame) == [] end @@ -75,6 +81,10 @@ defmodule Kino.FrameTest do frame = Kino.Frame.new() Kino.Frame.append(frame, 1) - assert_output({:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], %{type: :append}}) + + assert_output(%{ + type: :frame_update, + update: {:append, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) end end diff --git a/test/kino/output_test.exs b/test/kino/output_test.exs index c45866c4..cd323926 100644 --- a/test/kino/output_test.exs +++ b/test/kino/output_test.exs @@ -6,7 +6,7 @@ defmodule Kino.OutputTest do Kino.Config.configure(inspect: [limit: 1, syntax_colors: []]) list = Enum.to_list(1..100) - assert Kino.Output.inspect(list) == {:terminal_text, "[1, ...]", %{chunk: false}} + assert Kino.Output.inspect(list) == %{type: :terminal_text, text: "[1, ...]", chunk: false} Application.delete_env(:kino, :inspect) end diff --git a/test/kino/text_test.exs b/test/kino/text_test.exs index dca15ab7..62668568 100644 --- a/test/kino/text_test.exs +++ b/test/kino/text_test.exs @@ -4,15 +4,15 @@ defmodule Kino.TextTest do describe "new/1" do test "outputs plain text" do "Hello!" |> Kino.Text.new() |> Kino.render() - assert_output({:plain_text, "Hello!", %{chunk: false}}) + assert_output(%{type: :plain_text, text: "Hello!", chunk: false}) "Hello!" |> Kino.Text.new(terminal: false) |> Kino.render() - assert_output({:plain_text, "Hello!", %{chunk: false}}) + assert_output(%{type: :plain_text, text: "Hello!", chunk: false}) end test "outputs terminal text" do "Hello!" |> Kino.Text.new(terminal: true) |> Kino.render() - assert_output({:terminal_text, "Hello!", %{chunk: false}}) + assert_output(%{type: :terminal_text, text: "Hello!", chunk: false}) end end end diff --git a/test/kino/tree_test.exs b/test/kino/tree_test.exs index ebc037e9..c1c87384 100644 --- a/test/kino/tree_test.exs +++ b/test/kino/tree_test.exs @@ -184,7 +184,11 @@ defmodule Kino.TreeTest do end defp tree(input) do - %Kino.Layout{type: :grid, outputs: [js: %{js_view: %{ref: ref}}]} = Kino.Tree.new(input) + %Kino.Layout{ + type: :grid, + outputs: [%{type: :js, js_view: %{ref: ref}}] + } = Kino.Tree.new(input) + send(Kino.JS.DataStore, {:connect, self(), %{origin: "client:#{inspect(self())}", ref: ref}}) assert_receive {:connect_reply, data, %{ref: ^ref}} data diff --git a/test/kino_test.exs b/test/kino_test.exs index 525ac691..b10acf54 100644 --- a/test/kino_test.exs +++ b/test/kino_test.exs @@ -16,7 +16,7 @@ defmodule KinoTest do Kino.inspect(:hey) - assert_receive {:output, {:terminal_text, "\e[34m:hey\e[0m", %{chunk: false}}} + assert_receive {:output, %{type: :terminal_text, text: "\e[34m:hey\e[0m", chunk: false}} await_process(gl) end @@ -28,17 +28,19 @@ defmodule KinoTest do |> Stream.take(2) |> Kino.animate(fn i -> i end) - assert_output({:frame, [], %{ref: ref, type: :default}}) + assert_output(%{type: :frame, ref: ref, outputs: []}) - assert_output( - {:frame, [{:terminal_text, "\e[34m0\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m0\e[0m"}]} + }) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) end test "ignores failures" do @@ -51,12 +53,13 @@ defmodule KinoTest do i end) - assert_output({:frame, [], %{ref: ref, type: :default}}) + assert_output(%{type: :frame, ref: ref, outputs: []}) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) end) assert log =~ "Kino.animate" @@ -72,17 +75,19 @@ defmodule KinoTest do {:cont, i + state, i + state} end) - assert_output({:frame, [], %{ref: ref, type: :default}}) + assert_output(%{type: :frame, ref: ref, outputs: []}) - assert_output( - {:frame, [{:terminal_text, "\e[34m0\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m0\e[0m"}]} + }) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) end test "ignores failures" do @@ -95,17 +100,19 @@ defmodule KinoTest do {:cont, i + state, i + state} end) - assert_output({:frame, [], %{ref: ref, type: :default}}) + assert_output(%{type: :frame, ref: ref, outputs: []}) - assert_output( - {:frame, [{:terminal_text, "\e[34m1\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m1\e[0m"}]} + }) - assert_output( - {:frame, [{:terminal_text, "\e[34m4\e[0m", %{chunk: false}}], - %{ref: ^ref, type: :replace}} - ) + assert_output(%{ + type: :frame_update, + ref: ^ref, + update: {:replace, [%{type: :terminal_text, text: "\e[34m4\e[0m"}]} + }) end) assert log =~ "Kino.animate" @@ -137,11 +144,11 @@ defmodule KinoTest do end) assert_receive {:trace, _, :receive, {:"$gen_cast", {:subscribe, ref, _, _}}} - when button.attrs.ref == ref + when button.ref == ref info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) + send(button.destination, {:event, button.ref, info}) assert_receive ^info assert_receive ^info @@ -206,11 +213,11 @@ defmodule KinoTest do end) assert_receive {:trace, _, :receive, {:"$gen_cast", {:subscribe, ref, _, _}}} - when button.attrs.ref == ref + when button.ref == ref info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) + send(button.destination, {:event, button.ref, info}) assert_receive {:counter, 1} assert_receive {:counter, 2} @@ -264,11 +271,11 @@ defmodule KinoTest do end) assert_receive {:trace, _, :receive, {:"$gen_cast", {:subscribe, ref, _, _}}} - when button.attrs.ref == ref + when button.ref == ref info = %{origin: "client1"} - send(button.attrs.destination, {:event, button.attrs.ref, info}) - send(button.attrs.destination, {:event, button.attrs.ref, info}) + send(button.destination, {:event, button.ref, info}) + send(button.destination, {:event, button.ref, info}) assert_receive ^info assert_receive ^info