Raxol. UI. Components. Harness. Composer
(Raxol v2.6.1)
View Source
Harness prompt composer: wraps Raxol.UI.Components.Input.MultiLineInput
with harness-specific submit semantics, input history recall, bracketed
paste fidelity, and a queued-steer banner. Does not reimplement multi-line
editing -- cursor movement, selection, and text mutation stay entirely in
MultiLineInput; this module only decides when those primitives fire and
adds one banner above the prompt.
Submit semantics
Enter with no modifiers submits ({:component_event, id, {:submit, text}})
when the buffer is a single logical line (no embedded \n, regardless of
visual wrap) and non-empty after trimming. Enter with :shift or :alt
held, or Enter while the buffer already spans multiple logical lines
(continuation), inserts a literal newline instead via MultiLineInput's own
{:enter} message. force_submit/1 is exposed for a consumer (T12
keybinds) to wire an unconditional submit key (e.g. Ctrl+Enter) that
bypasses the single-line gate.
Legacy terminals and the degraded-mode inlet: many terminals do not
encode Shift+Enter or Alt+Enter distinctly (Alt+Enter arrives as ESC CR
and the ESC is dropped in parsing -- a bare Enter is all this component
sees), so the modifier branches are unreachable there. The
modifier-independent primary inlet to a first newline is backslash
continuation: a line ending in \ + Enter consumes the backslash and
inserts a newline instead of submitting. This works over tmux/SSH/mosh
at any point, including creating the very first newline. Escape rule
(simplest that round-trips a literal trailing backslash): count the
trailing backslash run -- an odd run means continuation (the final
backslash is consumed, the rest stay as typed); an even run means submit
with the run halved (\\ submits as \). The rule applies only on
the Enter-submit path; force_submit/1 and paste submit/insert content
verbatim. Bracketed paste and a consumer-wired force_submit/1 remain
alternate inlets, and a dim hint line (\ continue · paste multiline · ↵ submit) renders below the input while it is focused, empty, and has
no history yet. The modifier branches activate automatically when F1b
(kitty keyboard protocol) lands -- zero logic change here.
Input-shape normalization
handle_event/3 normalizes every incoming event through
Raxol.UI.Harness.InputEvent.normalize/1 before deciding anything.
Before T27's review round this component matched key events by pattern
(%{key: :enter, modifiers: modifiers}), which only exists on
Event.key_event/3's test-API shape -- the real terminal driver's two
shapes (event_translator.ex's boolean shift:/ctrl:/... fields,
input_parser.ex's optional fields) have no :modifiers key at all, so
the pattern never matched a REAL keypress. Enter never submitted,
printable characters never inserted, and everything silently fell
through to raw MultiLineInput delegation for actual terminal input --
this unit only ever worked in tests that construct Event.key_event/3
directly. InputEvent.normalize/1 erases that shape difference: Enter
from a real termbox driver, a real ANSI parser, or Event.key_event/3
all normalize to the same kind: :key, key: :enter and reach the same
handle_enter_key/2.
Printable characters arriving with compound modifiers (e.g. Shift+Alt)
are InputEvent.shortcut?/1 (alt held) and fall through to
MultiLineInput delegation, same as before -- canonical compound-
modifier handling belongs to F1a input canon, not this unit.
Bracketed paste
The terminal driver already parses ESC[200~...ESC[201~ into
%Raxol.Core.Events.Event{type: :paste, data: %{text: text}}
(Raxol.Terminal.Ansi.InputParser). This component routes that event
straight into MultiLineInput's {:clipboard_content, text} message -- the
same path Ctrl+V system-clipboard paste already uses
(MultiLineInput.SelectionOps.handle_clipboard_content/2) -- so pasted
newlines are inserted verbatim in one edit and never pass through the
Enter/submit decision above, no matter how many lines the paste contains.
History recall
Up at the first (visual) line, or Down at the last (visual) line, walks a bounded ring (default 100) of past submissions, newest first. The in-progress draft is saved on the first Up so Down can restore it. Any other cursor position forwards Up/Down to MultiLineInput unchanged (normal cursor movement / shift-selection).
Queued-steer banner
queued_steer: nil | %{text: String.t(), queued_at: term()} renders (when
set) one dim, truncated line above the prompt, prefixed with "⏸ " --
⏸ steer queued for next boundary: <text> -- visually distinct from
editable content. This is AD-2's two-signal model (interrupt vs. steer)
made visible.
Resize safety
No absolute width is persisted on this struct. render/2 derives
MultiLineInput's width from context[:available_width] on every call and
re-wraps the cached lines for that width, so a resized context produces
a correctly-wrapped prompt without any stored width going stale.
Summary
Functions
Unconditional submit, bypassing the single-line gate -- for a consumer
keybind (e.g. Ctrl+Enter) that must submit multi-line content directly.
Returns {state, []} unchanged if the trimmed buffer is empty.
Bounded, newest-first list of past submissions.
Callback implementation for Raxol.UI.Components.Base.Component.mount/1.
Replace the composer's content programmatically (e.g. after $EDITOR).
Callback implementation for Raxol.UI.Components.Base.Component.unmount/1.
Current composer text (delegates to the wrapped MultiLineInput).
Types
@type t() :: %{ id: String.t() | atom(), mli: Raxol.UI.Components.Input.MultiLineInput.t(), history: [String.t()], history_index: non_neg_integer() | nil, draft: String.t() | nil, queued_steer: queued_steer(), max_history: pos_integer(), style: map(), theme: map() }
Functions
Unconditional submit, bypassing the single-line gate -- for a consumer
keybind (e.g. Ctrl+Enter) that must submit multi-line content directly.
Returns {state, []} unchanged if the trimmed buffer is empty.
Bounded, newest-first list of past submissions.
Callback implementation for Raxol.UI.Components.Base.Component.mount/1.
Replace the composer's content programmatically (e.g. after $EDITOR).
Callback implementation for Raxol.UI.Components.Base.Component.unmount/1.
Current composer text (delegates to the wrapped MultiLineInput).