multiSelect<T> method
Asks the user to pick any number of options from a numbered list.
Accepts comma or space separated numbers, ranges like 1-3, and all.
Answering nothing takes defaults; when there are no defaults the
question is asked again, because an empty selection is never what the
caller wanted — a job with no platform cannot be built.
Implementation
List<T> multiSelect<T>(
String question,
List<T> options, {
required String Function(T) label,
String Function(T)? describe,
List<T> defaults = const [],
}) {
_logger.logInfo('${ColorizeLogger.bold('?')} $question');
for (var i = 0; i < options.length; i++) {
final marker = defaults.contains(options[i]) ? '›' : ' ';
final description = describe?.call(options[i]);
_logger.logInfo(
' $marker ${i + 1}) ${label(options[i])}'
'${description == null ? '' : ' ${ColorizeLogger.dim(description)}'}',
);
}
final hint = defaults.isEmpty
? '1-${options.length}, comma separated, or "all"'
: '1-${options.length} or "all" '
'(${defaults.map((d) => options.indexOf(d) + 1).join(',')})';
while (true) {
stdout.write(' ${ColorizeLogger.dim(hint)} ');
final answer = _readLine().trim().toLowerCase();
if (answer.isEmpty) {
if (defaults.isNotEmpty) return List<T>.from(defaults);
_logger.logWarning('pick at least one');
continue;
}
if (answer == 'all' || answer == 'a' || answer == '*') {
return List<T>.from(options);
}
final picked = _parseIndexes(answer, options.length);
if (picked == null) {
_logger.logWarning(
'use numbers between 1 and ${options.length}, e.g. "1,3" or "1-2"',
);
continue;
}
if (picked.isEmpty) {
_logger.logWarning('pick at least one');
continue;
}
return picked.map((i) => options[i]).toList();
}
}