Renders a scrollable plain-text log panel that accepts streamed line output.
Lines are appended via {:write, line} or {:write_lines, lines} events.
When :auto_scroll is enabled (default), the view tracks the newest lines.
Alternatively a file path can be provided; the widget builds a byte-offset
index for efficient random access into large files without loading them fully
into memory.
Scroll keys are handled when they reach the widget: ↑/↓ scroll by one line,
Page Up/Page Down by ten, Home/End jump to the top and bottom. The widget
declares no event kinds and is not focusable, so those keys have to be delivered
to handle_event/2 some other way.
Component tag
Tag :log, built by Drafter.App as {:log, opts}:
log(opts)There is no positional argument; every prop comes from opts.
Options
:lines- initial list of strings. Default[]. Only the newest:max_linesare kept, and the list is ignored when:file_pathnames an existing file:file_path- path to a file, indexed and read on demand. Defaultnil. Takes precedence over:lineswhen the file exists:max_lines- maximum number of lines kept in memory. Default1000:auto_scroll-boolean/0, jump back to the newest line after each write. Defaulttrue:wrap-boolean/0, wrap long lines instead of truncating them. Defaulttrue:highlight-boolean/0, apply basic token highlighting to bracketed numbers, quoted strings, numbers and the literalstrue,falseandnil. Defaultfalse:border-boolean/0, draw a single-line box border, which costs two rows and two columns of content. Defaultfalse:style-map/0of style properties. Default%{}:class- theme class atom or list of them, reachingmount/1as:classes. Default[]:height- read only bypreferred_height/2, never bymount/1. Default10
update/2 re-reads every option, rebuilding the file index when :file_path
changes to a file that exists. Through the component tree a re-render narrows that
to :file_path, :lines and :app_module, so everything else is effectively
mount-only there.
Widget value
Drafter.get_widget_value/1 is not implemented for this widget and returns nil.
Data channel
When the widget is declared with a data buffer, apply_data_buffer/3 replaces
:lines with the newest :max_lines items in the buffer.
Usage
log(lines: ["Starting...", "Done."], auto_scroll: true)
log(file_path: "/var/log/app.log", highlight: true)
Summary
Functions
Replaces :lines with the newest :max_lines items in the widget's data buffer.
The registry tag for this widget.
Turns the {:log, opts} element into a props map for mount/1.
Appends output and scrolls the view.
Builds the log state from props.
opts[:height], or 10 when it is absent.
Draws the visible window of the log into rect, returning exactly rect.height
strips.
Callback implementation for Drafter.Widget.unmount/1.
Folds fresh props into state.
Narrows the props a re-render feeds to update/2 to :file_path, :lines and
:app_module, so every other option stays as mounted.
Types
@type t() :: %Drafter.Widget.Log{ app_module: module() | nil, auto_scroll: boolean(), border: boolean(), classes: [atom()], file_path: Path.t() | nil, highlight: boolean(), line_offsets: [non_neg_integer()], lines: [String.t()], max_lines: pos_integer(), scroll_offset: non_neg_integer(), style: map(), total_lines: non_neg_integer(), wrap: boolean() }
Functions
@spec apply_data_buffer(t(), Drafter.RingBuffer.t(), Drafter.Widget.rect()) :: t()
Replaces :lines with the newest :max_lines items in the widget's data buffer.
The buffer contents become the whole log, they are not appended, and
:scroll_offset is left where it was.
@spec component_tag() :: :log
The registry tag for this widget.
iex> Drafter.Widget.Log.component_tag()
:log
@spec from_component_opts( term(), keyword() ) :: Drafter.Widget.props()
Turns the {:log, opts} element into a props map for mount/1.
The positional argument is ignored. :class is normalised into :classes and
:__app_module__ becomes :app_module.
iex> props = Drafter.Widget.Log.from_component_opts(nil, lines: ["a"])
iex> {props.lines, props.max_lines, props.auto_scroll, props.wrap, props.border}
{["a"], 1000, true, true, false}
@spec handle_event(Drafter.Event.t() | tuple() | :clear, t() | Drafter.Widget.props()) :: {:ok, t()} | {:noreply, t()}
Appends output and scrolls the view.
{:write, line} appends one line and {:write_lines, lines} appends a list, both
trimming to the newest :max_lines and jumping to the bottom when :auto_scroll
is on. :clear empties the buffer and resets the scroll offset. {:key, :end}
and {:key, :home} jump to the newest and oldest line, {:key, :page_down} and
{:key, :page_up} move ten lines, and {:key, :down} and {:key, :up} move one.
All of those return {:ok, state}; everything else returns {:noreply, state}.
Writes go to the in-memory buffer even when the widget was mounted with a
:file_path, in which case the rendered lines still come from the file.
iex> l = Drafter.Widget.Log.mount(%{lines: ["a"]})
iex> {:ok, written} = Drafter.Widget.Log.handle_event({:write, "b"}, l)
iex> written.lines
["a", "b"]
iex> l = Drafter.Widget.Log.mount(%{lines: ["a", "b"], max_lines: 2})
iex> {:ok, written} = Drafter.Widget.Log.handle_event({:write_lines, ["c", "d"]}, l)
iex> written.lines
["c", "d"]
iex> l = Drafter.Widget.Log.mount(%{lines: ["a", "b"]})
iex> {:ok, cleared} = Drafter.Widget.Log.handle_event(:clear, l)
iex> {cleared.lines, cleared.scroll_offset}
{[], 0}
iex> l = Drafter.Widget.Log.mount(%{lines: ["a"]})
iex> Drafter.Widget.Log.handle_event({:key, :enter}, l) |> elem(0)
:noreply
@spec mount(Drafter.Widget.props()) :: t()
Builds the log state from props.
With a :file_path that exists, the file is indexed by byte offset and :lines
is ignored. Otherwise the newest :max_lines entries of :lines are kept.
:scroll_offset always starts at 0, at the newest line.
iex> l = Drafter.Widget.Log.mount(%{lines: ["a", "b"]})
iex> {l.lines, l.total_lines, l.max_lines, l.auto_scroll, l.wrap}
{["a", "b"], 2, 1000, true, true}
iex> l = Drafter.Widget.Log.mount(%{lines: ["a", "b", "c"], max_lines: 2})
iex> {l.lines, l.total_lines}
{["b", "c"], 2}
iex> l = Drafter.Widget.Log.mount(%{})
iex> {l.lines, l.file_path, l.scroll_offset, l.highlight, l.border, l.classes}
{[], nil, 0, false, false, []}
@spec preferred_height( term(), keyword() ) :: pos_integer()
opts[:height], or 10 when it is absent.
iex> Drafter.Widget.Log.preferred_height(nil, [])
10
iex> Drafter.Widget.Log.preferred_height(nil, height: 30)
30
@spec render(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: [ Drafter.Draw.Strip.t() ]
Draws the visible window of the log into rect, returning exactly rect.height
strips.
Accepts either a t/0 or a raw props map, which is mounted first. Lines are
taken from the end of the buffer backwards by :scroll_offset, wrapped or
truncated to the content width according to :wrap, and padded with blank rows.
With :border set the content area loses one row and one column on each side.
Callback implementation for Drafter.Widget.unmount/1.
@spec update(Drafter.Widget.props(), t()) :: t()
Folds fresh props into state.
Re-reads :max_lines, :auto_scroll, :wrap, :style, :classes,
:app_module, :file_path, :highlight and :border. A :file_path that
differs from the current one and names an existing file rebuilds the byte-offset
index and replaces the content. Otherwise :lines replaces the buffer when it is
present and actually different, trimmed to the newest :max_lines. A nil
:file_path never clears the one already set.
iex> l = Drafter.Widget.Log.mount(%{lines: ["a"]})
iex> updated = Drafter.Widget.Log.update(%{lines: ["x", "y"]}, l)
iex> {updated.lines, updated.total_lines}
{["x", "y"], 2}
@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) :: Drafter.Widget.props()
Narrows the props a re-render feeds to update/2 to :file_path, :lines and
:app_module, so every other option stays as mounted.