run method

T? run({
  1. required List<T> buildItems(),
  2. DynamicAction onPrimary(
    1. T item,
    2. int index
    )?,
  3. DynamicAction onSecondary(
    1. T item,
    2. int index
    )?,
  4. DynamicAction onToggle(
    1. T item,
    2. int index
    )?,
  5. required void renderItem(
    1. FrameContext ctx,
    2. T item,
    3. int absoluteIndex,
    4. bool isFocused,
    ),
  6. void beforeItems(
    1. FrameContext ctx
    )?,
  7. KeyBindings? extraBindings,
  8. KeyBindings? customBindings,
})

Runs the dynamic list prompt.

buildItems - Function that returns current list of items. Called on every action to rebuild the list. onPrimary - Called when Enter/Right is pressed on an item. Return DynamicAction to control behavior. onSecondary - Called when Left is pressed on an item. Return DynamicAction to control behavior. onToggle - Called when Space is pressed on an item. Return DynamicAction to control behavior. renderItem - Custom item renderer. beforeItems - Content to render before the list. extraBindings - Additional key bindings. customBindings - Completely replace default bindings.

Returns selected item on confirm, null on cancel.

Implementation

T? run({
  required List<T> Function() buildItems,
  DynamicAction Function(T item, int index)? onPrimary,
  DynamicAction Function(T item, int index)? onSecondary,
  DynamicAction Function(T item, int index)? onToggle,
  required void Function(
    FrameContext ctx,
    T item,
    int absoluteIndex,
    bool isFocused,
  ) renderItem,
  void Function(FrameContext ctx)? beforeItems,
  KeyBindings? extraBindings,
  KeyBindings? customBindings,
}) {
  _initState(buildItems);

  void rebuild() {
    _items = buildItems();
    _nav.itemCount = _items.length;
  }

  DynamicAction handleAction(DynamicAction Function(T, int)? handler) {
    if (handler == null || _items.isEmpty) return DynamicAction.none;
    final item = _items[_nav.selectedIndex];
    return handler(item, _nav.selectedIndex);
  }

  // Create bindings
  if (customBindings != null) {
    _bindings = customBindings;
  } else {
    _bindings = KeyBindings.verticalNavigation(
          onUp: () => _nav.moveUp(),
          onDown: () => _nav.moveDown(),
        ) +
        KeyBindings([
          // Primary action (Enter/Right)
          KeyBinding.multi(
            {KeyEventType.enter, KeyEventType.arrowRight},
            (event) {
              final action = handleAction(onPrimary);
              switch (action) {
                case DynamicAction.confirm:
                  if (_items.isNotEmpty) {
                    _result = _items[_nav.selectedIndex];
                  }
                  return KeyActionResult.confirmed;
                case DynamicAction.rebuild:
                  rebuild();
                  return KeyActionResult.handled;
                case DynamicAction.rebuildAndReset:
                  rebuild();
                  _nav.reset();
                  return KeyActionResult.handled;
                case DynamicAction.cancel:
                  _cancelled = true;
                  return KeyActionResult.cancelled;
                case DynamicAction.none:
                  return KeyActionResult.handled;
              }
            },
            hintLabel: '→/Enter',
            hintDescription: 'select/enter',
          ),
          // Secondary action (Left)
          KeyBinding.single(
            KeyEventType.arrowLeft,
            (event) {
              final action = handleAction(onSecondary);
              switch (action) {
                case DynamicAction.confirm:
                  if (_items.isNotEmpty) {
                    _result = _items[_nav.selectedIndex];
                  }
                  return KeyActionResult.confirmed;
                case DynamicAction.rebuild:
                  rebuild();
                  return KeyActionResult.handled;
                case DynamicAction.rebuildAndReset:
                  rebuild();
                  _nav.reset();
                  return KeyActionResult.handled;
                case DynamicAction.cancel:
                  _cancelled = true;
                  return KeyActionResult.cancelled;
                case DynamicAction.none:
                  return KeyActionResult.handled;
              }
            },
            hintLabel: '←',
            hintDescription: 'back/collapse',
          ),
          // Toggle action (Space)
          if (onToggle != null)
            KeyBinding.single(
              KeyEventType.space,
              (event) {
                final action = handleAction(onToggle);
                switch (action) {
                  case DynamicAction.confirm:
                    if (_items.isNotEmpty) {
                      _result = _items[_nav.selectedIndex];
                    }
                    return KeyActionResult.confirmed;
                  case DynamicAction.rebuild:
                    rebuild();
                    return KeyActionResult.handled;
                  case DynamicAction.rebuildAndReset:
                    rebuild();
                    _nav.reset();
                    return KeyActionResult.handled;
                  case DynamicAction.cancel:
                    _cancelled = true;
                    return KeyActionResult.cancelled;
                  case DynamicAction.none:
                    return KeyActionResult.handled;
                }
              },
              hintLabel: 'Space',
              hintDescription: 'toggle',
            ),
        ]) +
        KeyBindings.cancel(onCancel: () => _cancelled = true);
  }

  if (extraBindings != null) {
    _bindings = _bindings + extraBindings;
  }

  final frame = FrameView(
    title: title,
    theme: theme,
    bindings: _bindings,
  );

  void render(RenderOutput out) {
    // Rebuild items on each render to catch state changes
    rebuild();

    frame.render(out, (ctx) {
      // Before items hook
      beforeItems?.call(ctx);

      // Visible window
      final window = _nav.visibleWindow(_items);

      ctx.listWindow(
        window,
        selectedIndex: _nav.selectedIndex,
        renderItem: (T item, int index, bool isFocused) {
          renderItem(ctx, item, index, isFocused);
        },
      );

      if (_items.isEmpty) {
        ctx.emptyMessage('empty');
      }
    });
  }

  final runner = PromptRunner(hideCursor: true);
  runner.runWithBindings(
    render: render,
    bindings: _bindings,
  );

  return _cancelled ? null : _result;
}