Drafter.Terminal.InputBuffer (drafter v0.5.0)

Copy Markdown View Source

Carries partially-received terminal input across reads.

Terminal input arrives in arbitrary chunks, and a control sequence can be split across two of them. feed/2 decodes whatever is complete and holds the undecided tail until the rest arrives.

A trailing lone ESC is held too, since it is both a complete keypress and the start of every escape sequence. flush/1 resolves it as the escape key. Whenever bytes are held, an :input_flush message is scheduled to the calling process after flush_after_ms/0; drivers resolve the tail by calling flush/1 on receipt.

The buffer also remembers which mouse buttons are down, from the presses and releases it decodes. A motion report that names a button the terminal never reported pressed, or reported released since, is delivered as a :move, not a :drag: a terminal in any-motion mode may encode a free pointer's motion with a button number rather than the "no button" code.

It also remembers the last {:cell_size, size} and {:pixel_mouse, on?} reports it decoded, and passes both to every parse, so SGR mouse reports are read as pixels while the terminal has confirmed pixel mode and the cell size is known. See "Pixel mouse reports" in Drafter.Terminal.ANSI.

iex> buffer = Drafter.Terminal.InputBuffer.new()
iex> {events, buffer} = Drafter.Terminal.InputBuffer.feed(buffer, "ab\e[")
iex> events
[{:key, :a}, {:key, :b}]
iex> {events, _buffer} = Drafter.Terminal.InputBuffer.feed(buffer, "A")
iex> events
[{:key, :up}]

Summary

Functions

Append a chunk and decode whatever is now complete.

Resolve any held bytes without waiting for more input.

Milliseconds of silence after which a held sequence is resolved.

An empty buffer, holding no bytes and with no flush scheduled.

Discard held bytes and any pending flush, decoding nothing.

Types

t()

@type t() :: %Drafter.Terminal.InputBuffer{
  cell_size: {pos_integer(), pos_integer()} | nil,
  held: MapSet.t(atom()),
  key_release: boolean(),
  pending: binary(),
  pixel_mouse: boolean(),
  timer: reference() | nil
}

Functions

feed(buffer, data)

@spec feed(t(), binary()) :: {[Drafter.Terminal.ANSI.event()], t()}

Append a chunk and decode whatever is now complete.

Returns the decoded events and the updated buffer. When bytes remain undecided an :input_flush message is scheduled to the calling process, replacing any previously scheduled one.

iex> {events, buffer} = Drafter.Terminal.InputBuffer.feed(Drafter.Terminal.InputBuffer.new(), "hi\e")
iex> events
[{:key, :h}, {:key, :i}]
iex> buffer.pending
"\e"

flush(buffer)

@spec flush(t()) :: {[Drafter.Terminal.ANSI.event()], t()}

Resolve any held bytes without waiting for more input.

Call on :input_flush, or when the input stream closes. Any scheduled flush is cancelled.

iex> {_events, buffer} = Drafter.Terminal.InputBuffer.feed(Drafter.Terminal.InputBuffer.new(), "\e")
iex> {events, %Drafter.Terminal.InputBuffer{pending: "", timer: nil}} = Drafter.Terminal.InputBuffer.flush(buffer)
iex> events
[{:key, :escape}]

An empty buffer yields no events.

iex> {[], %Drafter.Terminal.InputBuffer{pending: ""}} = Drafter.Terminal.InputBuffer.flush(Drafter.Terminal.InputBuffer.new())

flush_after_ms()

@spec flush_after_ms() :: pos_integer()

Milliseconds of silence after which a held sequence is resolved.

iex> Drafter.Terminal.InputBuffer.flush_after_ms()
40

new(opts \\ [])

@spec new(keyword()) :: t()

An empty buffer, holding no bytes and with no flush scheduled.

iex> Drafter.Terminal.InputBuffer.new()
%Drafter.Terminal.InputBuffer{pending: "", timer: nil, key_release: false}

Options

reset(buffer)

@spec reset(t()) :: t()

Discard held bytes and any pending flush, decoding nothing.

iex> {_events, buffer} = Drafter.Terminal.InputBuffer.feed(Drafter.Terminal.InputBuffer.new(), "\e[")
iex> %Drafter.Terminal.InputBuffer{pending: "", timer: nil} = Drafter.Terminal.InputBuffer.reset(buffer)