barflow 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {barflow-0.2.1/src/barflow.egg-info → barflow-0.2.2}/PKG-INFO +1 -1
  2. barflow-0.2.2/benchmarks/bench_import_to_iter.py +187 -0
  3. barflow-0.2.2/examples/gallery.py +271 -0
  4. barflow-0.2.2/examples/presets_gallery.py +145 -0
  5. {barflow-0.2.1 → barflow-0.2.2}/pyproject.toml +1 -1
  6. {barflow-0.2.1 → barflow-0.2.2}/setup.py +0 -50
  7. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/__init__.py +1 -1
  8. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/_core.cpp +94 -72
  9. barflow-0.2.2/src/barflow/bar_styles.py +450 -0
  10. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/spinners.py +53 -0
  11. barflow-0.2.2/src/barflow/themes.py +925 -0
  12. {barflow-0.2.1 → barflow-0.2.2/src/barflow.egg-info}/PKG-INFO +1 -1
  13. {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/SOURCES.txt +3 -1
  14. barflow-0.2.1/benchmarks/pgo_train.py +0 -102
  15. barflow-0.2.1/src/barflow/bar_styles.py +0 -203
  16. barflow-0.2.1/src/barflow/themes.py +0 -386
  17. {barflow-0.2.1 → barflow-0.2.2}/LICENSE +0 -0
  18. {barflow-0.2.1 → barflow-0.2.2}/MANIFEST.in +0 -0
  19. {barflow-0.2.1 → barflow-0.2.2}/README.md +0 -0
  20. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench.py +0 -0
  21. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_first_frame.py +0 -0
  22. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_memory.py +0 -0
  23. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_metadata_churn.py +0 -0
  24. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_multibar.py +0 -0
  25. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_raw.md +0 -0
  26. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_tail_latency.py +0 -0
  27. {barflow-0.2.1 → barflow-0.2.2}/benchmarks/results.md +0 -0
  28. {barflow-0.2.1 → barflow-0.2.2}/docs/DESIGN.md +0 -0
  29. {barflow-0.2.1 → barflow-0.2.2}/docs/FEATURE_REQUESTS.md +0 -0
  30. {barflow-0.2.1 → barflow-0.2.2}/docs/PUBLISHING.md +0 -0
  31. {barflow-0.2.1 → barflow-0.2.2}/examples/async_stream.py +0 -0
  32. {barflow-0.2.1 → barflow-0.2.2}/examples/parallel_presets.py +0 -0
  33. {barflow-0.2.1 → barflow-0.2.2}/examples/showcase.py +0 -0
  34. {barflow-0.2.1 → barflow-0.2.2}/examples/themes_showcase.py +0 -0
  35. {barflow-0.2.1 → barflow-0.2.2}/setup.cfg +0 -0
  36. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/_progress.py +0 -0
  37. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/aio.py +0 -0
  38. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/columns.py +0 -0
  39. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/hooks.py +0 -0
  40. {barflow-0.2.1 → barflow-0.2.2}/src/barflow/style.py +0 -0
  41. {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/dependency_links.txt +0 -0
  42. {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: barflow
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Fast Python progress bars with a C++ core. Windows-first.
5
5
  Author: NevermindNilas
6
6
  License-Expression: MIT
@@ -0,0 +1,187 @@
1
+ """Benchmark: total ms from zero to first iteration body.
2
+
3
+ Measures the *end-to-end* cost a user actually pays:
4
+ 1. import the library
5
+ 2. construct a progress bar around a range
6
+ 3. enter the first iteration
7
+
8
+ All three phases run inside a subprocess so the import is genuinely cold.
9
+ The subprocess prints a single perf_counter_ns timestamp; the driver
10
+ subtracts the interpreter-startup baseline to isolate library cost.
11
+
12
+ Usage:
13
+ python benchmarks/bench_import_to_iter.py
14
+ python benchmarks/bench_import_to_iter.py --runs 21
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import os
21
+ import statistics
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+
27
+ REPO = Path(__file__).resolve().parent.parent
28
+ SRC = REPO / "src"
29
+
30
+ # ---------- snippets ----------------------------------------------------------
31
+ # Each snippet must print exactly ONE integer: perf_counter_ns elapsed from
32
+ # the top of the script to the moment the first iteration body executes.
33
+
34
+ SNIPPETS: dict[str, str] = {
35
+ "rich": """\
36
+ import time, sys, os
37
+ _t0 = time.perf_counter_ns()
38
+ from rich.console import Console
39
+ from rich.progress import Progress
40
+ import io
41
+ _sink = io.StringIO()
42
+ _console = Console(file=_sink, force_terminal=True)
43
+ with Progress(console=_console, transient=True) as _p:
44
+ _task = _p.add_task("work", total=100)
45
+ for _i in range(100):
46
+ # === first iteration body reached ===
47
+ _elapsed = time.perf_counter_ns() - _t0
48
+ break
49
+ # print outside the context manager so rich doesn't capture it
50
+ os.write(1, (str(_elapsed) + "\\n").encode())
51
+ """,
52
+ "tqdm": """\
53
+ import time, os
54
+ _t0 = time.perf_counter_ns()
55
+ from tqdm import tqdm
56
+ import io
57
+ _sink = io.StringIO()
58
+ for _i in tqdm(range(100), file=_sink, total=100):
59
+ _elapsed = time.perf_counter_ns() - _t0
60
+ break
61
+ os.write(1, (str(_elapsed) + "\\n").encode())
62
+ """,
63
+ "barflow": """\
64
+ import time, sys, os
65
+ sys.path.insert(0, {src!r})
66
+ _t0 = time.perf_counter_ns()
67
+ # redirect fd 2 so the C renderer doesn't write to console
68
+ _devnull = os.open(os.devnull, os.O_WRONLY)
69
+ _saved = os.dup(2)
70
+ os.dup2(_devnull, 2)
71
+ from barflow import track
72
+ for _i in track(range(100), total=100):
73
+ _elapsed = time.perf_counter_ns() - _t0
74
+ break
75
+ os.dup2(_saved, 2)
76
+ os.close(_saved)
77
+ os.close(_devnull)
78
+ os.write(1, (str(_elapsed) + "\\n").encode())
79
+ """,
80
+ "alive-progress": """\
81
+ import time, os
82
+ _t0 = time.perf_counter_ns()
83
+ from alive_progress import alive_bar
84
+ import io
85
+ _sink = io.StringIO()
86
+ with alive_bar(100, file=_sink, force_tty=True) as _bar:
87
+ for _i in range(100):
88
+ _elapsed = time.perf_counter_ns() - _t0
89
+ break
90
+ os.write(1, (str(_elapsed) + "\\n").encode())
91
+ """,
92
+ }
93
+
94
+
95
+ # ---------- driver ------------------------------------------------------------
96
+
97
+ def _run_snippet(label: str, snippet: str, runs: int) -> list[int]:
98
+ """Return list of elapsed-ns values, one per successful run."""
99
+ env = os.environ.copy()
100
+ env["PYTHONPATH"] = str(SRC) + os.pathsep + env.get("PYTHONPATH", "")
101
+ env["PYTHONDONTWRITEBYTECODE"] = "1"
102
+
103
+ results: list[int] = []
104
+ for _ in range(runs):
105
+ proc = subprocess.run(
106
+ [sys.executable, "-c", snippet],
107
+ env=env,
108
+ stdout=subprocess.PIPE,
109
+ stderr=subprocess.DEVNULL,
110
+ text=True,
111
+ timeout=30,
112
+ )
113
+ if proc.returncode != 0:
114
+ continue
115
+ line = proc.stdout.strip().splitlines()[0] if proc.stdout.strip() else ""
116
+ try:
117
+ results.append(int(line))
118
+ except ValueError:
119
+ pass
120
+ return results
121
+
122
+
123
+ def _baseline(runs: int) -> float:
124
+ """Median wall-ns of a bare `python -c pass` — interpreter startup only."""
125
+ env = os.environ.copy()
126
+ env["PYTHONDONTWRITEBYTECODE"] = "1"
127
+ times: list[float] = []
128
+ for _ in range(runs):
129
+ t0 = time.perf_counter()
130
+ subprocess.run(
131
+ [sys.executable, "-c", "pass"],
132
+ env=env,
133
+ stdout=subprocess.DEVNULL,
134
+ stderr=subprocess.DEVNULL,
135
+ check=True,
136
+ )
137
+ times.append(time.perf_counter() - t0)
138
+ return statistics.median(times) * 1e9 # -> ns
139
+
140
+
141
+ def main() -> None:
142
+ ap = argparse.ArgumentParser(description="Import-to-first-iteration benchmark")
143
+ ap.add_argument("--runs", type=int, default=11,
144
+ help="trials per library (default 11)")
145
+ args = ap.parse_args()
146
+
147
+ print(f"=== Import -> show bar -> first iteration (runs={args.runs}) ===")
148
+ print(f" Python {sys.version}")
149
+ print()
150
+
151
+ # The snippet already measures elapsed time internally (perf_counter_ns),
152
+ # so it captures import + construction + first iter. No baseline subtraction
153
+ # needed — the timer starts AFTER the interpreter is already running.
154
+
155
+ header = f" {'library':<18s} {'median':>10s} {'min':>10s} {'p90':>10s} {'max':>10s}"
156
+ print(header)
157
+ print(" " + "-" * (len(header) - 2))
158
+
159
+ for label, snippet in SNIPPETS.items():
160
+ if label == "barflow":
161
+ snippet = snippet.format(src=str(SRC))
162
+ samples = _run_snippet(label, snippet, args.runs)
163
+ if not samples:
164
+ print(f" {label:<18s} {'SKIP (not installed or error)':>10s}")
165
+ continue
166
+ samples.sort()
167
+ median = statistics.median(samples)
168
+ mn = samples[0]
169
+ p90 = samples[int(round(0.90 * (len(samples) - 1)))]
170
+ mx = samples[-1]
171
+ print(
172
+ f" {label:<18s} "
173
+ f"{median / 1e6:>9.1f}ms "
174
+ f"{mn / 1e6:>9.1f}ms "
175
+ f"{p90 / 1e6:>9.1f}ms "
176
+ f"{mx / 1e6:>9.1f}ms"
177
+ )
178
+
179
+ print()
180
+ print("Timer starts AFTER interpreter boot (inside the subprocess).")
181
+ print("Measures: import lib + construct progress bar + reach first iteration body.")
182
+ print()
183
+ print("Lower is better.")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()
@@ -0,0 +1,271 @@
1
+ """Gallery — every preset rendering at once, side-by-side.
2
+
3
+ Inspired by alive-progress's `showtime`. Every selected preset gets
4
+ its own row; all rows redraw in the same animation frame so the
5
+ terminal looks like a wall of bars all racing simultaneously.
6
+
7
+ Each row pulls the preset's bar glyphs, color style, spinner frames,
8
+ and description style straight from `barflow.themes` so what you see
9
+ matches `barflow.Progress(theme=name)` exactly.
10
+
11
+ Run from any cwd:
12
+ python examples/gallery.py
13
+ python examples/gallery.py --section neon
14
+ python examples/gallery.py --section emoji --fps 30
15
+ python examples/gallery.py --only vaporwave hacker rocket_emoji
16
+ python examples/gallery.py --list
17
+ python examples/gallery.py --duration 8 --fps 24
18
+
19
+ Record as a GIF (requires `vhs` from charm.sh):
20
+ vhs examples/gallery.tape # → examples/gallery.gif
21
+
22
+ Tips:
23
+ - Resize your terminal tall enough to fit every row.
24
+ - 24-bit truecolor terminal recommended for the neon palette.
25
+ - Emoji presets need an emoji-capable font (Win Terminal, iTerm2,
26
+ modern gnome-terminal, kitty, alacritty + Noto Emoji, etc.).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import random
33
+ import sys
34
+ import time
35
+ from pathlib import Path
36
+
37
+ _REPO_SRC = Path(__file__).resolve().parent.parent / "src"
38
+ if _REPO_SRC.is_dir() and str(_REPO_SRC) not in sys.path:
39
+ sys.path.insert(0, str(_REPO_SRC))
40
+
41
+ # Force UTF-8 on Windows so emoji + box-drawing glyphs don't crash cp1252.
42
+ for _stream in (sys.stdout, sys.stderr):
43
+ try:
44
+ _stream.reconfigure(encoding="utf-8", errors="replace")
45
+ except (AttributeError, ValueError):
46
+ pass
47
+
48
+ from barflow import themes # noqa: E402
49
+ from barflow._core import ( # noqa: E402
50
+ COL_BAR, COL_SPINNER, COL_DESCRIPTION, COL_PERCENT, COL_TEXT,
51
+ )
52
+
53
+
54
+ RESET = "\x1b[0m"
55
+
56
+
57
+ # Curated lineups per section. Skip themes that need rate/eta columns to
58
+ # look right (classic, downloading, etc.) — gallery is bar-focused.
59
+ SECTIONS: dict[str, list[str]] = {
60
+ "neon": [
61
+ "vaporwave", "synthwave", "lightning", "plasma", "acid",
62
+ "midnight", "ember", "amber_crt", "miami", "gold_rush",
63
+ "alien", "deep_sea", "magma", "void", "chevron",
64
+ "rail_neon", "slash", "neon", "cyberpunk",
65
+ ],
66
+ "ascii": [
67
+ "hacker", "binary", "curly", "march", "wave", "rail_ascii",
68
+ "ascii", "equals", "brackets",
69
+ ],
70
+ "emoji": [
71
+ "fire_emoji", "rocket", "sakura", "storm",
72
+ "sparkle", "pacman", "heart_emoji", "moon", "weather",
73
+ ],
74
+ "brand": [
75
+ "github_dark", "discord", "dracula", "solarized", "nord", "gruvbox",
76
+ ],
77
+ "playful": [
78
+ "hearts", "stars", "arrows", "pipes", "shade",
79
+ "line", "double", "round", "matrix",
80
+ "fire", "ocean", "ice", "sunset", "forest",
81
+ ],
82
+ "all": [], # filled below
83
+ }
84
+ SECTIONS["all"] = (
85
+ SECTIONS["neon"] + SECTIONS["ascii"] + SECTIONS["emoji"]
86
+ + SECTIONS["brand"] + SECTIONS["playful"]
87
+ )
88
+
89
+
90
+ # ----- Column extraction --------------------------------------------------
91
+
92
+ def extract(cols):
93
+ """Pull bar glyphs, bar style, spinner frames + style, desc style.
94
+
95
+ Returns dict with keys: bar_width, bar_ansi, bar_glyphs (5-tuple),
96
+ spinner_frames (list[str]|None), spinner_ansi (str), desc_ansi (str),
97
+ prefix_text (str), prefix_ansi (str).
98
+ """
99
+ out = {
100
+ "bar_width": 30,
101
+ "bar_ansi": "",
102
+ "bar_glyphs": ("█", " ", [], "", ""),
103
+ "spinner_frames": None,
104
+ "spinner_ansi": "",
105
+ "desc_ansi": "",
106
+ "prefix_text": "",
107
+ "prefix_ansi": "",
108
+ }
109
+ for col in cols:
110
+ kind = col[0]
111
+ if kind == COL_BAR:
112
+ w = col[2]
113
+ out["bar_width"] = 30 if w is None or w < 0 else w
114
+ out["bar_ansi"] = col[4] or ""
115
+ out["bar_glyphs"] = col[5]
116
+ elif kind == COL_SPINNER:
117
+ out["spinner_frames"] = col[3]
118
+ out["spinner_ansi"] = col[4] or ""
119
+ elif kind == COL_DESCRIPTION:
120
+ out["desc_ansi"] = col[4] or ""
121
+ elif kind == COL_TEXT and not out["prefix_text"]:
122
+ out["prefix_text"] = col[1] or ""
123
+ out["prefix_ansi"] = col[4] or ""
124
+ return out
125
+
126
+
127
+ # ----- Per-row renderer ---------------------------------------------------
128
+
129
+ def build_bar(glyphs, width, fraction):
130
+ """Build one bar string from a 5-tuple glyphs spec at given fill fraction."""
131
+ fill, empty, partials, left, right = glyphs
132
+ cells = max(1, width)
133
+ levels = len(partials) + 1
134
+ total = cells * levels
135
+ filled = int(fraction * total + 0.5)
136
+ full_cells = filled // levels
137
+ partial_idx = filled % levels
138
+
139
+ body = fill * full_cells
140
+ remaining = cells - full_cells
141
+ if partial_idx > 0 and remaining > 0:
142
+ body += partials[partial_idx - 1]
143
+ remaining -= 1
144
+ body += empty * remaining
145
+ return f"{left}{body}{right}"
146
+
147
+
148
+ def render_row(name, parts, fraction, frame_tick, name_width):
149
+ """Render a single preset row at this frame."""
150
+ name_label = f"\x1b[1;97m{name:<{name_width}}{RESET}"
151
+
152
+ spinner = ""
153
+ frames = parts["spinner_frames"]
154
+ if frames:
155
+ glyph = frames[frame_tick % len(frames)]
156
+ ansi = parts["spinner_ansi"]
157
+ spinner = f"{ansi}{glyph}{RESET} " if ansi else f"{glyph} "
158
+
159
+ bar_str = build_bar(parts["bar_glyphs"], parts["bar_width"], fraction)
160
+ bar_part = f"{parts['bar_ansi']}{bar_str}{RESET}" if parts["bar_ansi"] else bar_str
161
+
162
+ pct = f"{int(fraction * 100):3d}%"
163
+ pct_color = "\x1b[1m" if fraction < 1.0 else "\x1b[1;92m"
164
+ pct_part = f"{pct_color}{pct}{RESET}"
165
+
166
+ return f"{spinner}{name_label} {bar_part} {pct_part}"
167
+
168
+
169
+ # ----- Showtime loop ------------------------------------------------------
170
+
171
+ def run_gallery(presets, *, duration=6.0, fps=24, seed=None):
172
+ if not presets:
173
+ print("nothing to show.", file=sys.stderr)
174
+ return
175
+
176
+ rng = random.Random(seed)
177
+ parts = {n: extract(themes.get(n)) for n in presets}
178
+ speeds = {n: rng.uniform(0.6, 1.6) for n in presets} # finish-time multiplier
179
+ fractions = {n: 0.0 for n in presets}
180
+ name_width = max(len(n) for n in presets)
181
+ n_rows = len(presets)
182
+ frame_delay = 1.0 / fps
183
+ total_frames = max(1, int(duration * fps))
184
+
185
+ out = sys.stdout
186
+ out.write("\x1b[?25l") # hide cursor
187
+ out.flush()
188
+
189
+ drew_once = False
190
+ t_start = time.perf_counter()
191
+ try:
192
+ for frame in range(total_frames + fps): # extra slack so all bars hit 100%
193
+ for n in presets:
194
+ step = speeds[n] / total_frames
195
+ fractions[n] = min(1.0, fractions[n] + step)
196
+
197
+ lines = [
198
+ render_row(n, parts[n], fractions[n], frame, name_width)
199
+ for n in presets
200
+ ]
201
+
202
+ if drew_once:
203
+ out.write(f"\x1b[{n_rows}A") # cursor up N rows
204
+ out.write("\r")
205
+ out.write("\n".join(f"\x1b[2K{l}" for l in lines))
206
+ out.write("\n")
207
+ out.flush()
208
+ drew_once = True
209
+
210
+ if all(f >= 1.0 for f in fractions.values()):
211
+ break
212
+ time.sleep(frame_delay)
213
+ finally:
214
+ out.write("\x1b[?25h") # show cursor
215
+ out.flush()
216
+
217
+ elapsed = time.perf_counter() - t_start
218
+ sys.stderr.write(
219
+ f"\n\x1b[1;92m{len(presets)} presets, {elapsed:.2f}s, "
220
+ f"{fps} fps target.\x1b[0m\n"
221
+ )
222
+
223
+
224
+ # ----- CLI ----------------------------------------------------------------
225
+
226
+ def main():
227
+ ap = argparse.ArgumentParser(
228
+ description=__doc__,
229
+ formatter_class=argparse.RawDescriptionHelpFormatter,
230
+ )
231
+ ap.add_argument("--section", choices=sorted(SECTIONS), default="all",
232
+ help="which lineup to show (default: all)")
233
+ ap.add_argument("--only", nargs="+", default=None,
234
+ help="explicit preset list (overrides --section)")
235
+ ap.add_argument("--duration", type=float, default=6.0,
236
+ help="approximate seconds for fastest bar to finish (default 6)")
237
+ ap.add_argument("--fps", type=int, default=24,
238
+ help="target frames per second (default 24)")
239
+ ap.add_argument("--seed", type=int, default=None,
240
+ help="seed for per-preset speed randomness")
241
+ ap.add_argument("--list", action="store_true",
242
+ help="list section lineups and exit")
243
+ args = ap.parse_args()
244
+
245
+ if args.list:
246
+ for sec, names in SECTIONS.items():
247
+ valid = [n for n in names if n in themes.THEMES]
248
+ print(f"\n[{sec}] ({len(valid)} presets)")
249
+ for n in valid:
250
+ print(f" {n}")
251
+ return
252
+
253
+ if args.only:
254
+ unknown = [n for n in args.only if n not in themes.THEMES]
255
+ if unknown:
256
+ print(f"unknown preset(s): {', '.join(unknown)}", file=sys.stderr)
257
+ sys.exit(2)
258
+ picks = args.only
259
+ else:
260
+ picks = [n for n in SECTIONS[args.section] if n in themes.THEMES]
261
+
262
+ sys.stderr.write(
263
+ f"\x1b[1;97mBarFlow gallery\x1b[0m — "
264
+ f"{len(picks)} presets, ~{args.duration:.1f}s, {args.fps} fps\n\n"
265
+ )
266
+
267
+ run_gallery(picks, duration=args.duration, fps=args.fps, seed=args.seed)
268
+
269
+
270
+ if __name__ == "__main__":
271
+ main()
@@ -0,0 +1,145 @@
1
+ """Preset & template gallery — neon, ASCII art, emoji, brand palettes.
2
+
3
+ Run from anywhere:
4
+ python examples/presets_gallery.py
5
+ python examples/presets_gallery.py --section neon
6
+ python examples/presets_gallery.py --only vaporwave hacker rocket_emoji
7
+ python examples/presets_gallery.py --list
8
+
9
+ Each preset runs a short animated demo. The header above each bar
10
+ prints the preset name in bright white so you can match what you like.
11
+
12
+ Sections:
13
+ neon — vaporwave, synthwave, lightning, plasma, acid, midnight, ...
14
+ ascii — hacker, binary, curly, march, wave, rail_ascii
15
+ emoji — rocket, sakura, storm, sparkle, pacman, heart, moon, ...
16
+ brand — github_dark, discord, dracula, solarized, nord, gruvbox
17
+ specialized — tiny, detailed, downloading, building, training
18
+ classic — original 27 themes (utilitarian, colorful, playful)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import sys
25
+ import time
26
+ from pathlib import Path
27
+
28
+ _REPO_SRC = Path(__file__).resolve().parent.parent / "src"
29
+ if _REPO_SRC.is_dir() and str(_REPO_SRC) not in sys.path:
30
+ sys.path.insert(0, str(_REPO_SRC))
31
+
32
+ import barflow
33
+ from barflow import themes
34
+
35
+
36
+ SECTIONS: dict[str, list[str]] = {
37
+ "neon": [
38
+ "vaporwave", "synthwave", "lightning", "plasma", "acid",
39
+ "midnight", "ember", "amber_crt", "miami", "gold_rush",
40
+ "alien", "deep_sea", "magma", "void", "chevron",
41
+ "rail_neon", "slash_neon",
42
+ ],
43
+ "ascii": [
44
+ "hacker", "binary", "curly", "march", "wave_ascii", "rail_ascii",
45
+ ],
46
+ "emoji": [
47
+ "fire_emoji", "rocket_emoji", "sakura_emoji", "storm_emoji",
48
+ "sparkle_emoji", "pacman_emoji", "heart_emoji", "moon_emoji",
49
+ "weather_emoji",
50
+ ],
51
+ "brand": [
52
+ "github_dark", "discord", "dracula", "solarized", "nord", "gruvbox",
53
+ ],
54
+ "specialized": [
55
+ "tiny", "detailed", "downloading", "building", "training",
56
+ ],
57
+ "classic": [
58
+ "classic", "minimal", "rich_like", "spinner", "mono", "ghost",
59
+ "ascii", "equals", "brackets",
60
+ "neon", "pastel", "retro", "matrix", "fire", "ocean", "ice",
61
+ "sunset", "forest", "cyberpunk",
62
+ "hearts", "stars", "arrows_march", "pipes", "shade_cool",
63
+ "line_clean", "double_frame", "round_retro",
64
+ ],
65
+ }
66
+
67
+
68
+ def hdr(name: str):
69
+ sys.stderr.write(
70
+ f"\n\x1b[1;97m{name}\x1b[0m "
71
+ f"\x1b[90m{'─' * max(2, 40 - len(name))}\x1b[0m\n"
72
+ )
73
+ sys.stderr.flush()
74
+
75
+
76
+ def section_hdr(name: str):
77
+ sys.stderr.write(
78
+ f"\n\x1b[1;95m━━━ {name.upper()} ━━━\x1b[0m\n"
79
+ )
80
+ sys.stderr.flush()
81
+
82
+
83
+ def run_preset(name: str, *, n: int = 100, delay: float = 0.012,
84
+ desc: str | None = None):
85
+ hdr(name)
86
+ cols = themes.get(name)
87
+ with barflow.Progress(*cols, total=n, desc=desc or "demo") as p:
88
+ for _ in range(n):
89
+ p.tick()
90
+ time.sleep(delay)
91
+
92
+
93
+ def main():
94
+ ap = argparse.ArgumentParser(description=__doc__,
95
+ formatter_class=argparse.RawDescriptionHelpFormatter)
96
+ ap.add_argument("--section", choices=sorted(SECTIONS), default=None,
97
+ help="show only one section")
98
+ ap.add_argument("--only", nargs="+", default=None,
99
+ help="subset of preset names to show")
100
+ ap.add_argument("--list", action="store_true",
101
+ help="print every preset name (grouped by section) and exit")
102
+ ap.add_argument("--n", type=int, default=100,
103
+ help="iterations per preset (default 100)")
104
+ ap.add_argument("--delay", type=float, default=0.012,
105
+ help="sleep per iter in seconds (default 0.012)")
106
+ args = ap.parse_args()
107
+
108
+ if args.list:
109
+ for sec, names in SECTIONS.items():
110
+ print(f"\n[{sec}]")
111
+ for n in names:
112
+ if n in themes.THEMES:
113
+ print(f" {n}")
114
+ return
115
+
116
+ if args.only:
117
+ unknown = [n for n in args.only if n not in themes.THEMES]
118
+ if unknown:
119
+ print(f"Unknown preset(s): {', '.join(unknown)}", file=sys.stderr)
120
+ print(f"Run with --list to see all names.", file=sys.stderr)
121
+ sys.exit(2)
122
+ for name in args.only:
123
+ run_preset(name, n=args.n, delay=args.delay)
124
+ return
125
+
126
+ sections = [args.section] if args.section else list(SECTIONS)
127
+ total = sum(len(SECTIONS[s]) for s in sections)
128
+ sys.stderr.write(
129
+ f"\x1b[1;97mBarFlow preset gallery\x1b[0m — "
130
+ f"{total} presets across {len(sections)} sections, "
131
+ f"~{args.n * args.delay:.1f}s each\n"
132
+ )
133
+
134
+ for sec in sections:
135
+ section_hdr(sec)
136
+ for name in SECTIONS[sec]:
137
+ if name not in themes.THEMES:
138
+ continue
139
+ run_preset(name, n=args.n, delay=args.delay, desc=name)
140
+
141
+ sys.stderr.write("\n\x1b[1;92mdone.\x1b[0m\n")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "barflow"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Fast Python progress bars with a C++ core. Windows-first."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"