run method
List<T>
run({
- bool filter(
- T item,
- String query
- String itemLabel(
- T item
- void renderItem(
- FrameContext ctx,
- T item,
- int absoluteIndex,
- bool isFocused,
- bool isSelected,
- String query,
- bool highlightMatches = true,
- KeyBindings? extraBindings,
Runs the searchable prompt.
filter - Custom filter function. Defaults to case-insensitive contains.
itemLabel - Converts item to string for display and default filter.
renderItem - Custom item renderer. If null, uses default checkbox/select style.
highlightMatches - Whether to highlight matching text in items.
extraBindings - Additional key bindings.
Returns selected items on confirm, empty list on cancel.
Implementation
List<T> run({
bool Function(T item, String query)? filter,
String Function(T item)? itemLabel,
void Function(
FrameContext ctx,
T item,
int absoluteIndex,
bool isFocused,
bool isSelected,
String query,
)? renderItem,
bool highlightMatches = true,
KeyBindings? extraBindings,
}) {
if (items.isEmpty) return [];
_initState();
// Default filter: case-insensitive contains on label
final effectiveFilter = filter ??
(T item, String query) {
final label = itemLabel?.call(item) ?? item.toString();
return label.toLowerCase().contains(query.toLowerCase());
};
void updateFilter() {
if (!_searchActive || _queryInput.isEmpty) {
_filtered = List.from(items);
} else {
final query = _queryInput.text;
_filtered =
items.where((item) => effectiveFilter(item, query)).toList();
}
_nav.itemCount = _filtered.length;
_nav.reset();
_selection.constrainTo(_filtered.length);
}
// Create bindings with search support
_bindings = KeyBindings.searchableList(
onUp: () => _nav.moveUp(),
onDown: () => _nav.moveDown(),
onSearchToggle: () {
_searchActive = !_searchActive;
if (!_searchActive) _queryInput.clear();
updateFilter();
},
searchBuffer: _queryInput,
isSearchEnabled: () => _searchActive,
onSearchInput: updateFilter,
onToggle: multiSelect && _filtered.isNotEmpty
? () => _selection.toggle(_nav.selectedIndex)
: null,
hasMultiSelect: multiSelect,
onCancel: () => _cancelled = true,
);
if (extraBindings != null) {
_bindings = _bindings + extraBindings;
}
final frame = FrameView(
title: title,
theme: theme,
bindings: _bindings,
);
void render(RenderOutput out) {
final rows = TerminalInfo.rows;
final available = rows > reservedLines ? rows - reservedLines : 5;
_nav.maxVisible = available.clamp(1, maxVisible);
frame.render(out, (ctx) {
// Search line
ctx.searchLine(_queryInput.text, enabled: _searchActive);
ctx.writeConnector();
// Visible window
final window = _nav.visibleWindow(_filtered);
ctx.listWindow(
window,
selectedIndex: _nav.selectedIndex,
renderItem: (T item, int absoluteIndex, bool isFocused) {
final isSelected = _selection.isSelected(absoluteIndex);
if (renderItem != null) {
renderItem(
ctx,
item,
absoluteIndex,
isFocused,
isSelected,
_queryInput.text,
);
} else {
// Default rendering
final label = itemLabel?.call(item) ?? item.toString();
final displayLabel = highlightMatches && _searchActive
? highlightSubstring(label, _queryInput.text, theme)
: label;
final checkbox = multiSelect ? ctx.lb.checkbox(isSelected) : ' ';
final arrow = ctx.lb.arrow(isFocused);
ctx.highlightedLine(
'$arrow $checkbox $displayLabel',
highlighted: isFocused,
);
}
},
);
if (_filtered.isEmpty) {
ctx.emptyMessage('no matches');
}
});
}
updateFilter();
final runner = PromptRunner(hideCursor: true);
final result = runner.runWithBindings(
render: render,
bindings: _bindings,
);
if (_cancelled || result == PromptResult.cancelled || _filtered.isEmpty) {
return [];
}
return _selection.getSelectedMany(
_filtered,
fallbackIndex: _nav.selectedIndex,
);
}