Selkie.git | docs/api/ | Selkie--Widget--TextInput.md


NAME
====

Selkie::Widget::TextInput - Single-line text input with cursor and editing

SYNOPSIS
========

```raku
use Selkie::Widget::TextInput;
use Selkie::Sizing;

my $input = Selkie::Widget::TextInput.new(
    sizing      => Sizing.fixed(1),
    placeholder => 'Search...',
);

$input.on-submit.tap: -> $text { run-search($text) };
$input.on-change.tap: -> $text { update-preview($text) };

# Password field: mask characters
my $pw = Selkie::Widget::TextInput.new(
    sizing    => Sizing.fixed(1),
    mask-char => '•',
);
```

DESCRIPTION
===========

A one-line text input. Arrow keys, Home, End, Backspace, and Delete behave as you'd expect. Characters wider than the visible width horizontally scroll the view to follow the cursor.

Four Supplies:

  * `on-submit` — fires once when the user presses Enter, carrying the current text

  * `on-change` — fires on every keystroke that modifies the buffer

  * `on-copy` — fires on Ctrl+C, carrying the currently-selected text

  * `on-cut` — fires on Ctrl+X, carrying the cut text (which is also deleted from the buffer)

For programmatic updates that shouldn't re-dispatch (e.g. syncing from a store subscription), use `set-text-silent` — it updates the buffer without emitting on `on-change`.

Mouse and selection
-------------------

Click positions the caret. Drag selects from the press point to the current cursor cell — the selection range is rendered with reverse-video. Double-click selects the word under the cursor; triple-click selects the entire buffer. `has-selection`, `selection-range`, and `selected-text` expose the current selection state.

Keyboard cooperates: Shift+Left / Shift+Right jump by word AND extend the selection (the legacy word-jump is now also a selection-extend); plain arrows clear any selection before moving. Ctrl+A selects all. Ctrl+C and Ctrl+X emit on the corresponding supplies — Selkie does not own the system clipboard, so apps wire OSC 52 / notcurses paste-buffer in their handlers. Backspace and Delete delete an active selection if present; typing replaces it.

Decorations: highlight spans and ghost suggestions
--------------------------------------------------

Two optional pull-based hooks let an app decorate the input without ever desyncing from the buffer:

  * `highlight-provider` — `Callable(Str $buffer)` returning a list of [Selkie::Widget::TextInput::HighlightSpan](Selkie--Widget--TextInput--HighlightSpan.md) (half-open char ranges + styles). Each range is repainted over the base text during render, clipped to the visible window. Span styles merge onto the input's own style, so fg-only spans keep the field background.

  * `suggest-provider` — `Callable(Str $buffer, UInt $cursor --` Str)> returning a completion tail to paint after end-of-buffer in the `ghost-style` (falling back to the theme's `input-ghost` slot), fish-shell style. Only consulted while focused with the caret at the end of the buffer. The ghost is display-only; pressing Right arrow at end-of-buffer accepts it via `insert-text`, emitting `on-change` exactly once. Right anywhere else keeps its normal cursor-move behavior.

Both hooks are called during `render` — they must be pure and cheap (memoise on the buffer string if not). Exceptions they throw are not caught. Because the widget pulls rather than being pushed, the decorations survive `set-text-silent` store-syncs, and external state changes (say, a new list of valid tokens) take effect by simply marking the input dirty — e.g. by subscribing the input widget to the relevant store path. Both hooks are ignored while `mask-char` is set: masked inputs expose no content semantics.

```raku
use Selkie::Widget::TextInput::HighlightSpan;

$input.highlight-provider = -> Str $buf {
    known-tokens($buf).map: -> $t {
        Selkie::Widget::TextInput::HighlightSpan.new(
            start => $t<from>, end => $t<to>,
            style => Selkie::Style.new(fg => $t<ok> ?? 0x80E060 !! 0xE06060),
        )
    }
};
$input.suggest-provider = -> Str $buf, UInt $ {
    best-completion-tail($buf) // ''
};
```

Modifier bubbling
-----------------

Modified keys (Ctrl, Alt, Super) bubble past the input so global keybinds still work — except for Ctrl+A / C / X (selection-related, handled internally) and except when the OS keyboard layout has already composed the modifier into a different printable character (e.g. UK Mac Alt-3 → `#`, US Mac Alt-2 → `™`). In that case the composed character is treated as typed input, since blocking it would make those characters untypeable on layouts that need a modifier to produce them. Bare characters are consumed for typing.

EXAMPLES
========

Store-synced input
------------------

```raku
$app.store.subscribe-with-callback(
    'sync-name',
    -> $s { ($s.get-in('form', 'name') // '').Str },
    -> $v { $name-input.set-text-silent($v) if $name-input.text ne $v },
    $name-input,
);
$name-input.on-change.tap: -> $v {
    $app.store.dispatch('form/set', field => 'name', value => $v);
};
```

SEE ALSO
========

  * [Selkie::Widget::MultiLineInput](Selkie--Widget--MultiLineInput.md) — multi-line variant with word wrap

  * [Selkie::Widget::Button](Selkie--Widget--Button.md) — for commit-only actions

### sub next-word-pos

```raku
sub next-word-pos(
    Str:D $s,
    Int:D $pos
) returns Int
```

Find the position of the start of the next word at or after `$pos` in `$s`. Word = run of `\w` chars. Skips through the current char's class (word or non-word), then through any trailing non-word chars, landing at the first word char of the next word — or `$s.chars` if there is no next word. Used by shift-right word-jump and by `MultiLineInput`'s 2D variant.

### sub prev-word-pos

```raku
sub prev-word-pos(
    Str:D $s,
    Int:D $pos
) returns Int
```

Find the position of the start of the previous word at or before `$pos` in `$s`. Skips backwards through any non-word chars, then backwards through word chars, landing on the index of the first char of that word — or 0 if we walked off the start. Used by shift-left and shift-backspace.

### has Int $!sel-anchor

Selection anchor offset. -1 means "no selection" — the cursor is a bare caret. When >= 0, the selection covers the half-open range from `min(anchor, cursor)` to `max(anchor, cursor)`. The cursor is the movable end; the anchor stays put while extending.

### has Str $.mask-char

Character painted in place of every buffer character. Setting it at construction is what makes an input a password field; it is never changed afterwards. Whether the mask is **currently** applied is a separate, runtime question — ask `masked`, not this.

### has Bool $!revealed

Reveal switch behind `masked`. Off by default, so a masked input starts masked; `set-revealed` flips it for show-password affordances. Kept separate from `mask-char` so a reveal is reversible without the widget having to remember what it used to mask with, and so revealing an input that was never masked is a harmless no-op rather than a special case.

### has Callable &.highlight-provider

Optional pull-based highlight hook: a `Callable(Str $buffer)` returning a list of `Selkie::Widget::TextInput::HighlightSpan`. Consulted on every `render`, so it must be pure and cheap — memoise on the buffer string if the computation is nontrivial. Because decorations are pulled at paint time they can never go stale against the buffer, including after `set-text-silent` (which deliberately skips `on-change`). Ignored while `mask-char` is set — masked inputs expose no content semantics.

### has Callable &.suggest-provider

Optional pull-based ghost-suggestion hook: a `Callable(Str $buffer, UInt $cursor --` Str)> returning the tail to paint after the end of the buffer (fish-shell style), or an undefined/empty string for none. Only consulted while the input is focused with the caret at end-of-buffer, and never while `mask-char` is set. The ghost is display-only — it lives outside the buffer and never affects editing or `on-change` — until the user accepts it with Right arrow at end-of-buffer, which inserts the tail via `insert-text` (one `on-change` emission).

### has Selkie::Style $.ghost-style

Style for the ghost-suggestion tail. Merged onto the input's base style, so a foreground-only style keeps the field background. Falls back to the theme's `input-ghost` slot when unset.

### method text

```raku
method text() returns Str
```

The current buffer contents.

### method has-selection

```raku
method has-selection() returns Bool
```

True iff there is an active selection (anchor differs from cursor). A bare caret returns False.

### method selection-range

```raku
method selection-range() returns Range
```

Half-open offset range of the current selection, normalised to `low..^high`. Returns `0..^0` when there's no selection.

### method selected-text

```raku
method selected-text() returns Str
```

The substring currently selected, or the empty string when there is no selection.

### method clear-selection

```raku
method clear-selection() returns Mu
```

Clear any active selection without moving the caret.

### method on-copy

```raku
method on-copy() returns Supply
```

Supply emitting the currently-selected text on Ctrl+C. The Selkie framework does not own the system clipboard — apps wire this up themselves via OSC 52 or notcurses paste-buffer. The supply only fires when there's an active selection.

### method on-cut

```raku
method on-cut() returns Supply
```

Supply emitting on Ctrl+X. Like on-copy but the selection is also deleted from the buffer.

### method set-text

```raku
method set-text(
    Str:D $t
) returns Mu
```

Replace the buffer's contents and place the caret at the end. Emits on `on-change`. Use this for user-driven updates (e.g. a "load from history" button); for programmatic syncs from a store path use `set-text-silent` instead to avoid feedback loops.

### method set-text-silent

```raku
method set-text-silent(
    Str:D $t
) returns Mu
```

Silent variant of `set-text` — updates the buffer without emitting on `on-change`. Wire this into store subscriptions that mirror external state into the input, so the input update doesn't dispatch an event that loops back through the store and re-fires the subscription.

### method clear

```raku
method clear() returns Mu
```

Empty the buffer. Equivalent to `set-text('')` — emits on `on-change`.

### method masked

```raku
method masked() returns Bool
```

True while the buffer is being painted as mask characters: the input was built with a `mask-char` **and** has not been revealed. This — not `mask-char.defined` — is what gates every content-revealing behaviour (the painted text, highlight spans, ghost suggestions).

### method revealed

```raku
method revealed() returns Bool
```

True while a masked input is showing its contents in the clear. Always False for an input that has no `mask-char`: there is nothing to reveal, so nothing is revealed.

### method set-revealed

```raku
method set-revealed(
    Bool:D $on
) returns Nil
```

Show or re-hide a masked input's contents — the show-password affordance. A no-op on an input with no `mask-char` (there is no mask to lift) and on a repeat of the current state, so wiring it to a checkbox that re-emits on every render costs nothing. Affects **display only**. The buffer, the caret, `text` and every Supply behave identically either way, so a form can be submitted revealed or hidden with the same result. =begin code :lang<raku> my $pw = Selkie::Widget::TextInput.new( sizing => Sizing.fixed(1), mask-char => '*'); $pw.set-revealed(True); # user ticked "show password" $pw.masked; # False $pw.text; # unchanged either way =end code

### method toggle-revealed

```raku
method toggle-revealed() returns Bool
```

Flip `revealed` and return the new state. Convenience for a keybind handler, which usually has nowhere to read the current state from. Returns False (and does nothing) on an unmasked input.

### method on-submit

```raku
method on-submit() returns Supply
```

Supply emitting the current buffer contents when the user presses Enter.

### method on-change

```raku
method on-change() returns Supply
```

Supply emitting the new buffer contents on every user-driven edit (typing, paste, delete, cut, `set-text`). Does not fire for `set-text-silent` — the silent variant is intended exactly to break the change-supplier ↔ store feedback loop.

### method set-focused

```raku
method set-focused(
    Bool $f
) returns Mu
```

Set the input's focus state. Called by `Selkie::App`'s focus dispatcher. The caret is only painted while focused.

### method insert-text

```raku
method insert-text(
    Str:D $text
) returns Nil
```

Insert `$text` at the current cursor position in one operation. Equivalent to typing each character in turn, but does ONE buffer concat instead of one per char — drops paste cost from O(n²) to O(n). Newlines and other control chars in `$text` are stripped (single-line input). Used by the App's paste-batching drain loop; application code can call it directly to programmatically insert text. If a selection is active, it is replaced (deleted then the new text is inserted at the deletion point) — matches the canonical "type to overwrite selection" behavior of every text editor.