Drafter.Widget.RichLog (drafter v0.3.2)

Copy Markdown View Source

Renders a scrollable log panel where each line can carry per-line style metadata.

Lines are plain strings or {text, meta} tuples. The meta map may include :color, :background, :bold, :dim, :italic, and :underline keys to style individual entries. When :reverse is true (default), the newest line appears at the bottom and the view auto-scrolls to follow new output. Optional line-number gutters are controlled by :show_line_numbers.

Append lines via {:write, content} or {:write_lines, lines} events. Send :clear to reset the buffer.

Component tag

Tag :rich_log, built by Drafter.App as {:rich_log, opts}:

rich_log(opts)

There is no positional argument; every prop comes from opts. Lines added after mount arrive as {:write, content} / {:write_lines, lines} events rather than through props.

Options

  • :lines - [String.t() | {String.t(), map()}]. Default []. Trimmed to the newest :max_lines; a bare string becomes {string, %{}} and anything else goes through to_string/1.

  • :max_lines - pos_integer/0 lines kept in memory. Default 1000.
  • :auto_scroll - boolean/0, jump back to the newest line on every write. Default true. Only read while handling {:write, content} and {:write_lines, lines}.
  • :wrap - boolean/0, wrap a long line over several rows instead of truncating it. Default true.
  • :reverse - boolean/0, newest line at the bottom, and drop overflow from the top. Default true. With false the view starts at the top and overflow is dropped from the bottom.
  • :show_line_numbers - boolean/0, draw a line number gutter. Default false. The gutter is as wide as the current line count plus two.
  • :style - map/0 of style overrides passed to the theme computation. Default %{}.
  • :class - theme class atom or list of them, normalised by Drafter.Style.normalize_classes/1 and reaching mount/1 as :classes. Default [].
  • :height - pos_integer/0 read only by preferred_height/2, never by mount/1. Default 10.

Per-line metadata

The meta map of a {text, meta} line may carry :color (default: the theme's colour, falling back to {200, 200, 200}), :background (default: the theme's background, falling back to {30, 30, 30}), and :bold, :dim, :italic and :underline, each false by default.

update/2 accepts every option above. Through the component tree update_props_from_mount/3 narrows that to :lines, :max_lines, :auto_scroll, :wrap, :reverse and :show_line_numbers, making :style, :classes and :app_module mount-only.

Usage

rich_log(lines: [
  {"INFO  Connected", %{color: {100, 200, 100}}},
  {"ERROR Timeout",   %{color: {255, 80, 80}, bold: true}}
])

Summary

Functions

Replaces :lines with the newest :max_lines entries of a Drafter.RingBuffer, normalised into {text, meta} tuples.

The component tag this widget registers under.

Builds the props map for a {:rich_log, opts} element.

Handles the log's own events, replacing the dispatch use Drafter.Widget would otherwise generate.

Builds the widget state from props.

The number of rows the element asks for: opts[:height], default 10.

Draws the visible slice of the log into rect, always returning exactly rect.height strips.

Callback implementation for Drafter.Widget.unmount/1.

Replaces the state fields named in props, keeping the current value for any key that is absent.

Narrows a re-render to :lines, :max_lines, :auto_scroll, :wrap, :reverse and :show_line_numbers.

Types

rich_line()

@type rich_line() :: {String.t(), map()}

t()

@type t() :: %Drafter.Widget.RichLog{
  app_module: module() | nil,
  auto_scroll: boolean(),
  classes: [atom()],
  lines: [rich_line()],
  max_lines: pos_integer(),
  reverse: boolean(),
  scroll_offset: non_neg_integer(),
  show_line_numbers: boolean(),
  style: map(),
  wrap: boolean()
}

Functions

apply_data_buffer(state, buffer, rect)

@spec apply_data_buffer(t(), Drafter.RingBuffer.t(), Drafter.Widget.rect()) :: t()

Replaces :lines with the newest :max_lines entries of a Drafter.RingBuffer, normalised into {text, meta} tuples.

The current lines are discarded whatever the buffer holds, including when it is empty. The rect is ignored.

component_tag()

@spec component_tag() :: :rich_log

The component tag this widget registers under.

iex> Drafter.Widget.RichLog.component_tag()
:rich_log

focused(state)

from_component_opts(args, opts)

@spec from_component_opts(
  term(),
  keyword()
) :: Drafter.Widget.props()

Builds the props map for a {:rich_log, opts} element.

The positional argument is ignored. :lines is passed through as given — mount/1 does the normalising — :class is normalised into :classes and :__app_module__ becomes :app_module.

iex> props = Drafter.Widget.RichLog.from_component_opts(nil, lines: ["a"])
iex> {props.lines, props.max_lines, props.reverse}
{["a"], 1000, true}

handle_event(event, state)

@spec handle_event(term(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:noreply, t()}

Handles the log's own events, replacing the dispatch use Drafter.Widget would otherwise generate.

A plain props map is passed through mount/1 first. Recognised events, each returning {:ok, new_state}:

  • {:write, content} - append one line; content is a string or a {text, meta} tuple
  • {:write_lines, lines} - append a list of lines
  • :clear - empty the buffer and reset the scroll offset
  • {:key, :end} - scroll back to the newest line
  • {:key, :home} - reset the scroll offset to zero
  • {:key, :page_down} / {:key, :page_up} - move by ten lines
  • {:key, :down} / {:key, :up} - move by one line

A write scrolls back to the newest line when :auto_scroll is set. Only the two-element {:key, key} form is matched; every other event, including {:key, key, mods}, returns {:noreply, state}.

iex> state = Drafter.Widget.RichLog.mount(%{})
iex> {:ok, written} = Drafter.Widget.RichLog.handle_event({:write, "hello"}, state)
iex> written.lines
[{"hello", %{}}]

iex> state = Drafter.Widget.RichLog.mount(%{lines: ["a"]})
iex> {:ok, cleared} = Drafter.Widget.RichLog.handle_event(:clear, state)
iex> {cleared.lines, cleared.scroll_offset}
{[], 0}

iex> state = Drafter.Widget.RichLog.mount(%{})
iex> {:ok, scrolled} = Drafter.Widget.RichLog.handle_event({:key, :page_up}, state)
iex> scrolled.scroll_offset
10

iex> state = Drafter.Widget.RichLog.mount(%{})
iex> Drafter.Widget.RichLog.handle_event({:key, :up, []}, state) == {:noreply, state}
true

mount(props)

@spec mount(Drafter.Widget.props()) :: t()

Builds the widget state from props.

Every option listed in the module doc is read here with the default stated there. :lines is normalised into {text, meta} tuples and trimmed to the newest :max_lines, and :scroll_offset always starts at 0.

iex> state = Drafter.Widget.RichLog.mount(%{lines: ["a", {"b", %{bold: true}}]})
iex> state.lines
[{"a", %{}}, {"b", %{bold: true}}]

iex> state = Drafter.Widget.RichLog.mount(%{lines: ["a", "b", "c"], max_lines: 2})
iex> state.lines
[{"b", %{}}, {"c", %{}}]

iex> state = Drafter.Widget.RichLog.mount(%{})
iex> {state.max_lines, state.auto_scroll, state.wrap, state.reverse, state.show_line_numbers}
{1000, true, true, true, false}

preferred_height(args, opts)

@spec preferred_height(
  term(),
  keyword()
) :: pos_integer()

The number of rows the element asks for: opts[:height], default 10.

iex> Drafter.Widget.RichLog.preferred_height(nil, [])
10

iex> Drafter.Widget.RichLog.preferred_height(nil, height: 30)
30

render(state, rect)

Draws the visible slice of the log into rect, always returning exactly rect.height strips.

state may be a plain props map, in which case it is passed through mount/1 first. Short output is padded with blank rows at the bottom. Overflow is dropped from the top when :reverse is set and from the bottom otherwise.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

@spec update(Drafter.Widget.props(), t()) :: t()

Replaces the state fields named in props, keeping the current value for any key that is absent.

New :lines are normalised and trimmed to :max_lines, and so are the existing lines when props lowers :max_lines. :scroll_offset is never touched here.

iex> state = Drafter.Widget.RichLog.mount(%{lines: ["a", "b", "c"]})
iex> Drafter.Widget.RichLog.update(%{max_lines: 2}, state).lines
[{"b", %{}}, {"c", %{}}]

iex> state = Drafter.Widget.RichLog.mount(%{lines: ["a"]})
iex> Drafter.Widget.RichLog.update(%{wrap: false}, state).lines
[{"a", %{}}]

update_props_from_mount(mount_props, existing_state, opts)

@spec update_props_from_mount(Drafter.Widget.props(), term(), keyword()) ::
  Drafter.Widget.props()

Narrows a re-render to :lines, :max_lines, :auto_scroll, :wrap, :reverse and :show_line_numbers.

:style, :classes and :app_module are dropped, so they are mount-only through the component tree.

iex> props = Drafter.Widget.RichLog.from_component_opts(nil, lines: ["a"])
iex> Drafter.Widget.RichLog.update_props_from_mount(props, %{}, []) |> Map.keys() |> Enum.sort()
[:auto_scroll, :lines, :max_lines, :reverse, :show_line_numbers, :wrap]