createSpinner function

Spinner createSpinner(
  1. WriteStream stream,
  2. String text, {
  3. ColorTheme? color,
  4. Symbols? symbols,
  5. EnvLookup? readEnv,
  6. SpinnerTimerFactory? startInterval,
})

The optional color/symbols/readEnv/startInterval overrides are the DI seams replacing the vitest theme mock, vi.stubEnv, and fake timers.

Implementation

Spinner createSpinner(
  WriteStream stream,
  String text, {
  ColorTheme? color,
  Symbols? symbols,
  EnvLookup? readEnv,
  SpinnerTimerFactory? startInterval,
}) {
  final theme = color ?? _defaultColorTheme();
  final syms = symbols ?? SYMBOLS;
  final isCI = _resolveIsCI(readEnv ?? lookupPlatformEnv);
  final frames = syms.spinner;
  var frameIndex = 0;
  SpinnerTimer? intervalId;
  var running = true;

  void clear() {
    // The TS guard also checks that clearLine/cursorTo exist; the Dart
    // WriteStream interface always provides them.
    if (stream.isTTY) {
      stream.clearLine(0);
      stream.cursorTo(0);
    }
  }

  void writeLine(String msg) {
    clear();
    stream.write('$msg\n');
  }

  void render() {
    if (!running) {
      return;
    }
    final frame = '${theme.info(frames[frameIndex])} ${theme.dim(text)}';
    if (stream.isTTY) {
      // Overwrite the previous frame in place instead of erasing the whole
      // line first: move to column 0, redraw over the old glyph/text, then
      // clear only any trailing leftover from a longer previous frame. A full
      // `\x1B[2K` erase before the redraw leaves a momentary blank line that
      // reads as flicker on Windows terminals; overwriting avoids it.
      stream.cursorTo(0);
      stream.write(frame);
      stream.clearLine(1);
    } else {
      stream.write(frame);
    }
    frameIndex = (frameIndex + 1) % frames.length;
  }

  if (stream.isTTY && !isCI) {
    intervalId = (startInterval ?? _startPeriodicTimer)(
      const Duration(milliseconds: 80),
      render,
    );
    render();
  } else {
    stream.write('${theme.dim(syms.bullet)} ${theme.dim(text)}\n');
  }

  return _SpinnerHandle(
    (String msg) {
      running = false;
      intervalId?.cancel();
      writeLine('${theme.success(syms.success)} ${theme.success(msg)}');
    },
    (String msg) {
      running = false;
      intervalId?.cancel();
      writeLine('${theme.error(syms.error)} ${theme.error(msg)}');
    },
    (String msg) {
      running = false;
      intervalId?.cancel();
      writeLine('${theme.warn(syms.warn)} ${theme.warn(msg)}');
    },
    () {
      running = false;
      intervalId?.cancel();
      clear();
    },
  );
}