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.
- {barflow-0.2.1/src/barflow.egg-info → barflow-0.2.2}/PKG-INFO +1 -1
- barflow-0.2.2/benchmarks/bench_import_to_iter.py +187 -0
- barflow-0.2.2/examples/gallery.py +271 -0
- barflow-0.2.2/examples/presets_gallery.py +145 -0
- {barflow-0.2.1 → barflow-0.2.2}/pyproject.toml +1 -1
- {barflow-0.2.1 → barflow-0.2.2}/setup.py +0 -50
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/__init__.py +1 -1
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/_core.cpp +94 -72
- barflow-0.2.2/src/barflow/bar_styles.py +450 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/spinners.py +53 -0
- barflow-0.2.2/src/barflow/themes.py +925 -0
- {barflow-0.2.1 → barflow-0.2.2/src/barflow.egg-info}/PKG-INFO +1 -1
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/SOURCES.txt +3 -1
- barflow-0.2.1/benchmarks/pgo_train.py +0 -102
- barflow-0.2.1/src/barflow/bar_styles.py +0 -203
- barflow-0.2.1/src/barflow/themes.py +0 -386
- {barflow-0.2.1 → barflow-0.2.2}/LICENSE +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/MANIFEST.in +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/README.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_first_frame.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_memory.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_metadata_churn.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_multibar.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_raw.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/bench_tail_latency.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/benchmarks/results.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/docs/DESIGN.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/docs/FEATURE_REQUESTS.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/docs/PUBLISHING.md +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/examples/async_stream.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/examples/parallel_presets.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/examples/showcase.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/examples/themes_showcase.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/setup.cfg +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/_progress.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/aio.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/columns.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/hooks.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow/style.py +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/dependency_links.txt +0 -0
- {barflow-0.2.1 → barflow-0.2.2}/src/barflow.egg-info/top_level.txt +0 -0
|
@@ -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()
|