Customizable Java Console Progress Indicator: Spinner, Bar, and Percent

Customizable Java Console Progress Indicator: Spinner, Bar, and Percent

Long-running console tasks feel more responsive and professional when you show progress. This article walks through building a small, customizable Java console progress indicator that supports three display modes: spinner, progress bar, and percentage with ETA. The indicator is lightweight, single-file, and thread-safe so you can use it in simple scripts or multi-threaded applications.

Features

  • Spinner mode for unknown-duration tasks
  • Progress bar with current/total and percentage
  • Percentage mode with estimated time remaining (ETA)
  • Configurable width, update interval, and output stream
  • Thread-safe start/update/finish API

Design overview

  • A ProgressIndicator class encapsulates state: total work, current progress, start time, mode, and formatting options.
  • A background thread performs periodic redraws so updates are smooth and throttled.
  • Console output uses carriage return ( ) to overwrite the current line; final output prints a newline.
  • The API exposes start(total), step(n), setProgress(n), and finish() methods.

Progress modes

  • Spinner: cyclic characters (e.g., | / — ) to show activity when total is unknown.
  • Bar: visual bar of configurable width filled proportionally to progress.
  • Percent+ETA: numeric percentage plus estimated remaining time computed from elapsed time and completed units.

Implementation

  • Use synchronized blocks or AtomicLong for safe concurrent updates.
  • Use System.err or a supplied PrintStream to avoid interfering with normal program output.
  • Avoid heavy formatting each update; only redraw when the visible string would change or at a fixed interval.

Example implementation (single-file)

java

import java.io.PrintStream; import java.time.Duration; import java.time.Instant; import java.util.concurrent.atomic.AtomicLong; public class ProgressIndicator { public enum Mode { SPINNER, BAR, PERCENT } private final PrintStream out; private final Mode mode; private final int width; private final long refreshMillis; private final char[] spinnerChars = {’|’,’/’,’-’,’\’}; private final AtomicLong current = new AtomicLong(0); private long total = -1; private volatile boolean running = false; private Thread worker; private Instant start; public ProgressIndicator(Mode mode, int width, long refreshMillis, PrintStream out) { this.mode = mode; this.width = Math.max(10, width); this.refreshMillis = Math.max(50, refreshMillis); this.out = out == null ? System.err : out; } public void start(long total) { if (running) return; this.total = total; running = true; start = Instant.now(); worker = new Thread(this::runLoop); worker.setDaemon(true); worker.start(); } public void step(long delta) { setProgress(current.addAndGet(delta)); } public void setProgress(long value) { current.set(value); } public void finish() { running = false; try { if (worker != null) worker.join(1000); } catch (InterruptedException ignored) {} render(true); out.println(); } private void runLoop() { int spin = 0; String last = ””; while (running) { String s = buildLine(spin); if (!s.equals(last)) { out.print(” “ + s); out.flush(); last = s; } spin = (spin + 1) % spinnerChars.length; try { Thread.sleep(refreshMillis); } catch (InterruptedException ignored) {} } } private String buildLine(int spin) { long cur = current.get(); if (mode == Mode.SPINNER) { return String.valueOf(spinnerChars[spin]) + ” “ + cur + (total > 0 ? (”/” + total) : ””); } else if (mode == Mode.BAR) { if (total <= 0) return buildLineForPercent(cur, 0); double frac = Math.min(1.0, (double)cur / total); int filled = (int)Math.round(frac (width - 10)); StringBuilder bar = new StringBuilder(); bar.append(’[’); for (int i=0;i<filled;i++) bar.append(’=’); for (int i=filled;i<width-10;i++) bar.append(’ ‘); bar.append(”] “); bar.append(String.format(”%d/%d”, cur, total)); return bar.toString(); } else { // PERCENT if (total <= 0) return buildLineForPercent(cur, 0); double frac = Math.min(1.0, (double)cur / total); return buildLineForPercent(cur, (int)Math.round(frac 100)); } } private String buildLineForPercent(long cur, int percent) { if (total <= 0) return String.format(”%d items processed”, cur); Duration elapsed = Duration.between(start, Instant.now()); long secs = Math.max(1, elapsed.getSeconds()); long perItem = cur > 0 ? secs / cur : 0; long remaining = Math.max(0, total - cur); long etaSecs = perItem remaining; String eta = formatSecs(etaSecs); return String.format(”%3d%% (%d/%d) ETA %s”, percent, cur, total, eta); } private String formatSecs(long s) { long h = s / 3600; s %= 3600; long m = s / 60; s %= 60; return (h>0?h+“h “:””) + (m>0?m+“m “:””) + s + “s”; } private void render(boolean finalLine) { String s = buildLine(0); out.print(” “ + s); if (finalLine) out.println(); out.flush(); } }

Usage examples

  • Spinner (unknown total):

java

ProgressIndicator p = new ProgressIndicator(ProgressIndicator.Mode.SPINNER, 40, 100, System.err); p.start(-1); for (int i=0;i<50;i++) { // work… p.step(1); Thread.sleep(120); } p.finish();
  • Progress bar:

java

ProgressIndicator p = new ProgressIndicator(ProgressIndicator.Mode.BAR, 50, 80, System.err); p.start(200); for (int i=0;i<200;i++) { / work / p.step(1); Thread.sleep(40); } p.finish();
  • Percent + ETA:

java

ProgressIndicator p = new ProgressIndicator(ProgressIndicator.Mode.PERCENT, 40, 200, System.err); p.start(1000); for (int i=0;i<1000;i++) { / work */ p.step(1); Thread.sleep(10); } p.finish();

Tips and best practices

  • Use System.err to avoid mixing progress with program output.
  • Throttle updates (100ms–300ms) to reduce CPU and terminal flicker.
  • For multi-threaded progress, centralize updates through AtomicLong or a single updater thread.
  • When redirecting output to a file, the carriage-return overwrite won’t work—print periodic lines instead.

Conclusion

This compact ProgressIndicator gives three useful console modes with a simple, thread-safe API and configurable behavior. It’s easy to extend (colors, estimated speed, per-task labels) while remaining lightweight for scripts and applications.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *