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 throughto_string/1.:max_lines-pos_integer/0lines kept in memory. Default1000.:auto_scroll-boolean/0, jump back to the newest line on every write. Defaulttrue. Only read while handling{:write, content}and{:write_lines, lines}.:wrap-boolean/0, wrap a long line over several rows instead of truncating it. Defaulttrue.:reverse-boolean/0, newest line at the bottom, and drop overflow from the top. Defaulttrue. Withfalsethe view starts at the top and overflow is dropped from the bottom.:show_line_numbers-boolean/0, draw a line number gutter. Defaultfalse. The gutter is as wide as the current line count plus two.:style-map/0of style overrides passed to the theme computation. Default%{}.:class- theme class atom or list of them, normalised byDrafter.Style.normalize_classes/1and reachingmount/1as:classes. Default[].:height-pos_integer/0read only bypreferred_height/2, never bymount/1. Default10.
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
@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
@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.
@spec component_tag() :: :rich_log
The component tag this widget registers under.
iex> Drafter.Widget.RichLog.component_tag()
:rich_log
@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}
@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;contentis 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
@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}
@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
@spec render(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: [ Drafter.Draw.Strip.t() ]
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.
Callback implementation for Drafter.Widget.unmount/1.
@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", %{}}]
@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]