Atui.TextInput (Atui v0.3.0)

Copy Markdown View Source

A single-line text field a view holds in its own state.

Like Atui.Panes, this is a struct with callbacks rather than a view: it owns no process and no part of the screen. The host keeps one in its state, offers keys to it, and draws it into a row of its own screen — so a field can sit in a border, in a table header, in a popup, wherever the host has a row to spare.

Holding one

defmodule Search do
  use Atui.View

  alias Atui.TextInput

  def mount(_opts), do: {:ok, %{input: TextInput.new(prompt: "search: ")}}

  def handle_key(:enter, state), do: {:ok, submit(state)}

  def handle_key(key, state) do
    case state.input |> TextInput.handle_key(key) |> TextInput.into(state) do
      {:pass, state} -> shortcut(state, key)
      reply -> reply
    end
  end

  def render(state, rect) do
    Screen.new(rect.width, rect.height)
    |> Screen.box(rect)
    |> TextInput.draw(state.input, Rect.new(1, 1, rect.width - 2, 1))
  end
end

into/3 rewrites a reply about the field into a reply about the state that holds it, which keeps the delegation one line long. Bind the keys the host wants for itself before delegating: a field claims every printable character, so a list underneath one navigates with the arrows and Enter, not with letters.

Which keys it claims

Printable characters, and the editing keys a readline user expects:

one character
Ctrl/Alt with one word
Home, End, Ctrl-A, Ctrl-Eeither end
Backspace, Deleteone character either side of the cursor
Ctrl-Wthe word before the cursor
Ctrl-U, Ctrl-Kto the start, to the end

Everything else — Enter, Tab, ESC, the vertical arrows — comes back as {:pass, input} for the host to deal with. A field never swallows a key it has no use for.

The cursor

Atui paints cells, and the terminal's own cursor stays hidden in the alternate screen, so the cursor here is a cell: draw/4 reverses the one the cursor sits on. That is why a field needs no cooperation from the runtime to be drawn anywhere — and why an unfocused field, drawn with focus: false, simply has no cursor in it.

Long values scroll horizontally: what is drawn is the window of the value that keeps the cursor in view, which is a pure function of the value, the cursor and the width, so nothing about scrolling is kept in the struct.

Summary

Functions

Empties the field.

Draws the field into one row of screen.

True while there is nothing in the field.

Offers a key to the field.

Rewrites a reply about the field into a reply about the state holding it.

Builds a field.

Replaces the text, putting the cursor at the end of it.

The text in the field.

The window of the value that draw/4 would show in width columns.

Types

reply()

@type reply() :: {:ok, t()} | {:pass, t()}

t()

@type t() :: %Atui.TextInput{
  cursor: non_neg_integer(),
  max_length: pos_integer() | nil,
  placeholder: String.t(),
  prompt: String.t(),
  value: String.t()
}

Functions

clear(input)

@spec clear(t()) :: t()

Empties the field.

draw(screen, input, rect, opts \\ [])

@spec draw(Atui.Screen.t(), t(), Atui.Rect.t(), keyword()) :: Atui.Screen.t()

Draws the field into one row of screen.

rect is where it goes; only its first row is used. Options:

  • :style — the value's style
  • :prompt_style — the prompt's (defaults to :style)
  • :placeholder_style — the placeholder's (defaults to :style)
  • :cursor_style — the cell the cursor is on (reverse video by default)
  • :focus — draw the cursor at all (default true)

empty?(text_input)

@spec empty?(t()) :: boolean()

True while there is nothing in the field.

handle_key(input, key)

@spec handle_key(t(), Atui.Key.t()) :: reply()

Offers a key to the field.

Answers {:ok, input} for a key it used and {:pass, input} for one it did not, the same contract Atui.View.handle_key/2 has — see the module docs for which keys are which.

into(reply, state, key \\ :input)

@spec into(reply(), map(), atom()) :: {:ok | :pass, map()}

Rewrites a reply about the field into a reply about the state holding it.

state.input |> TextInput.handle_key(key) |> TextInput.into(state)

key is where the field sits in the state, :input by default.

new(opts \\ [])

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

Builds a field.

Options:

  • :value — the initial text (the cursor starts after it)
  • :placeholder — what to draw while the value is empty
  • :prompt — a label drawn before the value, e.g. "search: "
  • :max_length — the most characters the value may hold

put_value(input, text)

@spec put_value(t(), String.t()) :: t()

Replaces the text, putting the cursor at the end of it.

value(input)

@spec value(t()) :: String.t()

The text in the field.

window(input, width)

@spec window(t(), pos_integer()) :: {String.t(), non_neg_integer()}

The window of the value that draw/4 would show in width columns.

Returns {text, cursor_column}, the column being where in text the cursor falls. Exposed because a host that draws the value itself still wants the scrolling rule, and because it is the honest thing to assert on in a test.