# `Tela.Component.TextInput`
[🔗](https://github.com/idabmat/tela/blob/v0.1.0/lib/tela/component/text_input.ex#L1)

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()`.

# `t`

```elixir
@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.

# `blink_cmd`

```elixir
@spec blink_cmd(t()) :: Tela.cmd()
```

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

Returns `nil` when `cursor_mode` is not `:blink` — no blink loop is needed
for `:static` or `:hidden` modes.

Pass the result as the cmd in your app's `init/1` or `handle_info/2` return.
Route the resulting tick message to `handle_blink/2`.

# `blur`

```elixir
@spec blur(t()) :: t()
```

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

# `focus`

```elixir
@spec focus(t()) :: t()
```

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

# `handle_blink`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Returns the current value of the input.

# `view`

```elixir
@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.

---

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