# `Drafter.Widget.RichLog`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/rich_log.ex#L1)

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` - `t:pos_integer/0` lines kept in memory. Default `1000`.
  * `:auto_scroll` - `t:boolean/0`, jump back to the newest line on every write.
    Default `true`. Only read while handling `{:write, content}` and
    `{:write_lines, lines}`.
  * `:wrap` - `t:boolean/0`, wrap a long line over several rows instead of
    truncating it. Default `true`.
  * `:reverse` - `t: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` - `t:boolean/0`, draw a line number gutter. Default
    `false`. The gutter is as wide as the current line count plus two.
  * `:style` - `t: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` - `t: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}}
    ])

# `rich_line`

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

# `t`

```elixir
@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()
}
```

# `apply_data_buffer`

```elixir
@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`

```elixir
@spec component_tag() :: :rich_log
```

The component tag this widget registers under.

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

# `focused`

# `from_component_opts`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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.

# `unmount`

# `update`

```elixir
@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`

```elixir
@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]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
