run method
T?
run({
- required RankResult? rankItem(),
- required String itemLabel(
- T item
- String? itemSubtitle(
- T item
- void renderItem(
- FrameContext ctx,
- RankedItem<
T> rankedItem, - int index,
- bool isFocused,
- String query,
- void beforeItems(
- FrameContext ctx,
- String query,
- bool useFuzzy,
- int matchCount,
- KeyBindings? extraBindings,
Runs the ranked list prompt.
rankItem - Ranking function that returns score and highlight spans.
Return null to exclude item from results.
itemLabel - Converts item to display label.
itemSubtitle - Optional subtitle for items.
renderItem - Custom item renderer. If null, uses default style.
beforeItems - Content to render before the list.
extraBindings - Additional key bindings.
Returns selected item on confirm, null on cancel.
Implementation
T? run({
required RankResult? Function(T item, String query, bool useFuzzy) rankItem,
required String Function(T item) itemLabel,
String? Function(T item)? itemSubtitle,
void Function(
FrameContext ctx,
RankedItem<T> rankedItem,
int index,
bool isFocused,
String query,
)? renderItem,
void Function(
FrameContext ctx, String query, bool useFuzzy, int matchCount)?
beforeItems,
KeyBindings? extraBindings,
}) {
if (items.isEmpty) return null;
_initState();
void updateRanking() {
final query = _queryInput.text;
if (query.isEmpty) {
_ranked = items
.map((item) => RankedItem(item, 0, const []))
.toList(growable: false);
} else {
final results = <RankedItem<T>>[];
for (final item in items) {
final rankResult = rankItem(item, query, _useFuzzy);
if (rankResult != null) {
results.add(RankedItem(item, rankResult.score, rankResult.spans));
}
}
results.sort((a, b) {
final sc = b.score.compareTo(a.score);
if (sc != 0) return sc;
return itemLabel(a.item)
.toLowerCase()
.compareTo(itemLabel(b.item).toLowerCase());
});
_ranked = results;
}
_nav.itemCount = _ranked.length;
_nav.reset();
}
// Create bindings
_bindings = KeyBindings.verticalNavigation(
onUp: () => _nav.moveUp(),
onDown: () => _nav.moveDown(),
) +
_queryInput.toTextInputBindings(onInput: updateRanking) +
KeyBindings.ctrlR(
onPress: () {
_useFuzzy = !_useFuzzy;
updateRanking();
},
hintDescription: 'toggle mode',
) +
KeyBindings.confirm(onConfirm: () {
if (_ranked.isNotEmpty) {
_result = _ranked[_nav.selectedIndex].item;
}
return KeyActionResult.confirmed;
}) +
KeyBindings.cancel(onCancel: () => _cancelled = true);
if (extraBindings != null) {
_bindings = _bindings + extraBindings;
}
final frame = FrameView(
title: title,
theme: theme,
bindings: _bindings,
);
void render(RenderOutput out) {
_nav.maxVisible =
(TerminalInfo.rows - reservedLines).clamp(5, maxVisible);
frame.render(out, (ctx) {
// Before items hook or default header
if (beforeItems != null) {
beforeItems(ctx, _queryInput.text, _useFuzzy, _ranked.length);
} else {
ctx.headerLine('Search', _queryInput.text);
ctx.writeConnector();
final mode = _useFuzzy ? 'Fuzzy' : 'Substring';
ctx.infoLine([mode, 'Matches: ${_ranked.length}']);
}
// Visible window
final window = _nav.visibleWindow(_ranked);
ctx.listWindow(
window,
selectedIndex: _nav.selectedIndex,
renderItem: (RankedItem<T> rankedItem, int index, bool isFocused) {
if (renderItem != null) {
renderItem(ctx, rankedItem, index, isFocused, _queryInput.text);
} else {
// Default rendering with span highlighting
final label = itemLabel(rankedItem.item);
final highlighted =
highlightSpans(label, rankedItem.spans, theme);
final subtitle = itemSubtitle?.call(rankedItem.item);
final subtitlePart = subtitle == null
? ''
: ' ${theme.dim}$subtitle${theme.reset}';
final arrow = ctx.lb.arrow(isFocused);
ctx.highlightedLine(
'$arrow $highlighted$subtitlePart',
highlighted: isFocused,
);
}
},
);
if (_ranked.isEmpty) {
ctx.emptyMessage('no matches');
}
});
}
updateRanking();
final runner = PromptRunner(hideCursor: true);
runner.runWithBindings(
render: render,
bindings: _bindings,
);
return _cancelled ? null : _result;
}