# `Drafter.Compositor`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/render/compositor.ex#L1)

Holds one session's screen as rows of styled cells and writes changed rows to its terminal.

The screen buffer is a list of `Drafter.Draw.Strip`, one per row, each padded to the
screen width. `render_strips/3` blits strips into it at a cell position; the frame is
written on the next `:render_frame` message, and only rows whose cache key changed are
sent, wrapped in synchronized-update markers unless `DRAFTER_NO_SYNC` is set. Every
byte for the terminal goes through the session's `Drafter.Render.Writer`. While text
handed to it is still unwritten no new frame is written, so a link that cannot keep up
sees fewer, complete frames, the rows changed meanwhile merged into one diff; a frame's
image bytes follow its text and are written only while no newer image has arrived.

    Drafter.Compositor.render_strips([Drafter.Draw.Strip.from_text("hello")], 2, 0)

One compositor exists per session and is resolved through `Drafter.Session.Context`
under the `:compositor` key, so the module-level functions address the caller's own
session. The writer's sink is the session's terminal driver, or, for the local
terminal, `/dev/tty` unless `DRAFTER_NO_PACED_WRITE` is set.

A resize arrives as `{:tui_event, {:resize, {cols, rows}}}` from the event manager;
the buffer is rebuilt empty at the new size and the whole screen is marked dirty.

## Images

Terminal-graphics bytes live outside the cell grid. A widget registers them with
`put_image/4`, positions them with `place_image/3` and withdraws them with
`clear_image/1`. Images are drawn after the text of a frame, and are redrawn when
their bytes or position changed. A text row an image lies on is written around the
image, the cells it covers left alone, so text changing beside an image never touches
it. An image whose rectangle does not fit entirely on screen is not drawn at all.

Stamps order concurrent generations: a `put_image/4` whose `stamp` is not greater
than the highest stamp already accepted for that id is discarded. `clear_image/1`
forgets the id's stamp, so the next `put_image/4` for it is accepted whatever its
stamp.

# `dirty_region`

```elixir
@type dirty_region() :: %{
  x: integer(),
  y: integer(),
  width: integer(),
  height: integer()
}
```

A rectangle of cells recorded as changed since the last frame.

Whether any region is recorded is what decides that a frame is due; which rows
are actually written is decided by comparing strip cache keys. `width` and
`height` are therefore not clipped to the screen and can be zero or negative for
a rectangle that starts past an edge.

# `image_region`

```elixir
@type image_region() :: %{
  :dx =&gt; integer(),
  :dy =&gt; integer(),
  :cols =&gt; non_neg_integer(),
  :rows =&gt; non_neg_integer(),
  optional(:stamp) =&gt; integer(),
  optional(:place) =&gt; iodata() | nil
}
```

Where an image sits and how big it is, as a widget's `image/3` returns it.

`:stamp` and `:place` are optional, defaulting to `0` and `nil`.

# `screen_buffer`

```elixir
@type screen_buffer() :: [Drafter.Draw.Strip.t()]
```

The screen as one padded `Drafter.Draw.Strip` per row, top row first.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear_image`

```elixir
@spec clear_image(term()) :: :ok
```

Hide the image region `id` and re-blank the cells it occupied.

Queues the region's `clear` sequence and marks the rows it covered dirty, so the
text beneath is written again. Its bytes and position are kept, so a later
`place_image/3` shows it again.

The id's stamp is forgotten either way, so the next `put_image/4` for it is
accepted whatever its stamp. A region that is already hidden or has no bytes yet
is only marked hidden, and an unknown id is ignored; neither schedules a frame.
Asynchronous.

# `clear_screen`

```elixir
@spec clear_screen() :: :ok
```

Blank the whole screen buffer and withdraw every image region.

Each image's `clear` sequence is queued so the terminal releases it. The regions
themselves are forgotten, bytes, positions and stamps alike, so a later
`place_image/3` for the same id shows nothing until `put_image/4` supplies bytes
again. Asynchronous.

# `get_buffer`

```elixir
@spec get_buffer(pid()) :: screen_buffer()
```

The composited screen buffer of the compositor at `pid`, one `Strip` per row.

Takes an explicit pid rather than resolving the session, so a caller outside the
session can read it. The rows returned are what the next frame will write from,
which is not necessarily what is on the terminal yet. Synchronous.

# `get_screen_size`

```elixir
@spec get_screen_size() :: {pos_integer(), pos_integer()}
```

The size of the screen buffer as `{cols, rows}`.

This is the size the buffer was built at, not a fresh measurement of the
terminal. Synchronous.

# `place_image`

```elixir
@spec place_image(term(), non_neg_integer(), non_neg_integer()) :: :ok
```

Position the image region `id` with its anchor at cell `x`, `y` and mark it visible.

Carries no image bytes. Calling it for an `id` that has no bytes yet records the
position; nothing is drawn until `put_image/4` supplies them. The image is drawn
after the text of a frame, and only when its bytes or position changed or a text
row under it was redrawn. Asynchronous.

# `put_image`

```elixir
@spec put_image(term(), iodata(), iodata(), image_region()) :: :ok
```

Store the terminal-graphics bytes (kitty, iTerm2, sixel) for an image region.

Arguments:

  * `id` — any term identifying the region; a later call with the same `id`
    replaces its bytes, keeping the position `place_image/3` gave it
  * `paint` — the sequence that draws the image
  * `clear` — the sequence that removes it, for protocols that hold an image
    outside the cell grid; `""` for protocols where re-blanking the cells is enough
  * `region` — where and how big the image is, as the placement map a widget's
    `image/3` returns:

    * `:dx`, `:dy` — cell offset of the image from the position given to
      `place_image/3`, so the image is drawn at `x + dx`, `y + dy`
    * `:cols`, `:rows` — size of the image in cells, used to decide whether it fits
      on screen and which text rows it covers
    * `:stamp` — generation counter for this `id`, default `0`
    * `:place` — sequence that redraws the image the terminal is already holding,
      default `nil`

A call is discarded outright, changing nothing, when `:stamp` is less than or equal
to the stamp of the last accepted call for the same `id`. The first call for an
`id` is always accepted, as is the first call after a `clear_image/1`, which
forgets the id's stamp. Callers that generate images concurrently must pass a
counter that only ever increases for a given `id`; passing the default `0` every
time means every call after the first is dropped.

`place` is sent instead of `paint` when the image is unchanged and is only being
drawn again because text was written across it. `nil` sends `paint` in that case
too.

Registering bytes does not make the image visible; `place_image/3` does.
Asynchronous.

# `refresh`

```elixir
@spec refresh() :: :ok
```

Redraw every row on the next frame, keeping the buffer contents.

Use after something outside the compositor has written to the terminal, which
makes the record of what is on screen untrustworthy. Asynchronous.

# `render_strips`

```elixir
@spec render_strips([Drafter.Draw.Strip.t()], non_neg_integer(), non_neg_integer()) ::
  :ok
```

Blit `strips` into the screen buffer with their top-left corner at cell `x`, `y`.

One strip per row, applied downwards from `y`. `x` and `y` are zero-based and
both default to `0`. Rows that fall outside the screen are dropped. A row is
padded with spaces out to the screen width, and an `x` of `0` or less replaces
the whole row. Callers are responsible for supplying strips that fit: a strip
reaching past the right edge leaves that row longer than the screen.

The affected rectangle is marked dirty and a frame is scheduled. Asynchronous.

    Drafter.Compositor.render_strips([Drafter.Draw.Strip.from_text("hello")], 2, 0)

# `resize`

```elixir
@spec resize(pos_integer(), pos_integer()) :: :ok
```

Set the screen buffer to `width` by `height` cells.

The buffer is rebuilt blank at the new size, every image region is withdrawn as
by `clear_screen/0` and the whole screen is redrawn on the next frame. Returns
before any of that has happened.

This is the same path a `{:resize, {cols, rows}}` event from the event manager
takes, so calling it does not stop the terminal's own size from winning later.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start a compositor.

Options:

  * `:name` — registered name, default `Drafter.Compositor`. Pass `nil` to start
    it unregistered, which is what a session that is not the local terminal does.
  * `:terminal_driver` — `Drafter.Terminal.Driver` (the default) or a
    `{module, pid}` pair whose module exports `write/2` and `get_size/1`.
  * `:event_manager` — manager to subscribe to for resize events, default
    `Drafter.Event.Manager`.

The initial screen size is read from the terminal driver. When the driver is
`Drafter.Terminal.Driver` and `DRAFTER_NO_PACED_WRITE` is unset, `/dev/tty` is
opened once here and every frame is written to it instead of through the driver;
it is closed on termination.

# `sync`

```elixir
@spec sync(pid()) :: :ok
```

Returns once everything handed to the compositor before this call is on the
terminal: a frame still waiting for its `:render_frame` is written now, and the
writer has passed every byte to the driver. Takes an explicit pid, like
`get_buffer/1`. Synchronous.

# `write_raw`

```elixir
@spec write_raw(iodata()) :: :ok
```

Write bytes straight to this session's terminal, outside the screen buffer.

For control sequences addressed to the terminal itself rather than to the screen,
such as an OSC 52 clipboard write. The bytes go to the terminal this session is
attached to, which for a remote session is the ssh or telnet client rather than
the tty the server was started from. Nothing is written to the cell grid and no
row is marked dirty.

---

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