CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-stdio

Standard input/output manager for Node.js with command-line parsing, async file reading, interactive terminal, and progress bars

Pending
Overview
Eval results
Files

progress-bar.mddocs/

Progress Bars

Visual command-line progress bars with automatic time calculations, customizable display, and ETA predictions for long-running operations.

Capabilities

ProgressBar Class

Creates and manages visual progress bars with automatic formatting and time estimation.

/**
 * Visual progress bar for command-line applications
 * @param size - Total progress value (default: 100)
 * @param options - Display and behavior configuration
 */
class ProgressBar {
  constructor(
    size?: number,
    options?: {
      /** Increment size for each tick() call (default: 1) */
      tickSize?: number;
      /** Suppress all output (default: false) */
      silent?: boolean;
      /** Terminal width for bar formatting (default: process.stdout.columns || 70) */
      terminalWidth?: number;
    }
  );
  
  /** Set current progress to specific value */
  setValue(value: number): string;
  
  /** Increment progress by tickSize amount */
  tick(): string;
  
  /** Register callback to execute when progress reaches 100% */
  onFinish(callback: Function): void;
  
  /** Get elapsed time in milliseconds since creation */
  getEllapsed(): number;
  
  /** Get estimated remaining time in milliseconds */
  getRemaining(): number;
  
  /** Render current progress bar and return display string */
  print(): string;
  
  /** Write text to stdout (respects silent mode) */
  write(text: string): void;
}

Usage Examples:

import { ProgressBar } from "stdio";

// Basic progress bar
const progress = new ProgressBar(100);
for (let i = 0; i <= 100; i++) {
  progress.setValue(i);
  await delay(50); // Simulate work
}

// Using tick() for incremental progress
const progress = new ProgressBar(1000, { tickSize: 10 });
while (progress.value < progress.size) {
  await processItem();
  progress.tick(); // Increment by 10
}

// Custom configuration
const progress = new ProgressBar(50, {
  tickSize: 2,
  silent: false,
  terminalWidth: 80
});

// With completion callback
const progress = new ProgressBar(100);
progress.onFinish(() => {
  console.log('\nProcessing complete!');
});

for (let i = 0; i <= 100; i++) {
  progress.setValue(i);
  await simulateWork();
}
// Callback executes automatically when setValue(100) is called

// File processing example
import { createReadStream } from 'fs';
import { stat } from 'fs/promises';

async function processLargeFile(filename: string) {
  const stats = await stat(filename);
  const progress = new ProgressBar(stats.size);
  
  const stream = createReadStream(filename);
  let bytesProcessed = 0;
  
  stream.on('data', (chunk) => {
    bytesProcessed += chunk.length;
    progress.setValue(bytesProcessed);
  });
  
  stream.on('end', () => {
    console.log('\nFile processing complete!');
  });
}

Progress Display Format

Progress bars automatically format with time information and ETA:

00:01:23 45% [##############################·····················] ETA 00:01:52

Display Components:

  • Elapsed Time: 00:01:23 - Time since progress started
  • Percentage: 45% - Current completion percentage
  • Visual Bar: [###···] - Visual representation with # for completed, · for remaining
  • ETA: 00:01:52 - Estimated time remaining

Constructor Options

Comprehensive configuration for progress bar behavior and appearance.

interface ProgressBarOptions {
  /** 
   * Increment size for tick() calls (default: 1)
   * Determines how much progress increases with each tick()
   */
  tickSize?: number;
  
  /** 
   * Suppress all output (default: false)
   * When true, progress bar is tracked but not displayed
   */
  silent?: boolean;
  
  /** 
   * Terminal width for formatting (default: process.stdout.columns || 70)
   * Controls total width of progress bar display
   */
  terminalWidth?: number;
}

Instance Properties

Access to progress bar state and configuration:

class ProgressBar {
  /** Total progress value (set in constructor) */
  size: number;
  
  /** Increment size for tick() calls */
  tickSize: number;
  
  /** Current progress value */
  value: number;
  
  /** Progress start timestamp */
  startTime: number;
  
  /** Recent time estimates for smoothing calculations */
  lastRemainingTimes: number[];
  
  /** Output suppression flag */
  silent: boolean;
  
  /** Terminal width for formatting */
  terminalWidth: number;
  
  /** Completion callback function */
  callback: Function;
}

Advanced Features

Time Calculation and ETA

Sophisticated time estimation with smoothing algorithms:

// ETA calculation uses running average of recent processing times
const progress = new ProgressBar(1000);

for (let i = 0; i < 1000; i++) {
  // Variable processing times are automatically handled
  const workTime = Math.random() * 100 + 50;
  await delay(workTime);
  
  progress.tick();
  
  // ETA becomes more accurate as more data points are collected
  console.log(`ETA: ${progress.getRemaining()}ms`);
}

Time Features:

  • Elapsed Time: Accurate tracking from progress start
  • ETA Smoothing: Uses rolling average of last 5 time measurements
  • Completion Detection: ETA shows 0 when progress reaches 100%
  • Time Formatting: Automatic formatting with days, hours, minutes, seconds

Visual Customization

Automatic visual formatting based on terminal width:

// Progress bar adapts to available terminal space
const progress = new ProgressBar(100, { terminalWidth: 120 });

// Bar width calculation:
// Total width = terminalWidth
// Bar width = terminalWidth - elapsed_text - percentage - eta_text
// Example: 120 - 8 - 4 - 12 = 96 characters for the actual bar

// On narrow terminals
const narrowProgress = new ProgressBar(100, { terminalWidth: 40 });
// Automatically adjusts bar width to fit

Silent Mode and Testing

Support for background processing and testing scenarios:

// Silent mode for background processing
const silentProgress = new ProgressBar(100, { silent: true });

// Progress is tracked but not displayed
for (let i = 0; i <= 100; i++) {
  silentProgress.setValue(i);
  // No output to terminal
}

// Check progress programmatically
console.log(`Final progress: ${silentProgress.value}/${silentProgress.size}`);

// Testing progress bars
function testProgressBar() {
  const progress = new ProgressBar(10, { silent: true });
  
  // Verify behavior without terminal output
  assert.equal(progress.value, 0);
  
  progress.tick();
  assert.equal(progress.value, 1);
  
  progress.setValue(5);
  assert.equal(progress.value, 5);
  
  const displayString = progress.print();
  assert(displayString.includes('50%'));
}

Error Handling and Edge Cases

Robust handling of various scenarios:

const progress = new ProgressBar(100);

// Values beyond size are clamped
progress.setValue(150);
console.log(progress.value); // 100 (clamped to size)

// Negative values are handled
progress.setValue(-10);
console.log(progress.value); // 0 (clamped to minimum)

// Division by zero protection
const zeroProgress = new ProgressBar(0);
zeroProgress.setValue(0);
// Safely handles edge case without crashing

// Completion callback safety
progress.onFinish(() => {
  throw new Error('Callback error');
});

try {
  progress.setValue(100); // Completion triggers callback
} catch (error) {
  console.error('Callback failed:', error.message);
  // Progress bar itself remains stable
}

Integration Patterns

Common patterns for different use cases:

File Upload/Download:

async function uploadFile(file: File, url: string) {
  const progress = new ProgressBar(file.size);
  
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch(url, {
    method: 'POST',
    body: formData
  });
  
  // Track upload progress (if supported by fetch implementation)
  const reader = response.body.getReader();
  let uploaded = 0;
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    uploaded += value.length;
    progress.setValue(uploaded);
  }
  
  return response;
}

Batch Processing:

async function processBatch<T>(
  items: T[], 
  processor: (item: T) => Promise<void>
): Promise<void> {
  const progress = new ProgressBar(items.length);
  
  progress.onFinish(() => {
    console.log(`\nProcessed ${items.length} items successfully!`);
  });
  
  for (let i = 0; i < items.length; i++) {
    try {
      await processor(items[i]);
      progress.tick();
    } catch (error) {
      console.error(`\nError processing item ${i}:`, error.message);
      // Continue with remaining items
      progress.tick();
    }
  }
}

Nested Progress:

async function complexOperation() {
  const mainProgress = new ProgressBar(3);
  
  console.log('Phase 1: Data Collection');
  await collectData();
  mainProgress.tick();
  
  console.log('Phase 2: Data Processing');
  await processDataWithProgress(); // This has its own progress bar
  mainProgress.tick();
  
  console.log('Phase 3: Results Generation');
  await generateResults();
  mainProgress.tick();
  
  console.log('Operation complete!');
}

async function processDataWithProgress() {
  const subProgress = new ProgressBar(1000, { 
    terminalWidth: 60 // Smaller bar for sub-operation
  });
  
  for (let i = 0; i < 1000; i++) {
    await processItem(i);
    subProgress.tick();
  }
}

Constants

/** Default terminal width when process.stdout.columns is unavailable */
const DEFAULT_TERMINAL_WIDTH = 70;

Install with Tessl CLI

npx tessl i tessl/npm-stdio

docs

ask.md

getopt.md

index.md

progress-bar.md

read.md

readLine.md

tile.json