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() {
    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,
  );
}