pixbar
Sub-cell-precision two-value progress bar for narrow terminal widths. Uses standard Unicode 1/8 block characters (no font installation required) to get 0.96% step at 13 cells, 0.31% at 40 cells — meaningful resolution even when you only have a handful of columns to spare.
[ ████████▌ buffer here ░░░░░ ]
[]
= "0.1"
Quick start
use Bar;
let s = new
.primary // 0.0 .. 1.0
.secondary // semantically >= primary; rendered as buffer / lookahead
.render;
print!;
Bar::new(width) auto-detects two things from the environment:
- Capability (glyph set) defaults to
EighthBlock; override with.capability(Capability::Ascii)orAPB_FORCE_CAP=ascii. - Color is on if
stdoutis a TTY andNO_COLORis unset, off otherwise. Force with.color(true)/.color(false)or call.render_plain().
What's distinctive
- Two-value overlay: a primary progress and a secondary "buffer / lookahead" marker on the same bar — like a video player's "played + buffered" indicator. None of the standard Rust progress crates ship this.
- Sub-cell precision: 8 sub-positions per cell via
▏▎▍▌▋▊▉█. 1% precision at 13 cells; 0.31% at 40 cells. - Pure-function renderer:
(width, primary, secondary, capability) -> Vec<Cell>is referentially transparent. Snapshot- and property-tested. - Zero runtime dependencies in the library — only the optional
pixbar-benchbinary pulls incrossterm.
What this crate does NOT do
It is a renderer, not a framework. Out of scope:
- ETA / elapsed / throughput tracking — bring your own time math
- Spinner / indeterminate progress
MultiProgressorchestration- Iterator wrapping (
for x in collection.progress()) - Style templating (
"{bar:40} {pos}/{len} {eta}") - Auto-hide on non-TTY
Reach for indicatif if you need those.
Capability tiers
| Capability | Sub-positions per cell | Min width for 1% precision | Characters used |
|---|---|---|---|
Ascii |
1 | 100 | █ and space |
EighthBlock |
8 | 13 | U+2580..U+258F (standard Unicode block elements) |
Auto-detected at runtime (defaults to EighthBlock); override via
APB_FORCE_CAP=ascii|eighth.
Examples
Consumers other than stdout
Bar::render() to stdout is the easy path, but the renderer is also designed
to feed TUI libraries, HTML reports, and pipelines. The Cell IR is the
extension point.
Piping to a file / NO_COLOR / log output
Bar::new() auto-detects this. If you'd rather not rely on detection (e.g.
you're building a string for elsewhere), be explicit:
use Bar;
let plain = new.primary.secondary.render_plain;
// "████████████████████▌ " — glyphs only, no SGR escapes.
.render_plain() is equivalent to .color(false).render().
ratatui (or any cell-grid TUI)
Consume the [Cell] IR directly and paint your own spans. Boundary cells
carry their secondary segment in the background color — see the
boundary-bg note.
A simple consumer that respects this:
use ;
use *;
If your renderer cannot paint cell backgrounds, you have two reasonable
fallbacks: snap boundary cells to the nearest full cell (lose sub-cell
precision), or call bar.render_plain() and accept that boundary glyphs
will look slightly off near the edge.
HTML
Enable the html feature and use pixbar::html::to_html:
= { = "0.1", = ["html"] }
use ;
let bar = new.primary.secondary;
let html = to_html;
// Returns a `<pre>…</pre>` block with inline `style="color:…;background:…"`.
Driving capability and color detection yourself
pixbar::detect() and pixbar::detect_color() are public if you want to
consult them outside the builder:
use ;
let bar = new
.capability
.color
.primary
.secondary;
Architecture
A pure function classifies each cell into one of Empty / PrimaryFull / SecondaryFull / PrimaryBoundary / SecondaryBoundary / DegradedOverlap, with
an integer sub-position. Glyph and color tables are end-of-pipe lookups, so
the renderer can target ANSI (default) or HTML (test-only) from the same
intermediate representation. See
the design doc
for the full algorithm; the spec describes an additional PatchedSixteenth
tier that was implemented and then removed once measurement confirmed
EighthBlock already exceeds the 1% target at any reasonable width.
License
MIT — see LICENSE.