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

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_lines`
    are kept, and the list is ignored when `:file_path` names an existing file
  * `:file_path` - path to a file, indexed and read on demand. Default `nil`.
    Takes precedence over `:lines` when the file exists
  * `:max_lines` - maximum number of lines kept in memory. Default `1000`
  * `:auto_scroll` - `t:boolean/0`, jump back to the newest line after each write.
    Default `true`
  * `:wrap` - `t:boolean/0`, wrap long lines instead of truncating them. Default
    `true`
  * `:highlight` - `t:boolean/0`, apply basic token highlighting to bracketed
    numbers, quoted strings, numbers and the literals `true`, `false` and `nil`.
    Default `false`
  * `:border` - `t:boolean/0`, draw a single-line box border, which costs two rows
    and two columns of content. Default `false`
  * `:style` - `t:map/0` of style properties. Default `%{}`
  * `:class` - theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`
  * `:height` - read only by `preferred_height/2`, never by `mount/1`. Default
    `10`

`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)

# `t`

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

# `apply_data_buffer`

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

# `component_tag`

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

The registry tag for this widget.

    iex> Drafter.Widget.Log.component_tag()
    :log

# `focused`

# `from_component_opts`

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

# `handle_event`

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

# `mount`

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

# `preferred_height`

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

# `render`

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

# `unmount`

# `update`

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

# `update_props_from_mount`

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

---

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