Tela.Component.TextInput (tela v0.1.0)

Copy Markdown View Source

A single-line text input component.

Tela.Component.TextInput accepts printable character input and supports cursor navigation, deletion, and optional password masking. It respects focus state — when blurred, key events are ignored and the cursor is hidden.

Usage

defmodule MyApp do
  use Tela
  alias Tela.Component.TextInput
  alias Tela.Frame

  @impl Tela
  def init(_args) do
    ti = TextInput.init(placeholder: "Pikachu", char_limit: 156) |> TextInput.focus()
    {%{input: ti}, TextInput.blink_cmd(ti)}
  end

  @impl Tela
  def handle_event(model, %Tela.Key{key: key}) when key in [:enter, :escape, {:ctrl, "c"}] do
    {model, :quit}
  end

  def handle_event(model, key) do
    {input, cmd} = TextInput.handle_event(model.input, key)
    {%{model | input: input}, cmd}
  end

  @impl Tela
  def handle_info(model, msg) do
    {input, cmd} = TextInput.handle_blink(model.input, msg)
    {%{model | input: input}, cmd}
  end

  @impl Tela
  def view(model) do
    header = Frame.new("What's your favorite Pokémon?\n\n")
    footer = Frame.new("\n\n(esc to quit)")
    Frame.join([header, TextInput.view(model.input), footer], separator: "")
  end
end

Focus

A TextInput is blurred by default. Call focus/1 to enable input and show the cursor. Call blur/1 to hide the cursor and stop accepting key events. In a multi-field form, only one input should be focused at a time.

Cursor

TextInput uses a virtual cursor — a reverse-video character embedded directly in the content string at the cursor position. The frame returned by view/1 always has cursor: nil; the real terminal cursor is kept hidden so it does not overwrite the virtual cursor's colour.

The cursor character is the grapheme under the cursor (or a space when the cursor is past the last grapheme), rendered with Style.reverse(focused_style). This produces a coloured reverse-video block that inherits the focused colour.

Cursor visibility is controlled by cursor_mode:

  • :blink — the cursor blinks on and off every 530 ms (default). The application timer drives this by toggling cursor_visible, which gates whether the reverse-video character appears in content.
  • :static — the cursor is always visible.
  • :hidden — no cursor character is embedded in content.

Use blink_cmd/1 to start the blink loop and handle_blink/2 to process tick messages. The stale-tick guard (same pattern as Tela.Component.Spinner) ensures that in-flight blink tasks from a previous mode or replaced input are silently discarded. Use set_cursor_mode/2 to change modes at runtime.

Echo modes

Set echo_mode: to control how the value is displayed:

  • :normal — characters rendered as typed (default).
  • :password — each character replaced by echo_char (default "*"). Pass echo_char: "•" for a bullet. The underlying value is stored unmasked and returned by value/1.
  • :none — nothing is rendered; the input acts as a fully invisible field. Useful for pinentry-style inputs. The underlying value is still stored and returned unmasked by value/1.

Styling

focused_style and blurred_style are Tela.Style.t() values applied to the prompt and text depending on focus state. Both default to Tela.Style.new().

Summary

Types

t()

The text input model. Build with init/1; treat as opaque.

Functions

Returns a {:task, fun} cmd that sleeps 530 ms then sends {:text_input_blink, id} back to the runtime.

Blurs the input, disabling key events and hiding the cursor.

Focuses the input, enabling key events and showing the cursor.

Processes a blink tick message for this input.

Handles a key event. Returns {new_model, cmd}.

Initialises a new text input model.

Sets the cursor mode.

Replaces the current value, clamping to char_limit if set, and moves the cursor to the end of the new value.

Returns the current value of the input.

Renders the input as a Tela.Frame.

Types

t()

@type t() :: %Tela.Component.TextInput{
  blurred_style: Tela.Style.t(),
  char_limit: non_neg_integer(),
  cursor: non_neg_integer(),
  cursor_id: non_neg_integer(),
  cursor_mode: :blink | :static | :hidden,
  cursor_visible: boolean(),
  echo_char: String.t(),
  echo_mode: :normal | :password | :none,
  focused: boolean(),
  focused_style: Tela.Style.t(),
  placeholder: String.t(),
  prompt: String.t(),
  value: String.t()
}

The text input model. Build with init/1; treat as opaque.

Functions

blur(ti)

@spec blur(t()) :: t()

Blurs the input, disabling key events and hiding the cursor.

focus(ti)

@spec focus(t()) :: t()

Focuses the input, enabling key events and showing the cursor.

handle_blink(ti, arg2)

@spec handle_blink(t(), term()) :: {t(), Tela.cmd()}

Processes a blink tick message for this input.

Matches {:text_input_blink, id} where id equals the input's current cursor_id. On a match, toggles cursor_visible, rotates cursor_id, and returns {new_input, blink_cmd(new_input)} to re-arm the loop.

Any non-matching message — including stale ticks from a previous mode or a replaced input — returns {input, nil} unchanged.

handle_event(ti, key)

@spec handle_event(t(), Tela.Key.t()) :: {t(), Tela.cmd()}

Handles a key event. Returns {new_model, cmd}.

Key events are ignored when the input is blurred. When cursor_mode is :blink, any focused keypress resets the cursor to visible and re-arms the blink timer, so the cursor is always immediately visible after typing or navigating.

init(opts)

@spec init(keyword()) :: t()

Initialises a new text input model.

Options

  • prompt: — prefix rendered before the input text (default "> ").
  • placeholder: — text shown when value is empty and the input is blurred (default "").
  • char_limit: — maximum number of graphemes accepted; 0 means no limit (default 0).
  • echo_mode::normal, :password, or :none. Password mode masks each character with echo_char. None mode renders nothing (default :normal).
  • echo_char: — the masking character used in password mode (default "*").
  • cursor_mode::blink, :static, or :hidden (default :blink).
  • focused_style:Tela.Style.t() applied to prompt and text when focused (default Tela.Style.new()).
  • blurred_style:Tela.Style.t() applied to prompt and text when blurred (default Tela.Style.new()).

set_cursor_mode(ti, mode)

@spec set_cursor_mode(t(), :blink | :static | :hidden) :: t()

Sets the cursor mode.

Accepted values: :blink, :static, :hidden.

Rotates cursor_id so any in-flight blink task from the previous mode becomes stale and is silently discarded by handle_blink/2. Resets cursor_visible to true so that switching back to :blink starts with the cursor shown.

set_value(ti, str)

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

Replaces the current value, clamping to char_limit if set, and moves the cursor to the end of the new value.

value(text_input)

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

Returns the current value of the input.

view(ti)

@spec view(t()) :: Tela.Frame.t()

Renders the input as a Tela.Frame.

Uses a virtual cursor — a reverse-video character embedded in the content string at the cursor position. The frame cursor is always nil; the real terminal cursor is kept hidden so it does not overwrite the virtual cursor's colour.

When the cursor is visible (focused, cursor_mode not :hidden, and blink phase on), the grapheme under the cursor — or a space when the cursor is past the last grapheme — is rendered with Style.reverse(focused_style). This produces a coloured reverse-video block that inherits the focused colour.

When blurred, cursor_mode is :hidden, or the blink phase is off, no cursor character is embedded and content is the plain styled text.