run method

List<T> run({
  1. bool filter(
    1. T item,
    2. String query
    )?,
  2. String itemLabel(
    1. T item
    )?,
  3. void renderItem(
    1. FrameContext ctx,
    2. T item,
    3. int absoluteIndex,
    4. bool isFocused,
    5. bool isSelected,
    6. String query,
    )?,
  4. bool highlightMatches = true,
  5. 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() {
    _filteredIndices = [
      for (var i = 0; i < items.length; i++)
        if (_queryInput.isEmpty ||
            effectiveFilter(items[i], _queryInput.text))
          i,
    ];
    _filtered = [for (final i in _filteredIndices) items[i]];
    _nav.itemCount = _filtered.length;
    _nav.reset();
  }

  // Create bindings with search support
  _bindings = KeyBindings.searchableList(
    onUp: () => _nav.moveUp(),
    onDown: () => _nav.moveDown(),
    onSearchToggle: () {
      _searchActive = !_searchActive;
    },
    searchBuffer: _queryInput,
    isSearchEnabled: () => _searchActive,
    onSearchInput: updateFilter,
    onToggle: multiSelect
        ? () {
            if (_filtered.isNotEmpty) {
              _selection.toggle(_filteredIndices[_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(_filteredIndices[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
                ? 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) {
    return [];
  }

  return _selection.getSelectedMany(
    items,
    fallbackIndex:
        _filtered.isEmpty ? null : _filteredIndices[_nav.selectedIndex],
  );
}