Small, single C++ header to display async animations, counters, and progress bars.
Use it by including barkeep.h
in your project.
barkeep strives to be non-intrusive.
barkeep also has python bindings.
-
Display a waiting animation with a message:
using namespace std::chrono_literals; namespace bk = barkeep; auto anim = bk::Animation({.message = "Working"}); /* do work */ std::this_thread::sleep_for(10s); anim->done();
-
Supports several styles:
auto anim = bk::Animation({.message = "Downloading...", .style = bk::Earth});
-
Display a counter to monitor a numeric variable while waiting:
int work{0}; auto c = bk::Counter(&work, { .message = "Reading lines", .speed = 1., .speed_unit = "line/s" }); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); // read & process line work++; } c->done();
-
Display a progress bar to monitor a numeric variable and measure its completion by comparing against a total:
int work{0}; auto bar = bk::ProgressBar(&work, { .total = 505, .message = "Reading lines", .speed = 1., .speed_unit = "line/s", }); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); // read & process line work++; } bar->done();
-
Bars can also be styled. Some styles have color:
int work{0}; auto bar = bk::ProgressBar(&work, { .total = 505, .message = "Reading lines", .speed = 1., .speed_unit = "line/s", .style = bk::ProgressBarStyle::Pip, }); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); // read & process line work++; } bar->done();
-
Displaying can be deferred with
.show = false
, and explicitly invoked by callingshow()
, instead of at construction time.Finishing the display can be done implicitly by the destructor, instead of calling
done()
(this allows RAII-style use).The following are equivalent:
int work{0}; auto bar = bk::ProgressBar(&work, {.total = 505}); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); work++; } bar->done();
int work; auto bar = bk::ProgressBar(&work, {.total = 505, .show = false}); work = 0; bar->show(); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); work++; } bar->done();
int work{0}; { auto bar = bk::ProgressBar(&work, {.total = 505}); for (int i = 0; i < 505; i++) { std::this_thread::sleep_for(13ms); work++; } }
-
Automatically iterate over a container with a progress bar display (instead of monitoring an explicit progress variable):
std::vector<float> v(300, 0); std::iota(v.begin(), v.end(), 1); // 1, 2, 3, ..., 300 float sum = 0; for (auto x : bk::IterableBar(v, {.message = "Summing", .interval = .02})) { std::this_thread::sleep_for(1.s/x); sum += x; } std::cout << "Sum: " << sum << std::endl;
Detail: IterableBar starts the display not at the time of construction, ...
... but at the time of the first call to
begin()
. Thus, it is possible to set it up prior to loop execution.Similarly, it ends the display not at the time of destruction, but at the first increment of the iterator past the end. Thus, even if the object stays alive after the loop, the display will be stopped.
Therefore, you could initialize it earlier than the loop execution, and destroy it late afterwards:
std::vector<float> v(300, 0); std::iota(v.begin(), v.end(), 1); // 1, 2, 3, ..., 300 float sum = 0; bk::IterableBar bar(v, {.message = "Summing", .interval = .02}); // <-- At this point, display is not yet shown. // Thus, more work can be done here. for (auto x : bar) { // <-- Display starts showing. std::this_thread::sleep_for(1.s/x); sum += x; } // <-- Display stops here even if `bar` object is still alive. // Thus, more work can be done here. std::cout << "Sum: " << sum << std::endl;
-
Combine diplays using
|
operator to monitor multiple variables:std::atomic<size_t> sents{0}, toks{0}; auto bar = bk::ProgressBar(&sents, { .total = 1010, .message = "Sents", .show = false}) | bk::Counter(&toks, { .message = "Toks", .speed = 1., .speed_unit = "tok/s", .show = false}); bar->show(); for (int i = 0; i < 1010; i++) { // do work std::this_thread::sleep_for(13ms); sents++; toks += (1 + rand() % 5); } bar->done();
(Observe the non-running initialization of components using
.show = false
, which is needed for composition.) -
Use "no tty" mode to, e.g., output to log files:
std::atomic<size_t> sents{0}; auto bar = bk::ProgressBar(&sents, { .total = 401, .message = "Sents", .speed = 1., .interval = 1., .no_tty = true, }); for (int i = 0; i < 401; i++) { std::this_thread::sleep_for(13ms); sents++; } bar->done();
no_tty
achieves two things:- Change the delimiter from
\r
to\n
to avoid wonky looking output in your log files. - Change the default interval to a minute to avoid overwhelming logs (in the example above, we set the interval ourselves explicitly).
- Change the delimiter from
See demo.cpp
for more examples.
You can enable advanced formatting by either
- defining the
BARKEEP_ENABLE_FMT_FORMAT
compile-time flag, at the expense of introducing a dependency tofmt
(which has an optional header-only mode), or - defining the
BARKEEP_ENABLE_STD_FORMAT
flag, which uses the standardstd::format
from<format>
, which might require a more recent compiler version (e.g. gcc >= 13.1) despite not introducing external dependencies.
Unlike fmt::format
, std::format
does not support named arguments, which is a limitation you might consider.
Thus, std::format
requires to use integer identifiers to refer to bar components as you will see below.
In either of these cases, Counter
s and ProgressBar
s have an additional Config
option "format
".
This option can be used to format the entire display using a fmt
-like format string instead of using textual options like message
or speed_unit
:
-
A counter:
-
with
fmt
enabled:size_t work{0}; auto c = bk::Counter(&work, { .format = "Picked up {value} flowers, at {speed:.1f} flo/s", .speed = 0.1 }); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(13ms), work++; } c->done();
-
with standard
<format>
enabled:size_t work{0}; auto c = bk::Counter(&work, { .format = "Picked up {0} flowers, at {1:.1f} flo/s", .speed = 0.1 }); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(13ms), work++; } c->done();
-
-
A bar:
-
with
fmt
enabled:size_t work{0}; auto bar = bk::ProgressBar(&work, { .total = 1010, .format = "Picking flowers {value:4d}/{total} {bar} ({speed:.1f} flo/s)", .speed = 0.1 }); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(9ms), work++; } bar->done();
-
with standard
<format>
enabled:size_t work{0}; auto bar = bk::ProgressBar(&work, { .total = 1010, .format = "Picking flowers {0:4d}/{3} {1} ({4:.1f} flo/s)", .speed = 0.1 }); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(9ms), work++; } bar->done();
-
When format
is used, other textual parameters, such as message
or speed_unit
are ignored.
- For counters, you can use the predefined identifiers
{value}
({0}
), and{speed}
({1}
) withfmt
(<format>
). - With bars, you can use
{value}
({0}
),{bar}
({1}
),{percent}
({2}
),{total}
({3}
), and{speed}
({4}
) withfmt
(<format>
).
Additionally, some basic ansi color sequences are predefined as identifiers which could be used to add color:
-
with
fmt
enabled:std::atomic<size_t> work{0}; auto bar = bk::ProgressBar(&work, { .total = 1010, .format = "Picking flowers {blue}{value:4d}/{total} {green}{bar} " "{yellow}{percent:3.0f}%{reset} ({speed:.1f} flo/s)", .speed = 0.1}); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(9ms), work++; } bar->done();
-
with standard
<format>
enabled:std::atomic<size_t> work{0}; auto bar = bk::ProgressBar(&work, { .total = 1010, .format = "Picking flowers {8}{0:4d}/{3} {6}{1} " "{7}{2:3.0f}%{11} ({4:.1f} flo/s)", .speed = 0.1}); for (int i = 0; i < 1010; i++) { std::this_thread::sleep_for(9ms), work++; } bar->done();
-
You can use
{red}
,{green}
,{yellow}
,{blue}
,{magenta}
,{cyan}
, and{reset}
withfmt
. -
With the standard
<format>
you can use the following, based on whether you are specifying aCounter
or aProgressBar
:red green yellow blue magenta cyan reset Counter
{2}
{3}
{4}
{5}
{6}
{7}
{8}
ProgressBar
{5}
{6}
{7}
{8}
{9}
{10}
{11}
See demo-fmtlib.cpp
or demo-stdfmt.cpp
for more examples.
- Progress variables (and
total
for progress bar) can be floating point types too. They can also be negative and/or decreasing (careful with the numeric type to avoid underflows). - Note that progress variable is taken by pointer, which means it needs to outlive the display.
- Display runs on a concurrent, separate thread, doing concurrent reads on your progress variable. See this section for what that might imply.
- The examples above use C++20's designated initializers.
If you prefer to use an older C++ version, you can simply initialize the config classes (e.g.
ProgressBarConfig
) the regular way to pass options into display classes (e.g.ProgressBar
).
barkeep is header only, so you can simply include the header in your C++ project. Still, this section details how to build the demos, tests and python bindings and can be used for reference.
If you don't want to deal with even a Makefile, you can simply invoke the compiler on the corresponding .cpp
files.
- First clone with submodules:
Or if you already cloned without the
git clone --recursive https://github.com/oir/barkeep cd barkeep
recursive
option, you can init the submodules:git clone https://github.com/oir/barkeep cd barkeep git submodule update --init
- Then, build & run the demo like:
(You can replace
g++ -std=c++20 -I./ tests/demo.cpp -o demo.out ./demo.out
g++
with your choice of compiler likeclang
.) - Or, build the tests like:
g++ -std=c++20 -I./ -I./subprojects/Catch2_/single_include/ tests/test.cpp -o test.out g++ -std=c++20 -I./ -I./subprojects/Catch2_/single_include/ tests/test-stdfmt.cpp -o test-stdfmt.out g++ -std=c++20 -I./ -I./subprojects/Catch2_/single_include/ -I./subprojects/fmt_/include/ tests/test-fmtlib.cpp -o test-fmtlib.out ./test.out ./test-stdfmt.out ./test-fmtlib.out
Detail: Github submodules are staged in folders that end with a
_
to avoid clashing with Meson's subproject downloading.
Python bindings are slightly more involved, therefore a proper build system is recommended, see below.
If you don't want to deal with a complex build system, but also don't want to invoke raw compiler commands, you can use make
.
Clone the repo with submodules as in the previous section and cd
into it.
Build demo and tests:
make all
...and run:
./demo.out
./test.out
./test-stdfmt.out
./test-fmtlib.out
Python bindings are slightly more involved, therefore a proper build system is recommended, see below.
Meson has its own subproject staging logic, thus cloning the submodules is not needed.
-
pip install meson sudo apt install ninja-build # could be a different cmd for your OS
-
Configure (from the root repo directory):
meson setup build
-
Then the target
tests
can be used to build all demos and tests:meson compile -C build tests ./build/tests/test.out ./build/tests/test-stdfmt.out ./build/tests/test-fmtlib.out ./build/tests/demo.out ./build/tests/demo-stdfmt.out ./build/tests/demo-fmtlib.out
-
If you have python dev dependencies available, all python binding targets are collected under the
python
target. The output ofconfigure
command will list those, e.g.:Message: Python targets: Message: barkeep.cpython-39-darwin Message: barkeep.cpython-310-darwin Message: barkeep.cpython-311-darwin Message: barkeep.cpython-312-darwin
meson compile -C build python
Then you can run python tests or demos, e.g.:
PYTHONPATH=build/python/ python3.11 -m pytest -s python/tests/test.py PYTHONPATH=build/python/ python3.11 python/tests/demo.py
By default, python bindings assume
std::atomic<double>
support. This requires availability of supporting compilers, e.g. g++-13 instead of Clang 15.0.0. Such compilers can be specified duringconfigure
step:CXX=g++-13 meson setup build
Alternatively, you can disable atomic float support by providing the appropriate compile flag if you don't have a supporting compiler:
CXXFLAGS="-DBARKEEP_ENABLE_ATOMIC_FLOAT=0" meson setup build