Metadata-Version: 2.4
Name: progz
Version: 1.0.0
Summary: Lightweight, dependency-free terminal progress bar with unique, customizable styles
Author-email: Geo Joseph <geodesignx@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Geo Joseph
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/geojoseph19/progz
Project-URL: Repository, https://github.com/geojoseph19/progz
Project-URL: Issues, https://github.com/geojoseph19/progz/issues
Project-URL: Changelog, https://github.com/geojoseph19/progz/blob/main/CHANGELOG.md
Keywords: progress,bar,terminal,cli,animation
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Terminals
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: hypothesis; extra == "dev"
Dynamic: license-file

# ProgZ

[![CI](https://github.com/geojoseph19/progz/actions/workflows/ci.yml/badge.svg)](https://github.com/geojoseph19/progz/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/progz)](https://pypi.org/project/progz/)

Lightweight, dependency-free terminal progress bar with unique, customizable styles.

![progz](https://raw.githubusercontent.com/geojoseph19/progz/main/docs/gifs/track.gif)

```
⠹ ━━━━━━━━━━━━━━────────── processing item 42
```

Zero runtime dependencies. Pure Python 3.10+ stdlib.

---

## Features

- One-liner: `for item in track(items):`
- 24-bit ANSI RGB rendering, no dependencies
- Braille spinner tied to elapsed time
- Composable layout: stack spinner, bar, text, percent, count, rate, ETA, elapsed, description in any order
- Progress-based colors: map any percentage or range to a color via `color_stops`, with optional gradient blending
- Sub-cell resolution via eighth-block glyphs (`BLOCKS` preset)
- Indeterminate mode for unknown totals: bouncing bar, plain counts
- Throttled redraws (30 Hz default): a loop over 10 million items stays cheap
- Truncation to terminal width, so long lines never wrap-corrupt
- `bar.write()` prints log lines above a live bar
- Transient bars that erase themselves on finish
- ASCII fallback for dumb terminals; ANSI works on Windows 10+ out of the box
- Context manager and manual API
- Fully customizable via `Style` dataclass
- Python 3.10+, fully typed

---

## Gallery

Recordings below are generated from the files in `examples/`.

Presets (`SHIMMER`, `ASCII`, `BLOCKS`, `MINIMAL`, `RAINBOW`):

![presets](https://raw.githubusercontent.com/geojoseph19/progz/main/docs/gifs/presets.gif)

Numeric readouts (`PERCENT`, `COUNT`, `RATE`, `ETA`, `ELAPSED`):

![numbers](https://raw.githubusercontent.com/geojoseph19/progz/main/docs/gifs/numbers.gif)

Indeterminate mode (unknown total):

![indeterminate](https://raw.githubusercontent.com/geojoseph19/progz/main/docs/gifs/indeterminate.gif)

Logging above a live bar with `bar.write()`:

![write above](https://raw.githubusercontent.com/geojoseph19/progz/main/docs/gifs/write_above.gif)

---

## Installation

```bash
pip install progz
```

---

## Quick Start

```python
from progz import track

for item in track(items, description="processing"):
    process(item)
```

Or with explicit control:

```python
from progz import ProgressBar

with ProgressBar(total=len(items)) as bar:
    for item in items:
        process(item)
        bar.update()
```

---

## Examples

### With description

```python
with ProgressBar(total=100, description="loading") as bar:
    for i, item in enumerate(items):
        process(item)
        bar.update(description=f"item {i}")
```

### Update description without advancing

```python
with ProgressBar(total=100) as bar:
    bar.set_description("warming up")
    time.sleep(1)
    for item in items:
        process(item)
        bar.update()
```

### Manual (no context manager)

```python
bar = ProgressBar(total=100)
for item in items:
    process(item)
    bar.update()
bar.finish()
```

### Numeric readouts

```python
from progz import ProgressBar, Style, Component

style = Style(layout=(
    Component.SPINNER,
    Component.BAR,
    Component.PERCENT,
    Component.COUNT,
    Component.RATE,
    Component.ETA,
    Component.ELAPSED,
))

with ProgressBar(total=10_000, style=style) as bar:
    ...
```

Renders readouts like ` 42% 4200/10000 1.2k it/s ~00:04 00:03`. The rate
is an exponentially weighted moving average with O(1) state; ETA derives
from it.

### Unknown total (indeterminate mode)

```python
for chunk in track(stream(), description="receiving"):
    handle(chunk)

# or explicitly
bar = ProgressBar(total=None)
```

Without a total, `BAR` renders a bouncing segment, `PERCENT` and `ETA`
show `--`, and `COUNT` shows a plain count. `track()` falls back to this
automatically for iterables without `len()`.

### Printing above a live bar

```python
with ProgressBar(total=100) as bar:
    for i, item in enumerate(items):
        if i % 10 == 0:
            bar.write(f"checkpoint {i}")
        process(item)
        bar.update()
```

`bar.write()` erases the bar line, prints the message plus a newline, and
redraws the bar below it.

### Transient bars

```python
with ProgressBar(total=100, transient=True) as bar:
    ...
# bar line is erased on finish; the log stays clean
```

### ASCII fallback

```python
from progz import ProgressBar, ASCII

with ProgressBar(total=100, style=ASCII) as bar:
    ...
```

### Custom style

```python
from progz import ProgressBar, Style

style = Style(
    bar_width=40,
    speed=1.5,
    filled_char="█",
    empty_char="░",
)

with ProgressBar(total=100, style=style) as bar:
    ...
```

### Progress-based colors

`color_stops` maps progress to the bar fill color. Each stop is
`(threshold, (r, g, b))`; the fill uses the last stop at or below the
current progress ratio.

```python
from progz import ProgressBar, Style

# Red below 50%, yellow from 50%, green from 90%
style = Style(color_stops=(
    (0.0, (220, 60, 60)),
    (0.5, (230, 200, 60)),
    (0.9, (80, 200, 120)),
))
```

Add `interpolate=True` to blend colors smoothly between stops. Add
`color_by_position=True` to color each bar cell by its own position
instead of the current progress: the bar fills left to right and each
cell keeps its percentage's color, so a finished bar shows the full
color journey.

```python
# Smooth red-to-green gradient painted across the bar
style = Style(
    color_stops=((0.0, (220, 60, 60)), (1.0, (80, 200, 120))),
    interpolate=True,
    color_by_position=True,
)
```

---

## API Reference

### `track(iterable, description="", total=None, style=None, file=None, refresh_rate=30.0, transient=False)`

Iterate while drawing a progress bar. `total` is inferred via `len()`
when available; otherwise the bar runs in indeterminate mode. The bar
always reaches a final state, including when the loop raises or stops
early. Remaining parameters match `ProgressBar`.

### `ProgressBar(total, description="", style=None, file=None, refresh_rate=30.0, transient=False)`

| Parameter      | Type            | Default       | Description                     |
|----------------|-----------------|---------------|---------------------------------|
| `total`        | `int \| None`   | required      | Steps to completion; `None` for indeterminate mode |
| `description`  | `str`           | `""`          | Text shown to the right of bar  |
| `style`        | `Style \| None` | `SHIMMER`     | Visual style configuration      |
| `file`         | `TextIO \| None`| `sys.stderr`  | Output stream (pass `sys.stdout` to print inline) |
| `refresh_rate` | `float`         | `30.0`        | Max redraws per second; `0` disables throttling |
| `transient`    | `bool`          | `False`       | Erase the bar on finish instead of keeping it |

#### Methods

| Method                                | Description                              |
|---------------------------------------|------------------------------------------|
| `update(n=1, description=None)`       | Advance by n steps, optionally update description |
| `set_description(description)`        | Update description without advancing     |
| `write(message)`                      | Print a line above the live bar          |
| `finish()`                            | Complete bar and move to next line       |
| `completed` *(property)*             | Current completed count                  |
| `total` *(property)*                 | Total steps; `None` in indeterminate mode |

---

### `Style`

```python
@dataclass(frozen=True)
class Style:
    layout: tuple[Component, ...] = (Component.SPINNER, Component.BAR, Component.DESCRIPTION)
    bar_width: int = 24                              # characters wide
    speed: float = 0.6                               # shimmer sweep cycles/sec
    filled_char: str = "━"                           # character for filled portion
    empty_char: str = "─"                            # character for empty portion
    fill_text: str = ""                              # string rendered by Component.TEXT
    min_brightness: int = 80                         # grey floor (0 to 255)
    brightness_range: int = 175                      # grey range above floor
    empty_rgb: RGB = (60, 60, 60)                    # empty zone color
    spinner_color_rgb: RGB = (0, 200, 200)
    spinner_frames: tuple[str, ...] = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
    color_stops: tuple[tuple[float, RGB], ...] = ((0.0, (255, 255, 255)),)
    interpolate: bool = False                        # blend between adjacent stops
    color_by_position: bool = False                  # cells keep their own position color
    block_chars: tuple[str, ...] = ()                # partial-fill glyphs for sub-cell resolution
```

`RGB` is a type alias for `tuple[int, int, int]`, exported from `progz`.

`color_stops` thresholds must be strictly increasing, in 0.0 to 1.0.
`Style` raises `ValueError` otherwise. Progress below the first
threshold uses the first stop's color. The shimmer wave modulates
brightness on top of the stop color; the default single white stop is
the classic greyscale shimmer. Stops are ignored when color is off.

### `Component`

The `layout` tuple selects which components render, and in what order (left to
right). Omit a component to hide it. Import from `progz`.

| Component               | Renders                                              |
|-------------------------|------------------------------------------------------|
| `Component.SPINNER`     | Animated braille frame; skipped when color is off    |
| `Component.BAR`         | Fill bar tied to progress; bouncing segment when total is unknown |
| `Component.TEXT`        | `fill_text` string with a shimmer wave               |
| `Component.PERCENT`     | Percentage readout, e.g. ` 42%`; ` --%` when total is unknown |
| `Component.DESCRIPTION` | The `description` label, rendered unstyled           |
| `Component.COUNT`       | `42/1000` readout; plain count when total is unknown |
| `Component.ELAPSED`     | Elapsed time, `01:23` (`h:mm:ss` past one hour)      |
| `Component.RATE`        | Smoothed throughput, e.g. `1.2k it/s`                |
| `Component.ETA`         | Estimated time remaining, e.g. `~00:45`              |

Note: a layout containing only `Component.SPINNER` renders nothing when
color is off (for example in CI or with `NO_COLOR` set). Include `BAR`,
`PERCENT`, or `DESCRIPTION` if output must be visible without color.

```python
from progz import ProgressBar, Style, Component

# Bar with a percentage readout, no spinner
style = Style(layout=(Component.BAR, Component.PERCENT, Component.DESCRIPTION))

with ProgressBar(total=100, style=style) as bar:
    ...
```

### Pre-defined styles

| Name      | Description                        |
|-----------|------------------------------------|
| `SHIMMER` | Unique sine-wave brightness gradient, Unicode chars (default) |
| `ASCII`   | `#`/`-` chars, `(BAR, DESCRIPTION)` layout (no spinner) |
| `BLOCKS`  | Eighth-block sub-cell fill (`▏▎▍▌▋▊▉█`): 8 visible states per cell |
| `MINIMAL` | Bar plus percent, nothing else     |
| `RAINBOW` | Interpolated multi-color gradient painted across the bar |

---

## Color and platform handling

Detection runs once, at construction time:

- `NO_COLOR` disables color and wins over everything.
- `FORCE_COLOR` enables color even for non-TTY streams.
- `TERM=dumb` disables color.
- On Windows 10+, virtual terminal processing is enabled automatically
  (stdlib `ctypes`, no colorama). If the console rejects it, progz falls
  back to plain output instead of escape garbage.

## Logging alongside a bar

Route log output through `bar.write()` and the bar coexists with the
stdlib `logging` module:

```python
import logging

class BarHandler(logging.Handler):
    def __init__(self, bar):
        super().__init__()
        self.bar = bar

    def emit(self, record):
        self.bar.write(self.format(record))
```

---

## Performance Notes

- Redraws are throttled (default 30 Hz): between draws, `update()` is a
  counter bump plus one `time.monotonic()` call. `finish()` always draws
  the final frame, so no state change is ever lost.
- Per-update cost is O(1) in `total`: a bar over 10 million items is as
  cheap per step as one over 10.
- `render_frame()` is pure: no I/O, no side effects
- No background threads; animation samples elapsed time on `update()`
- Rendered lines are truncated to the terminal width, so a narrow
  terminal never wrap-corrupts
- Uses `\r\033[2K` to overwrite in color mode; space-padding in ASCII mode
- 24-bit RGB via raw ANSI sequences, no terminal library needed
- `benchmarks/bench.py` measures import time, per-update cost, and frame
  render cost; CI enforces an import-time budget

---

## Contributing

1. Fork the repo
2. `pip install -e ".[dev]"`
3. `pytest && mypy src/progz`
4. Submit a PR

---

## License

MIT
