chooseAny<T extends Object?> method

List<T> chooseAny<T extends Object?>(
  1. String? message, {
  2. required List<T> choices,
  3. List<T>? defaultValues,
  4. String display(
    1. T choice
    )?,
})

Prompts user with message to choose zero or more values from the provided choices.

An optional list of defaultValues can be specified. The defaultValues must be one of the provided choices.

This method requires a terminal to be attached to stdout. See https://api.dart.dev/stable/dart-io/Stdout/hasTerminal.html.

Implementation

List<T> chooseAny<T extends Object?>(
  String? message, {
  required List<T> choices,
  List<T>? defaultValues,
  String Function(T choice)? display,
}) {
  final resolvedDisplay = display ?? (value) => '$value';
  final hasDefaults = defaultValues != null && defaultValues.isNotEmpty;
  final selections = hasDefaults
      ? defaultValues.map((value) => choices.indexOf(value)).toSet()
      : <int>{};
  var index = 0;

  void writeChoices() {
    _stdout
      // save cursor
      ..write('\x1b7')
      // hide cursor
      ..write('\x1b[?25l')
      ..writeln('$message');

    for (final choice in choices) {
      final choiceIndex = choices.indexOf(choice);
      final isCurrent = choiceIndex == index;
      final isSelected = selections.contains(choiceIndex);
      final checkBox = isSelected ? lightCyan.wrap('◉') : '◯';
      if (isCurrent) {
        _stdout
          ..write(green.wrap('❯'))
          ..write(' $checkBox  ${lightCyan.wrap(resolvedDisplay(choice))}');
      } else {
        _stdout
          ..write(' ')
          ..write(' $checkBox  ${resolvedDisplay(choice)}');
      }
      if (choices.last != choice) {
        _stdout.write('\n');
      }
    }
  }

  _stdin
    ..echoMode = false
    ..lineMode = false;

  writeChoices();

  List<T>? results;
  while (results == null) {
    final key = _readKey();
    final keyIsUpOrKKey =
        key.controlChar == ControlCharacter.arrowUp || key.char == 'k';
    final keyIsDownOrJKey =
        key.controlChar == ControlCharacter.arrowDown || key.char == 'j';
    final keyIsSpaceKey = key.char == ' ';
    final keyIsEnterOrReturnKey = key.controlChar == ControlCharacter.ctrlJ ||
        key.controlChar == ControlCharacter.ctrlM;

    if (keyIsUpOrKKey) {
      index = (index - 1) % (choices.length);
    } else if (keyIsDownOrJKey) {
      index = (index + 1) % (choices.length);
    } else if (keyIsSpaceKey) {
      selections.contains(index)
          ? selections.remove(index)
          : selections.add(index);
    } else if (keyIsEnterOrReturnKey) {
      _stdin
        ..lineMode = true
        ..echoMode = true;

      results = selections.map((index) => choices[index]).toList();

      _stdout
        // restore cursor
        ..write('\x1b8')
        // clear to end of screen
        ..write('\x1b[J')
        // show cursor
        ..write('\x1b[?25h')
        ..write('$message ')
        ..writeln(
          styleDim.wrap(
            lightCyan.wrap('${results.map(resolvedDisplay).toList()}'),
          ),
        );

      break;
    }

    // restore cursor
    _stdout.write('\x1b8');
    writeChoices();
  }

  return results;
}