promptUntilValidPick<T extends Object> function

Future<T> promptUntilValidPick<T extends Object>({
  1. required String prompt,
  2. required Set<T> options,
  3. required StringifiedPromptOption stringify(
    1. int index,
    2. T option
    ),
  4. PromptAnswerReader answerReader = defaultAnswerReader,
  5. String onError(
    1. String input
    )?,
  6. String selectorFormatter(
    1. String selector
    )?,
  7. T? defaultValue,
  8. String separator = ")",
  9. String promptIndent = "",
  10. String optionsIndent = "\t",
  11. String errorIndent = "",
  12. StringSink? output,
})

Notice

This function does not block the calling Isolate, until a full line is available. Instead a Future that completes when a full line is available is awaited.

If you want to block, use the synchronous version instead.

See promptUntilValidPickSync.

Description

Prompt the user to pick one of the values from options. This function only returns, if the user inputs a valid selector.

stringify transforms each value from options into a representable string and associates a selector with the value.

Each selector must fulfill the following requirements:

  • it must be unique

  • it must not be a blank string (if you want to assign an option to a blank string use defaultValue)

  • it must not contain any linefeeds (\n)

If the user inputs an invalid selector, then the result of onError gets written to stdout and the user gets prompted again until they input a valid selector. The default function for onError informs the user that they picked an invalid selector.

selectorFormatter can be used to customize the representation of the selector. For example, this function can apply ANSI Escape Codes to affect the appearance of the selector. By default, no formatting is applied to a selector.

separator is the string between each selector and its associated option. A whitespace gets always appended to the separator.

Implementation

Future<T> promptUntilValidPick<T extends Object>({
  required String prompt,
  required Set<T> options,
  required StringifiedPromptOption Function(int index, T option) stringify,
  PromptAnswerReader answerReader = defaultAnswerReader,
  String Function(String input)? onError,
  String Function(String selector)? selectorFormatter,
  T? defaultValue,
  String separator = ")",
  String promptIndent = "",
  String optionsIndent = "\t",
  String errorIndent = "",
  StringSink? output,
}) async {
  final oSink = output ?? stdout;

  onError ??= (input) {
    return input.isBlank
        ? "You must pick an option"
        : "There is no option for this value: $input";
  };
  selectorFormatter ??= (selector) => selector;

  final selectorOptionsMap = <String, T>{};
  final optionsStrBuf = IndentedStringBuffer(indent: optionsIndent);

  void checkIsValidSelector(String selector) {
    if (selector.isBlank) {
      throw ResultError(
        selector,
        functionName: "transform",
        description: "A selector must not be a blank string. If you want to "
            "assign an option to a blank string use 'defaultValue'.",
      );
    }

    if (selector.contains("\n")) {
      throw ResultError(
        selector,
        functionName: "transform",
        description: "A selector must not contain any linefeeds.",
      );
    }

    if (selectorOptionsMap.containsKey(selector)) {
      throw ResultError(
        selector,
        functionName: "transform",
        description:
            "The provided selector already exists. Each selector must be "
            "unique: $selector",
      );
    }
  }

  var i = 0;
  for (final item in options) {
    final (selector: selector, option: stringifiedOption) = stringify(i, item);

    checkIsValidSelector(selector);
    selectorOptionsMap[selector] = item;

    optionsStrBuf.writeIndented(selectorFormatter(selector));
    optionsStrBuf.write("$separator ");
    optionsStrBuf.write(stringifiedOption);
    if (i < options.length - 1) {
      optionsStrBuf.writeln();
    }

    i++;
  }

  final optionsStr = optionsStrBuf.toString();

  oSink.write(promptIndent);
  oSink.writeln(prompt);

  oSink.writeln(optionsStr);

  while (true) {
    final answer = (await answerReader() ?? "").trim();
    if (null != defaultValue && answer.isBlank) return defaultValue;

    final result = selectorOptionsMap[answer];
    if (null != result) return result;

    oSink.writeln();
    oSink.write(errorIndent);
    oSink.writeln(onError(answer));

    oSink.writeln(optionsStr);
  }
}