Selkie--Widget--TextInput

NAME

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

SYNOPSIS

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

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

$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

sub next-word-pos

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

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 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

method text() returns Str

The current buffer contents.

method has-selection

method has-selection() returns Bool

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

method selection-range

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

method selected-text() returns Str

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

method clear-selection

method clear-selection() returns Mu

Clear any active selection without moving the caret.

method on-copy

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

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

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

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

method clear() returns Mu

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

method on-submit

method on-submit() returns Supply

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

method on-change

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

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

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.

Selkie v0.10.0

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.4.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::App
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalSequences
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.