chooseOne<T extends Object?> method

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

Prompts user with message to choose one value from the provided choices.

An optional defaultValue can be specified. The defaultValue 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

T chooseOne<T extends Object?>(
  String? message, {
  required List<T> choices,
  T? defaultValue,
  String Function(T choice)? display,
}) {
  final resolvedDisplay = display ?? (value) => '$value';
  final hasDefault =
      defaultValue != null && resolvedDisplay(defaultValue).isNotEmpty;
  var index = hasDefault ? choices.indexOf(defaultValue) : 0;

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

    for (final choice in choices) {
      final isCurrent = choices.indexOf(choice) == index;
      final checkBox = isCurrent ? 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();

  T? result;
  while (result == null) {
    final key = _readKey();
    final isArrowUpOrKKey =
        key.controlChar == ControlCharacter.arrowUp || key.char == 'k';
    final isArrowDownOrJKey =
        key.controlChar == ControlCharacter.arrowDown || key.char == 'j';
    final isReturnOrEnterOrSpaceKey =
        key.controlChar == ControlCharacter.ctrlJ ||
            key.controlChar == ControlCharacter.ctrlM ||
            key.char == ' ';

    if (isArrowUpOrKKey) {
      index = (index - 1) % (choices.length);
    } else if (isArrowDownOrJKey) {
      index = (index + 1) % (choices.length);
    } else if (isReturnOrEnterOrSpaceKey) {
      _stdin
        ..lineMode = true
        ..echoMode = true;

      _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(resolvedDisplay(choices[index]))),
        );

      result = choices[index];
      break;
    }

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

  return result!;
}