Repository: livebook-dev/kino (elixir)

Issue:
Add ANSI escape support to `Kino.Text`
Currently, `Kino.Text` always renders as `Kino.Output.plain_text(content)`. To render text containing ANSI escape sequences, you have to create your own wrapper struct that uses `Kino.Output.text(content)`.

Being able to use `Kino.Text` to support text with ANSI escape sequences would be a nice little improvement:

```elixir
Kino.Text.new(my_text, plain: false) # default true
# or
Kino.Text.new(my_text, ansi: true) # default false
```

Pull request description:
Support ANSI escape sequences in `Kino.Text`
Closes #313 

Patch:
diff --git a/lib/kino/render.ex b/lib/kino/render.ex
index 66ba86d..ba999b1 100644
--- a/lib/kino/render.ex
+++ b/lib/kino/render.ex
@@ -47,6 +47,10 @@ defimpl Kino.Render, for: Kino.Image do
 end
 
 defimpl Kino.Render, for: Kino.Text do
+  def to_livebook(%{terminal: true} = text) do
+    Kino.Output.text(text.content)
+  end
+
   def to_livebook(text) do
     Kino.Output.plain_text(text.content)
   end
diff --git a/lib/kino/text.ex b/lib/kino/text.ex
index 565166e..2ce9905 100644
--- a/lib/kino/text.ex
+++ b/lib/kino/text.ex
@@ -1,28 +1,40 @@
 defmodule Kino.Text do
   @moduledoc ~S'''
-  A kino for rendering plain text content.
+  A kino for rendering text content.
 
-  For rich text use `Kino.Markdown`.
+  For rich text, use `Kino.Markdown`.
 
   ## Examples
 
       Kino.Text.new("Hello!")
 
+      [:green, "Hello!"]
+      |> IO.ANSI.format()
+      |> IO.iodata_to_binary()
+      |> Kino.Text.new(terminal: true)
+
   '''
 
   @enforce_keys [:content]
 
-  defstruct [:content]
+  defstruct [:content, terminal: false]
 
   @opaque t :: %__MODULE__{
-            content: binary()
+            content: binary(),
+            terminal: boolean()
           }
 
   @doc """
   Creates a new kino displaying the given text content.
+
+  ## Options
+
+    * `:terminal` - whether to render the text as if it were printed to
+      standard output, supporting ANSI escape codes. Defaults to `false`
   """
-  @spec new(String.t()) :: t()
-  def new(content) do
-    %__MODULE__{content: content}
+  @spec new(String.t(), opts) :: t() when opts: [terminal: boolean()]
+  def new(content, opts \\ []) when is_binary(content) do
+    opts = Keyword.validate!(opts, terminal: false)
+    %__MODULE__{content: content, terminal: opts[:terminal]}
   end
 end


Test patch:
diff --git a/test/kino/text_test.exs b/test/kino/text_test.exs
new file mode 100644
index 0000000..0016c1c
--- /dev/null
+++ b/test/kino/text_test.exs
@@ -0,0 +1,18 @@
+defmodule Kino.TextTest do
+  use Kino.LivebookCase, async: true
+
+  describe "new/1" do
+    test "outputs plain text" do
+      "Hello!" |> Kino.Text.new() |> Kino.render()
+      assert_output({:plain_text, "Hello!"})
+
+      "Hello!" |> Kino.Text.new(terminal: false) |> Kino.render()
+      assert_output({:plain_text, "Hello!"})
+    end
+
+    test "outputs terminal text" do
+      "Hello!" |> Kino.Text.new(terminal: true) |> Kino.render()
+      assert_output({:text, "Hello!"})
+    end
+  end
+end