Owl.LiveScreen (Owl v0.13.1)

Copy Markdown View Source

A server that handles live updates in the terminal.

It partially implements The Erlang I/O Protocol, so it is possible to use Owl.LiveScreen as an I/O device in Logger and functions like Owl.IO.puts/2, IO.puts/2. When used as an I/O device, output is printed above dynamic blocks.

Example

require Logger
:ok = :logger.remove_handler(:default)

:ok =
  :logger.add_handler(:default, :logger_std_h, %{
    config: %{type: {:device, Owl.LiveScreen}},
    formatter: Logger.Formatter.new(),
    # Required on OTP 28+: when the handler id is `:default` and no `:filters`
    # key is given, `:logger.add_handler/3` injects `filter_default: :stop`
    # plus the kernel's default domain filters, which silently drop Elixir's
    # `domain: [:elixir]` log events. Setting filters explicitly keeps logs
    # flowing on both OTP 27 and OTP 28+.
    filter_default: :log,
    filters: []
  })

Owl.LiveScreen.add_block(:dependency,
  state: :init,
  render: fn
    :init -> "init..."
    dependency -> ["dependency: ", Owl.Data.tag(dependency, :yellow)]
  end
)

Owl.LiveScreen.add_block(:compiling,
  render: fn
    :init -> "init..."
    filename -> ["compiling: ", Owl.Data.tag(to_string(filename), :cyan)]
  end
)

["ecto", "phoenix", "ex_doc", "broadway"]
|> Enum.each(fn dependency ->
  Owl.LiveScreen.update(:dependency, dependency)

  1..5
  |> Enum.map(&"filename#{&1}.ex")
  |> Enum.each(fn filename ->
    Owl.LiveScreen.update(:compiling, filename)
    Process.sleep(1000)
    Logger.debug("#{filename} compiled for dependency #{dependency}")
  end)
end)

Summary

Functions

Adds a sticky block to the bottom of the screen that can be updated using update/3.

Waits for the next rendering.

Renders data in buffer and resets the server state, removing all blocks.

Returns the current device used by the LiveScreen.

Updates the device used by LiveScreen.

Starts a server.

Renders data in buffer and terminates the server.

Updates the block state to be used in the next render iteration.

Types

add_block_option()

@type add_block_option() ::
  {:state, any()} | {:render, (block_state :: any() -> Owl.Data.t())}

block_id()

@type block_id() :: any()

start_option()

@type start_option() ::
  {:name, GenServer.name()}
  | {:refresh_every, pos_integer()}
  | {:terminal_width, pos_integer() | :auto}
  | {:device, IO.device()}

Functions

add_block(server \\ __MODULE__, block_id, opts)

@spec add_block(GenServer.server(), block_id(), [add_block_option()]) :: :ok

Adds a sticky block to the bottom of the screen that can be updated using update/3.

Options

  • :render - a function that accepts state and returns a view of the block. Defaults to Function.identity/1, which means state must have type Owl.Data.t/0.
  • :state - initial state of the block. Defaults to nil.

Example

Owl.LiveScreen.add_block(:footer, state: "starting...")
# which is equivalent to
Owl.LiveScreen.add_block(:footer, render: fn
  nil -> "starting..."
  data -> data
end)

await_render(server \\ __MODULE__)

@spec await_render(GenServer.server()) :: :ok

Waits for the next rendering.

Useful to ensure that the last published state is rendered on the screen.

Example

Owl.LiveScreen.add_block(:status, state: "starting...")
Owl.LiveScreen.update(:status, "done!")
Owl.LiveScreen.await_render()

capture_stdio(server \\ __MODULE__, callback)

@spec capture_stdio(GenServer.server(), (-> result)) :: result when result: any()

Redirects output from :stdio to Owl.LiveScreen.

Example

Owl.ProgressBar.start(id: :users, label: "Creating users", total: 100)

Owl.LiveScreen.capture_stdio(fn ->
  Enum.each(1..100, fn i ->
    Process.sleep(10)
    Owl.ProgressBar.inc(id: :users)
    # Output from the next call will be printed above the progress bar
    IO.inspect([:test, i])
  end)
end)

Owl.LiveScreen.await_render()

flush(server \\ __MODULE__)

@spec flush(GenServer.server()) :: :ok

Renders data in buffer and resets the server state, removing all blocks.

The server keeps running and can accept new blocks after this call. Use this to clear the screen and start a new set of blocks without restarting the server.

get_device(server \\ __MODULE__)

@spec get_device(GenServer.server()) :: IO.device()

Returns the current device used by the LiveScreen.

set_device(server \\ __MODULE__, new_device)

@spec set_device(GenServer.server(), IO.device()) :: :ok

Updates the device used by LiveScreen.

This is useful, for example, when using the Erlang SSH console.

Example

group_leader = Process.group_leader()

Owl.LiveScreen.set_device(group_leader)

start_link(opts)

@spec start_link([start_option()]) :: GenServer.on_start()

Starts a server.

The server is started automatically by the :owl application as a named process.

Options

  • :name - used for name registration as described in the "Name registration" section in the documentation for GenServer. Defaults to Owl.LiveScreen.
  • :refresh_every - the interval for refreshing the screen, in milliseconds. Defaults to 60.
  • :terminal_width - the terminal width in characters. Defaults to :auto, which uses Owl.IO.columns/1.
  • :device - an I/O device. Defaults to :stdio. If a terminal is not available, the server will not be started.

stop(server \\ __MODULE__)

@spec stop(GenServer.server()) :: :ok

Renders data in buffer and terminates the server.

Unlike flush/1, the server cannot be used after this call.

update(server \\ __MODULE__, block_id, block_state)

@spec update(GenServer.server(), block_id(), block_state :: any()) :: :ok

Updates the block state to be used in the next render iteration.

Example

Owl.LiveScreen.add_block(:footer, state: "starting...")
Process.sleep(1000)
Owl.LiveScreen.update(:footer, "...almost done...")
Process.sleep(1000)
Owl.LiveScreen.update(:footer, "done!!!")