Raxol. UI. Components. Harness. Picker
(Raxol v2.6.1)
View Source
Overlay picker primitive (AD-U3): prompt + ranked list + async cancelable preview -- the fzf-shape that serves every "pick one of N" (sessions, runs, tool-calls, command palette, file mentions). One primitive, many projections (T15's job); this unit builds the component itself.
Substrate honesty
This component renders in ordinary buffer mode -- a ranked list
laid out in a normal flow column, no AbsoluteLayer/CellDim
full-viewport cover. The overlay-over-inline SUBSTRATE question (does
summoning this picker as a screen overlay mean a scoped full-viewport
cover via terminal save/restore, or a footer-anchored expansion?) is
deferred to the D-PA/T0 verdict and lands with T15/T24, which mount
this component inside whichever substrate they choose. Nothing here
assumes a particular host; render/2 just needs a context with
:available_width (and optionally :available_height, used to size
the visible window) the way any other component does.
Filtering
Uses Raxol.UI.ListScorer.rank/4 (the list-item fuzzy scorer) against
key_fn.(item) for every item on every query change -- not
Raxol.Search.Fuzzy, which searches buffer cells, a different problem.
Props
:items-- required, the full unfiltered list.:key_fn-- required,item -> String.t(); derives both the search key and the rendered label for each item.:on_select-- message tag emitted on Enter, default:select. Emits{:component_event, id, {on_select, item}}for the currently selected item; a no-op if the ranked list is empty.:on_cancel-- message tag emitted on Escape, default:cancel. Emits{:component_event, id, on_cancel}.:preview_fn-- optional,item -> {:ok, content} | {:error, reason}, run viaTask.async/1for the currently selected item. When absent, no preview pane renders at all. Must be side-effect-free / cancel-safe: on any selection change the in-flight preview task is killed withTask.shutdown(task, :brutal_kill)(no cleanup, noterminate, no chance to close what it opened), sopreview_fnmust not perform unguarded side effects that leak on an abrupt kill -- no bare file handles, sockets, ports, or DB connections held across the call. Read a snapshot, compute, return; if it must acquire a resource, wrap it so an abrupt process death can't strand it.:placeholder-- prompt placeholder shown while the query is empty (default"").:visible_height-- rows in the scrollable list window (default10); windowing isRaxol.UI.ScrollWindow(cursor-follow, edge-anchored).
Behavior
Type-to-filter: printable keys append to the query, Backspace removes
the last grapheme; the ranked list and cursor (reset to the top match)
update on every change. Up/Down move the cursor, clamped and windowed
by ScrollWindow. Enter selects the current item. Escape dismisses.
Stale-preview cancellation (the fzf#3134 lesson)
Every selection change (typing that changes the top match, or Up/Down
landing on a different item) cancels the in-flight preview Task for
the previous selection via Task.shutdown/2 and starts a new one
for the current item, tagging the pending result with the new task's
own ref. Two independent guards keep a superseded result from ever
reaching the screen:
Task.shutdown/2kills the old task process and flushes any completion message already sitting in this process's mailbox -- most of the time, that's the end of it.- Even in the race where a result was already in flight before the
shutdown call landed,
update/2only applies a{ref, result}(or{:DOWN, ref, ...}) message whenrefmatchesstate.preview_refat the time the message is handled. A result tagged with a superseded ref is pattern-matched into a catch-all clause and dropped -- it never touchesstate.preview, so it can never render.
A real host process must forward whatever arrives in its mailbox for
a spawned preview task straight into this component's update/2 --
the raw {ref, result} / {:DOWN, ref, :process, pid, reason} shapes
Task.async/1 itself produces, unwrapped. That's what makes the
ref-matching guard above work with zero translation layer.
Summary
Functions
Callback implementation for Raxol.UI.Components.Base.Component.mount/1.
Current preview state.
Current query string.
Current ranked (filtered) result list.
The currently selected item, or nil if the ranked list is empty.
Callback implementation for Raxol.UI.Components.Base.Component.unmount/1.
Types
@type t() :: %{ id: term(), items: [term()], key_fn: (term() -> String.t()), on_select: term(), on_cancel: term(), preview_fn: (term() -> {:ok, String.t()} | {:error, term()}) | nil, placeholder: String.t(), visible_height: pos_integer(), query: String.t(), ranked: [Raxol.UI.ListScorer.result()], cursor: non_neg_integer(), scroll_top: non_neg_integer(), preview: preview_state(), preview_task: Task.t() | nil, preview_ref: reference() | nil, style: map(), theme: map() }
Functions
Callback implementation for Raxol.UI.Components.Base.Component.mount/1.
@spec preview(t()) :: preview_state()
Current preview state.
Current query string.
@spec ranked(t()) :: [Raxol.UI.ListScorer.result()]
Current ranked (filtered) result list.
The currently selected item, or nil if the ranked list is empty.
Callback implementation for Raxol.UI.Components.Base.Component.unmount/1.