choose<T> function

T? choose<T>(
  1. String message,
  2. Iterable<T> options,
  3. {T? defaultsTo,
  4. String prompt = 'Enter your choice',
  5. bool chevron = true,
  6. @deprecated bool colon = true,
  7. AnsiCode inputColor = cyan,
  8. bool color = true,
  9. bool conceal = false,
  10. bool interactive = true,
  11. Iterable<String>? names}
)

Displays to the user a list of options, and returns once one has been chosen.

Each option will be prefixed with a number, corresponding to its index + 1.

A default option may be provided by means of defaultsTo.

A custom prompt may be provided, which is then forwarded to get.

This function also supports an interactive mode, where user arrow keys are processed. In interactive mode, you can provide a defaultIndex for the UI to start on.

color, defaultsTo, inputColor, conceal, and chevron are forwarded to get.

Example:

Choose a color:

1) Red
2) Blue
3) Green

Implementation

T? choose<T>(String message, Iterable<T> options,
    {T? defaultsTo,
    String prompt = 'Enter your choice',
    // int defaultIndex = 0,
    bool chevron = true,
    @deprecated bool colon = true,
    AnsiCode inputColor = cyan,
    bool color = true,
    bool conceal = false,
    bool interactive = true,
    Iterable<String>? names}) {
  if (options.isEmpty) {
    throw ArgumentError.value('`options` may not be empty.');
  }

  if (defaultsTo != null && !options.contains(defaultsTo)) {
    throw ArgumentError('$defaultsTo is not contained in $options, and therefore cannot be the default value.');
  }

  if (names != null && names.length != options.length) {
    throw ArgumentError('$names must have length ${options.length}, not ${names.length}.');
  }

  if (names != null && names.any((s) => s.length != 1)) {
    throw ArgumentError('Every member of $names must be a string with a length of 1.');
  }

  var map = <T, String>{};
  for (var option in options) {
    map[option] = option.toString();
  }

  if (chevron && colon) message += ':';

  var b = StringBuffer();

  b.writeln(message);

  if (interactive && ansiOutputEnabled && !Platform.isWindows) {
    var index = defaultsTo != null ? options.toList().indexOf(defaultsTo) : 0;
    var oldEchoMode = stdin.echoMode;
    var oldLineMode = stdin.lineMode;
    var needsClear = false;
    if (color) {
      print(wrapWith(b.toString(), [darkGray, styleBold]));
    } else {
      print(b);
    }

    void writeIt() {
      if (!needsClear) {
        needsClear = true;
      } else {
        for (var i = 0; i < options.length; i++) {
          goUpOneLine();
          clearLine();
        }
      }

      for (var i = 0; i < options.length; i++) {
        var key = map.keys.elementAt(i);
        var msg = map[key];
        AnsiCode code;

        if (index == i) {
          code = cyan;
          msg = '* $msg';
        } else {
          code = darkGray;
          msg = '$msg  ';
        }

        if (names != null) {
          msg = names.elementAt(i) + ') $msg';
        }

        if (color) {
          print(code.wrap(msg));
        } else {
          print(msg);
        }
      }
    }

    do {
      int ch;
      writeIt();

      try {
        stdin.lineMode = stdin.echoMode = false;
        ch = stdin.readByteSync();

        if (ch == $esc) {
          ch = stdin.readByteSync();
          if (ch == $lbracket) {
            ch = stdin.readByteSync();
            if (ch == $A) {
              // Up key
              index--;
              if (index < 0) index = options.length - 1;
              writeIt();
            } else if (ch == $B) {
              // Down key
              index++;
              if (index >= options.length) index = 0;
              writeIt();
            }
          }
        } else if (ch == $lf) {
          // Enter key pressed - submit
          return map.keys.elementAt(index);
        } else {
          // Check if this matches any name
          var s = String.fromCharCode(ch);
          if (names != null && names.contains(s)) {
            index = names.toList().indexOf(s);
            return map.keys.elementAt(index);
          }
        }
      } finally {
        stdin.lineMode = oldLineMode;
        stdin.echoMode = oldEchoMode;
      }
    } while (true);
  } else {
    b.writeln();

    for (var i = 0; i < options.length; i++) {
      var key = map.keys.elementAt(i);
      var indicator = names != null ? names.elementAt(i) : (i + 1).toString();
      b.write('$indicator) ${map[key]}');
      if (key == defaultsTo) b.write(' [Default - Press Enter]');
      b.writeln();
    }

    b.writeln();
    if (color) {
      print(wrapWith(b.toString(), [darkGray, styleBold]));
    } else {
      print(b);
    }

    var line = get(
      prompt,
      chevron: false,
      inputColor: inputColor,
      color: color,
      conceal: conceal,
      validate: (s) {
        if (s.isEmpty) return defaultsTo != null;
        if (map.values.contains(s)) return true;
        if (names != null && names.contains(s)) return true;
        var i = int.tryParse(s);
        if (i == null) return false;
        return i >= 1 && i <= options.length;
      },
    );

    if (line.isEmpty) return defaultsTo;
    int? i;
    if (names != null && names.contains(line)) {
      i = names.toList().indexOf(line) + 1;
    } else {
      i = int.tryParse(line);
    }

    if (i != null) return map.keys.elementAt(i - 1);
    return map.keys.elementAt(map.values.toList(growable: false).indexOf(line));
  }
}