run method

T? run({
  1. required RankResult? rankItem(
    1. T item,
    2. String query,
    3. bool useFuzzy
    ),
  2. required String itemLabel(
    1. T item
    ),
  3. String? itemSubtitle(
    1. T item
    )?,
  4. void renderItem(
    1. FrameContext ctx,
    2. RankedItem<T> rankedItem,
    3. int index,
    4. bool isFocused,
    5. String query,
    )?,
  5. void beforeItems(
    1. FrameContext ctx,
    2. String query,
    3. bool useFuzzy,
    4. int matchCount,
    )?,
  6. 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;
}