Files
2026-05-27 23:29:53 +01:00

3.9 KiB

progress

(no submodules)

Purpose

createProgress renders a fixed-width ASCII progress bar for tracking long-running operations with a known total. It writes bar updates to any WritableStreamLike output — TTY or non-TTY — using in-place line clearing on TTY terminals and newline-separated lines on non-TTY streams (CI, pipes). The bar updates are driven by explicit update(current) calls from the caller; no internal timer is used.

The instance is AbortSignal-aware: aborting the signal silently makes all subsequent update() calls no-ops, allowing calling code to cancel a long operation without crashing.

Features

  • Fixed-width block bar with configurable width (default 40 characters) using █ fill and ░ empty characters
  • Percentage and current/total count displayed after the bar: [████████████░░░░░░░░] 60% (60/100)
  • TTY mode: clears the current line and re-renders in-place via clearLine / cursorTo
  • Non-TTY mode: writes each update on its own line for readable CI output
  • render() returns the bar string without writing to the stream — useful for embedding in larger output
  • complete() forces 100% and appends a newline; idempotent (safe to call multiple times)
  • AbortSignal support: abort event listener marks the instance done; pre-fired signal respected via guard check in update()

Usage: Basic Example

import { createProgress } from "@suaveplan/cli/progress/progress";

const p = createProgress({ total: 100 });

for (let i = 0; i <= 100; i++) {
  p.update(i);
  await sleep(20);
}

p.complete();

Usage: Advanced Example

import { createProgress } from "@suaveplan/cli/progress/progress";
import { Writable } from "node:stream";

const controller = new AbortController();
const p = createProgress({
  total: files.length,
  width: 60,
  signal: controller.signal,
});

let processed = 0;
for (const file of files) {
  if (controller.signal.aborted) break;
  await processFile(file);
  p.update(++processed);
}

p.complete();

API Reference

createProgress(opts: ProgressOptions): ProgressInstance

Creates a progress bar instance. No output is written until update() or complete() is called.

opts.total: number — the maximum value (100%). Must be positive.

opts.width?: number — width of the bar in characters (default 40).

opts.stream?: WritableStreamLike — output target (default process.stdout). TTY in-place rendering requires clearLine and cursorTo methods plus isTTY === true.

opts.signal?: AbortSignal — when aborted, future update() calls become no-ops and the bar stops advancing.

ProgressInstance

update(current: number): void — updates the displayed progress to current. Values are clamped to [0, total]. No-op if complete() was already called or the signal was aborted.

complete(): void — writes a 100% bar with a trailing newline and marks the instance done. Idempotent.

render(): string — returns the current bar string without writing to the stream.

ProgressOptionsSchema

Zod schema for ProgressOptions — validates total (positive number) and width (optional positive number). Excludes stream and signal.

WritableStreamLike

Re-exported from ../spinner/spinner.types.js. Any object with write(text: string): void satisfies this interface; clearLine, cursorTo, and isTTY are optional and enable TTY mode when present.

Implementation Notes

Progress percentage is computed as Math.min(1, current / total) to handle over-total updates gracefully. The filled block count is Math.round(pct * width) — rounding avoids a full-bar display at 99% on small widths. The complete() guard (done flag) prevents double-writes when called multiple times. The AbortSignal listener uses { once: true } so it self-removes after the first event.