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. Frames are wrapped in synchronized-update markers unless DRAFTER_NO_SYNC is set.
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. Output goes to the session's terminal driver, or, for the local terminal,
straight to /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 or when a text row beneath them was rewritten. 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.
Summary
Types
A rectangle of cells recorded as changed since the last frame.
Where an image sits and how big it is, as a widget's image/3 returns it.
The screen as one padded Drafter.Draw.Strip per row, top row first.
Functions
Returns a specification to start this module under a supervisor.
Hide the image region id and re-blank the cells it occupied.
Blank the whole screen buffer and withdraw every image region.
The composited screen buffer of the compositor at pid, one Strip per row.
The size of the screen buffer as {cols, rows}.
Position the image region id with its anchor at cell x, y and mark it visible.
Store the terminal-graphics bytes (kitty, iTerm2, sixel) for an image region.
Redraw every row on the next frame, keeping the buffer contents.
Blit strips into the screen buffer with their top-left corner at cell x, y.
Set the screen buffer to width by height cells.
Start a compositor.
Write bytes straight to this session's terminal, outside the screen buffer.
Types
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.
@type image_region() :: %{ :dx => integer(), :dy => integer(), :cols => non_neg_integer(), :rows => non_neg_integer(), optional(:stamp) => integer(), optional(:place) => 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.
@type screen_buffer() :: [Drafter.Draw.Strip.t()]
The screen as one padded Drafter.Draw.Strip per row, top row first.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@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.
@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.
@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.
@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.
@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.
@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 sameidreplaces its bytes, keeping the positionplace_image/3gave itpaint— the sequence that draws the imageclear— the sequence that removes it, for protocols that hold an image outside the cell grid;""for protocols where re-blanking the cells is enoughregion— where and how big the image is, as the placement map a widget'simage/3returns::dx,:dy— cell offset of the image from the position given toplace_image/3, so the image is drawn atx + 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 thisid, default0:place— sequence that redraws the image the terminal is already holding, defaultnil
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.
@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.
@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)
@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.
@spec start_link(keyword()) :: GenServer.on_start()
Start a compositor.
Options:
:name— registered name, defaultDrafter.Compositor. Passnilto 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 exportswrite/2andget_size/1.:event_manager— manager to subscribe to for resize events, defaultDrafter.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.
@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.