Skip to content

Progress Indicators ​

Display progress bars and spinners for long-running operations.

Progress Bar ​

Basic Usage ​

scala
import wvlet.uni.cli.ProgressBar

val bar = ProgressBar(total = 100).start()

for i <- 1 to 100 do
  Thread.sleep(50)
  bar.increment()

bar.finish()

Custom Styling ​

scala
import wvlet.uni.cli.{ProgressBar, Tint}

val bar = ProgressBar(total = 1000)
  .withWidth(30)                    // Bar width
  .withPrefix("Downloading: ")      // Text before bar
  .withSuffix(" files")             // Text after bar
  .withShowPercent(true)            // Show percentage
  .withShowCount(true)              // Show count (e.g., 50/100)
  .withColor(Tint.cyan)            // Bar color
  .start()

Update Progress ​

scala
val bar = ProgressBar(total = 100).start()

// Set to specific value
bar.update(50)

// Increment by 1
bar.increment()

// Increment by specific amount
bar.increment(10)

// Check if complete
if bar.isComplete then
  bar.finish()

Success and Failure ​

scala
val bar = ProgressBar(total = 100).start()

try
  for i <- 1 to 100 do
    bar.increment()
    riskyOperation(i)
  bar.finish()  // Success
catch
  case e: Exception =>
    bar.fail()  // Failure
    throw e

Configuration Options ​

OptionDescriptionDefault
totalTotal items to processRequired
widthWidth of the bar in characters40
completeCharacter for completed portion█
incompleteCharacter for incomplete portion░
prefixText before the bar""
suffixText after the bar""
showPercentShow percentagetrue
showCountShow item countfalse
colorBar colorTint.green
hideCursorHide cursor during progresstrue

Spinner ​

For indeterminate operations:

scala
import wvlet.uni.cli.Spinner

val spinner = Spinner()
  .withText("Loading...")
  .start()

// Do work...
Thread.sleep(3000)

spinner.stop()

Spinner Configuration ​

scala
import wvlet.uni.cli.{Spinner, SpinnerStyle, Tint}

val spinner = Spinner()
  .withText("Loading...")
  .withSpinner(SpinnerStyle.Line)
  .withColor(Tint.magenta)
  .withEnabled(true)
  .withHideCursor(true)

Spinner Styles ​

scala
import wvlet.uni.cli.{Spinner, SpinnerStyle}

// Different animation styles
Spinner().withSpinner(SpinnerStyle.Default)
Spinner().withSpinner(SpinnerStyle.Line)

Start with Text Override ​

scala
val spinner = Spinner().withText("Original")

// Override text when starting
val running = spinner.start("Processing...")
running.text shouldBe "Processing..."
running.stop()

Success and Failure States ​

scala
val spinner = Spinner().withText("Processing...").start()

// Mark as successful with message
spinner.succeed("Done!")

// Or mark as failed with message
spinner.fail("Error occurred")

Disabling Spinner ​

scala
// Disable spinner completely
val spinner = Spinner().noSpinner.start("Test")
spinner.isSpinning shouldBe true
spinner.stop()

// Disable color output
val spinner = Spinner().noColor

Disabling Progress ​

For non-interactive environments:

scala
val isInteractive = System.console() != null

val bar = ProgressBar(total = 100)
  .withEnabled(isInteractive)
  .start()

// Works the same, but no output if disabled
bar.increment()
bar.finish()

Example: File Processing ​

scala
import wvlet.uni.cli.{ProgressBar, Tint}
import java.io.File

def processFiles(files: List[File]): Unit =
  val bar = ProgressBar(total = files.size)
    .withPrefix("Processing: ")
    .withShowCount(true)
    .withColor(Tint.blue)
    .start()

  for file <- files do
    processFile(file)
    bar.increment()

  bar.finish()
  println(Tint.green("All files processed!"))

Example: Download Progress ​

scala
def downloadWithProgress(url: String, totalBytes: Long): Unit =
  val bar = ProgressBar(total = totalBytes)
    .withPrefix("Downloading: ")
    .withWidth(50)
    .start()

  var downloaded = 0L
  while downloaded < totalBytes do
    val chunk = downloadChunk(url, downloaded)
    downloaded += chunk.length
    bar.update(downloaded)

  bar.finish()

Example: Spinner with Result ​

scala
import wvlet.uni.cli.Spinner

def fetchData(): Result =
  val spinner = Spinner()
    .withText("Fetching data...")
    .start()

  try
    val result = api.fetchData()
    spinner.succeed("Data fetched successfully")
    result
  catch
    case e: Exception =>
      spinner.fail(s"Failed: ${e.getMessage}")
      throw e

Best Practices ​

  1. Set realistic totals - Know the work upfront
  2. Update frequently enough - But not too often
  3. Handle interruption - Call finish() or fail()
  4. Disable in CI - Check for TTY
  5. Use spinners for unknown duration
  6. Use succeed/fail - Provide clear completion status

Released under the Apache 2.0 License.