32 releases (4 stable)

Uses new Rust 2024

new 1.3.0 Jan 31, 2026
0.3.2 Nov 26, 2025
0.2.14 May 16, 2025
0.2.8 Mar 30, 2025

#115 in Command-line interface

Download history 248/week @ 2025-10-18 15/week @ 2025-10-25 3/week @ 2025-11-22 1/week @ 2026-01-24 1792/week @ 2026-01-31

1,793 downloads per month
Used in 3 crates

MIT license

185KB
3.5K SLoC

clx

Components for building CLI applications in Rust with rich terminal output.

Features

  • Progress Jobs - Hierarchical progress indicators with spinners, status tracking, and nested child jobs
  • OSC Integration - Terminal progress bar integration for supported terminals (Ghostty, VS Code, Windows Terminal, VTE-based)
  • Styling - Color and formatting utilities for stderr and stdout output
  • Diagnostics - Frame logging for debugging and LLM-friendly verification

Installation

Add to your Cargo.toml:

[dependencies]
clx = "1"

Usage

Progress Jobs

Create progress indicators using the builder pattern:

use clx::progress::{ProgressJobBuilder, ProgressStatus, ProgressJobDoneBehavior};

// Create and start a progress job
let job = ProgressJobBuilder::new()
    .prop("message", "Processing files...")
    .on_done(ProgressJobDoneBehavior::Collapse)
    .start();

// Add child jobs for nested progress
let child = job.add(
    ProgressJobBuilder::new()
        .prop("message", "Subtask 1")
        .build()
);

// Update status when complete
child.set_status(ProgressStatus::Done);
job.set_status(ProgressStatus::Done);

Progress with Percentage

Track progress with current/total values:

let job = ProgressJobBuilder::new()
    .body("{{ spinner() }} {{ message }} {{ progress_bar(flex=true) }}")
    .prop("message", "Downloading")
    .progress_total(100)
    .progress_current(0)
    .start();

// Update progress
for i in 0..=100 {
    job.progress_current(i);
}
job.set_status(ProgressStatus::Done);

Multi-Operation Progress

For tasks with multiple stages (e.g., download → checksum → extract), use start_operations() to track overall progress while showing accurate values for each stage:

let job = ProgressJobBuilder::new()
    .body("{{ spinner() }} {{ message }} {{ bytes() }} {{ progress_bar(width=20) }}")
    .prop("message", "Starting...")
    .start();

// Declare 3 operations
job.start_operations(3);

// Operation 1: Download (50 MB file)
job.message("Downloading...");
job.progress_total(50_000_000);
for i in 0..50 {
    job.progress_current(i * 1_000_000);
    // bytes() shows "25.0 MB / 50.0 MB"
    // OSC terminal progress shows ~16% (halfway through op 1 of 3)
}

// Operation 2: Verify checksum
job.next_operation();
job.message("Verifying...");
job.progress_total(50_000_000);
// OSC shows 33-66% as this progresses

// Operation 3: Extract files
job.next_operation();
job.message("Extracting...");
job.progress_total(200); // 200 files
// bytes() shows file count, OSC shows 66-100%

job.set_status(ProgressStatus::Done);

This ensures the OSC terminal progress indicator (in iTerm2, VS Code, etc.) smoothly advances from 0-100% across all operations, while bytes() and other template functions display the actual values for the current operation.

Custom Templates

Progress jobs use Tera templates:

let job = ProgressJobBuilder::new()
    .body("{{ spinner(name='dots') }} [{{ cur }}/{{ total }}] {{ message }}")
    .prop("message", "Building")
    .prop("cur", &0)
    .prop("total", &10)
    .start();

Available template functions:

  • spinner(name='...') - Animated spinner. Available spinners:
    • Classic: line, dot, mini_dot, jump, pulse, points, hamburger, ellipsis
    • Minimal: arrow, triangle, square, circle, bounce, arc, box_bounce
    • Aesthetic: star, hearts, clock, weather
    • Growing: grow_horizontal, grow_vertical, meter
    • Emoji: globe, moon, monkey, runner, oranges, smiley
  • progress_bar(flex=true) - Progress bar that fills available width
  • progress_bar(width=N) - Fixed-width progress bar
  • elapsed() - Time since job started (e.g., "1m23s")
  • eta() - Estimated time remaining based on progress
  • rate() - Throughput rate (e.g., "42.5/s")
  • bytes() - Progress as human-readable bytes (e.g., "5.2 MB / 10.4 MB")
    • bytes(total=false) - Show only current bytes without total (e.g., "5.2 MB")
    • bytes(hide_complete=true) - Hide when progress reaches 100%

Available template filters:

  • {{ content | flex }} - Truncates content to fit available width
  • {{ content | flex_fill }} - Pads content with spaces to fill available width (for right-aligning subsequent content)
  • Color filters: {{ text | cyan }}, {{ text | blue }}, {{ text | green }}, {{ text | yellow }}, {{ text | red }}, {{ text | magenta }}
  • Style filters: {{ text | bold }}, {{ text | dim }}, {{ text | underline }}

Tera's built-in {% if %} conditionals are also available for conditional rendering.

Right-Aligned Progress Bars

Use flex_fill to push content to the right edge:

let job = ProgressJobBuilder::new()
    .body("{{ spinner() }} {{ message | flex_fill }}{{ progress_bar(flex=true) }}")
    .prop("message", "Downloading")
    .progress_total(100)
    .start();

This produces output like:

⠋ Downloading                              [========>           ]

Status Types

use clx::progress::ProgressStatus;

job.set_status(ProgressStatus::Running);     // Spinner animation
job.set_status(ProgressStatus::Pending);     // Paused indicator
job.set_status(ProgressStatus::Done);        // Success checkmark
job.set_status(ProgressStatus::Failed);      // Error indicator
job.set_status(ProgressStatus::Warn);        // Warning indicator
job.set_status(ProgressStatus::Hide);        // Hidden from display

OSC Terminal Progress

Automatically shows progress in terminal title bars for supported terminals:

use clx::osc;

// Disable OSC progress (must be called before any progress jobs start)
osc::configure(false);

Terminal Lock

Synchronize output with progress display:

use clx::progress::with_terminal_lock;

// Write to stderr without interfering with progress display
with_terminal_lock(|| {
    eprintln!("Log message");
});

Text Mode

For non-interactive environments:

use clx::progress::{set_output, ProgressOutput};

set_output(ProgressOutput::Text);  // Simple text output
set_output(ProgressOutput::UI);    // Rich terminal UI (default)

Diagnostics

Enable frame logging to capture what users see:

CLX_TRACE_LOG=frames.jsonl cargo run --example progress

To preserve ANSI escape codes in the output (useful for debugging color/styling issues):

CLX_TRACE_LOG=frames.jsonl CLX_TRACE_RAW=1 cargo run --example progress

Each line in the log file is a JSON object with:

  • rendered - The exact text displayed (ANSI codes stripped by default, or raw if CLX_TRACE_RAW is set)
  • jobs - Structured array of job states (id, status, message, progress, children)

Example output:

{"rendered":"✔ Task 1\n⠋ Task 2 [5/10]","jobs":[{"id":0,"status":"done","message":"Task 1","progress":null,"children":[]},{"id":1,"status":"running","message":"Task 2","progress":[5,10],"children":[]}]}

This is useful for:

  • Debugging progress display issues
  • Automated testing of CLI output
  • LLM-based verification of user-visible behavior

Using LLMs to Debug Progress Display

The JSONL diagnostic format is designed to be easily parsed by LLMs. When you encounter issues with your progress display, you can capture a diagnostic log and share it with an LLM for analysis.

Capture diagnostic output:

CLX_TRACE_LOG=debug.jsonl cargo run --bin myapp

Share with an LLM:

Provide the contents of debug.jsonl along with your code and a description of the issue. The LLM can analyze:

  • Rendered output - What users actually see on screen (with ANSI codes stripped for readability)
  • Job hierarchy - Parent/child relationships between progress jobs
  • Status transitions - How job statuses change over time (pending → running → done)
  • Progress values - Current/total progress values and whether they update correctly
  • Timing issues - Whether jobs appear/disappear in the expected order

Example prompt:

I'm using clx for progress display but the nested jobs aren't appearing correctly.
Here's my code:
[paste your code]

Here's the diagnostic output:
[paste contents of debug.jsonl]

Can you identify why the child jobs aren't visible?

What LLMs can help diagnose:

  • Jobs that never transition from pending to running
  • Child jobs not properly nested under parents
  • Progress bars not updating (progress values stay static)
  • Jobs completing in wrong order
  • Template rendering issues (missing variables, malformed output)
  • Flex/truncation problems (content not fitting terminal width)

The structured jobs array provides machine-readable state that complements the human-readable rendered field, making it straightforward for LLMs to correlate what the code intended with what users actually see.

Threading Model

clx's progress system is designed for safe concurrent access from multiple threads. Understanding its threading model helps when integrating with multi-threaded applications.

How It Works

  1. Background Thread: A dedicated thread refreshes the display at regular intervals (default 200ms)
  2. Lazy Start: The thread only starts when the first job update occurs
  3. Auto Stop: The thread exits automatically when all jobs complete
  4. Smart Refresh: Skips terminal writes when output is unchanged

Multi-threaded Example

use clx::progress::{ProgressJobBuilder, ProgressStatus, with_terminal_lock};
use std::sync::Arc;
use std::thread;

let job = ProgressJobBuilder::new()
    .prop("message", "Processing")
    .progress_total(100)
    .start();

// Clone Arc for each worker thread
let handles: Vec<_> = (0..4).map(|i| {
    let job = Arc::clone(&job);
    thread::spawn(move || {
        for j in 0..25 {
            job.progress_current(i * 25 + j);
        }
    })
}).collect();

for h in handles {
    h.join().unwrap();
}

job.set_status(ProgressStatus::Done);

Synchronizing with Logging

Use with_terminal_lock() to prevent your output from being overwritten by progress updates:

use clx::progress::with_terminal_lock;

// Safe to write without interference from progress display
with_terminal_lock(|| {
    eprintln!("Log message");
});

Or use the println() method on a job, which handles the locking automatically:

job.println("Found 42 files to process");

Text Mode for CI/Pipes

When stdout/stderr isn't a terminal, use text mode to disable cursor manipulation:

use clx::progress::{set_output, ProgressOutput};

if std::env::var("CI").is_ok() || !console::user_attended_stderr() {
    set_output(ProgressOutput::Text);
}

Controlling the Refresh Loop

Function Effect
pause() Clear display and stop refreshing
resume() Restore display and resume refreshing
stop() Stop loop and render final state
stop_clear() Stop loop and clear display
set_interval(d) Change refresh interval
flush() Force immediate refresh

API Overview

clx::progress

Type Description
ProgressJobBuilder Builder for creating progress jobs
ProgressJob Active progress job handle
ProgressStatus Job status enum (Running, Done, Failed, etc.)
ProgressJobDoneBehavior What to do when job completes (Keep, Collapse, Hide)
ProgressOutput Output mode (UI, Text)

ProgressJob Methods

Method Description
progress_current(n) Set current progress value
progress_total(n) Set total progress value
increment(n) Increment progress by n
start_operations(n) Declare n operations for multi-operation tracking
next_operation() Advance to the next operation
message(s) Set the message property
prop(key, val) Set a template property
set_status(s) Set job status
set_body(s) Change the template
println(s) Print a line without interfering with display
add(job) Add a child job
remove() Remove this job from display

Module Functions

Function Description
with_terminal_lock(f) Execute function with terminal lock held
set_output(mode) Set output mode
output() Get current output mode
set_interval(duration) Set refresh interval
interval() Get current refresh interval
flush() Force refresh
stop() Stop progress display
stop_clear() Stop and clear progress display

clx::osc

Type Description
ProgressState OSC progress state (None, Normal, Error, Indeterminate, Warning)
Function Description
configure(enabled) Enable/disable OSC progress

Examples

Run the included examples:

cargo run --example progress      # Basic progress demo
cargo run --example styling       # Styling demo
cargo run --example osc_progress  # OSC progress demo
cargo run --example right_align   # Right-aligned progress bars

License

MIT

Dependencies

~10–14MB
~262K SLoC