promptSelect<T> static method

T promptSelect<T>({
  1. required String message,
  2. required List<T> options,
  3. required String labelProvider(
    1. T
    ),
  4. int defaultIndex = 0,
})

Prompts the user to select one option using arrow keys.

Implementation

static T promptSelect<T>({
  required String message,
  required List<T> options,
  required String Function(T) labelProvider,
  int defaultIndex = 0,
}) {
  // If not running in a terminal (e.g. piped input, CI, tests), fallback to text prompt
  if (!stdin.hasTerminal) {
    print('\n${Logger.green}?${Logger.reset} ${Logger.bold}$message${Logger.reset}');
    for (int i = 0; i < options.length; i++) {
      final isDefault = i == defaultIndex;
      final label = labelProvider(options[i]);
      final indexStr = '${Logger.gray}[${i + 1}]${Logger.reset}';
      if (isDefault) {
        print('    $indexStr ${Logger.cyan}$label (default)${Logger.reset}');
      } else {
        print('    $indexStr $label');
      }
    }
    while (true) {
      stdout.write('  ${Logger.bold}Enter option (1-${options.length}) [${defaultIndex + 1}]:${Logger.reset} ');
      final input = stdin.readLineSync(encoding: utf8)?.trim() ?? '';
      if (input.isEmpty) {
        return options[defaultIndex];
      }
      final choice = int.tryParse(input);
      if (choice != null && choice >= 1 && choice <= options.length) {
        return options[choice - 1];
      }
      Logger.warning('Invalid option. Please enter a number between 1 and ${options.length}.');
    }
  }

  // Hide cursor
  stdout.write('\x1B[?25l');

  print('${Logger.green}?${Logger.reset} ${Logger.bold}$message${Logger.reset} ${Logger.gray}(Use arrow keys, press Enter to select)${Logger.reset}');

  int selectedIndex = defaultIndex;

  void drawOptions() {
    for (int i = 0; i < options.length; i++) {
      final isSelected = i == selectedIndex;
      final marker = isSelected ? '${Logger.cyan}❯${Logger.reset}' : ' ';
      final label = labelProvider(options[i]);
      if (isSelected) {
        print('  $marker ${Logger.cyan}$label${Logger.reset}');
      } else {
        print('    $label');
      }
    }
  }

  drawOptions();

  while (true) {
    final key = _readKey();
    bool changed = false;

    if (key == 1) { // UP
      if (selectedIndex > 0) {
        selectedIndex--;
      } else {
        selectedIndex = options.length - 1;
      }
      changed = true;
    } else if (key == 2) { // DOWN
      if (selectedIndex < options.length - 1) {
        selectedIndex++;
      } else {
        selectedIndex = 0;
      }
      changed = true;
    } else if (key == 10) { // ENTER
      // Move cursor up to overwrite previous prompt block
      stdout.write('\x1B[${options.length + 1}A');
      // Clear from cursor down
      stdout.write('\x1B[J');
      // Print clean confirmation line
      print('${Logger.green}✔${Logger.reset} ${Logger.bold}$message${Logger.reset} ${Logger.cyan}${labelProvider(options[selectedIndex])}${Logger.reset}');

      // Restore cursor
      stdout.write('\x1B[?25h');
      return options[selectedIndex];
    }

    if (changed) {
      // Move cursor up to redraw options
      stdout.write('\x1B[${options.length}A');
      stdout.write('\x1B[J');
      drawOptions();
    }
  }
}