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::Rich, }); 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.)Instead of using
|
operator, you can also callComposite()
with the components explicitly, which also accepts an additional string argument as the delimiter between the components. See the example below. -
If your display is multiline (has
\n
appear in it), all lines are automatically rerendered during animations. The example below combines three bars similarly to the example above, however uses\n
as the delimiter:std::atomic<size_t> linear{0}, quad{0}, cubic{0}; auto bars = bk::Composite( {bk::ProgressBar(&linear, { .total = 100, .message = "Linear ", .speed = 0, .style = bk::Rich, .show = false, }), bk::ProgressBar(&quad, { .total = 5050, .message = "Quadratic", .speed = 0, .style = bk::Rich, .show = false, }), bk::ProgressBar(&cubic, { .total = 171700, .message = "Cubic ", .speed = 0, .style = bk::Rich, .show = false, })}, "\n"); bars->show(); for (int i = 0; i < 100; i++) { std::this_thread::sleep_for(130ms); linear++; quad += linear; cubic += quad; } bars->done();
-
Display status messages:
auto s = bk::Status({.message = "Working"}); std::this_thread::sleep_for(2.5s); s->message("Still working"); std::this_thread::sleep_for(2.5s); s->message("Almost done"); std::this_thread::sleep_for(2.5s); s->message("Done"); s->done();
Unlike other displays,
Status
display does not monitor a string variable but instead expects it as an argument tomessage()
calls. This is because a string is too big of an object to have unguarded concurrent access (see this section). -
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.
Usually when you get to a point where you think you might want a waiting animation, you probably already have some variables you are monitoring and maybe even occasionally printing to screen. Displaying an animation comes as an afterthought.
barkeep strives to be minimally intrusive by monitoring existing variables using pointers, so that in such situations you can start using it with very little code change.
Before #include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
// tokenize by space
std::vector<std::string> tknz(std::string s) {
std::vector<std::string> rval;
std::istringstream iss(s);
for (std::string word; iss >> word;) {
rval.push_back(word);
}
return rval;
}
void process_document(const std::string& doc,
std::ofstream& out,
size_t& total_chars,
size_t& total_tokens) {
auto tokens = tknz(doc);
for (auto& token : tokens) {
out << token << std::endl;
total_chars += token.size();
total_tokens++;
}
out << std::endl;
}
int main(int /*argc*/, char** /*argv*/) {
std::vector<std::string> docs = {/*...*/};
std::ofstream out("tokens.txt");
size_t chars = 0, tokens = 0;
for (size_t i = 0; i < docs.size(); ++i) {
std::cout << "Doc " << i << std::endl;
process_document(docs[i], out,
chars, tokens);
}
std::cout << "Total: " << chars
<< tokens << std::endl;
return 0;
} |
After #include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <barkeep/barkeep.h>
namespace bk = barkeep;
// tokenize by space
std::vector<std::string> tknz(std::string s) {
std::vector<std::string> rval;
std::istringstream iss(s);
for (std::string word; iss >> word;) {
rval.push_back(word);
}
return rval;
}
void process_document(const std::string& doc,
std::ofstream& out,
size_t& total_chars,
size_t& total_tokens) {
auto tokens = tknz(doc);
for (auto& token : tokens) {
out << token << std::endl;
total_chars += token.size();
total_tokens++;
}
out << std::endl;
}
int main(int /*argc*/, char** /*argv*/) {
std::vector<std::string> docs = {/*...*/};
std::ofstream out("tokens.txt");
size_t chars = 0, tokens = 0, i = 0;
auto bar = bk::ProgressBar(&i, {.total=docs.size(), .show=false}) |
bk::Counter(&tokens, {.message="Tokens", .show=false}) |
bk::Counter(&chars, {.message="Chars", .show=false});
bar->show();
for (i = 0; i < docs.size(); ++i) {
process_document(docs[i], out,
chars, tokens);
}
bar->done();
std::cout << "Total: " << chars
<< tokens << std::endl;
return 0;
} |
In the example above, we add a display to monitor the loop variable i
, total_chars
, and total_tokens
.
For-loop changes slightly (because i
needs to be declared earlier), but the way in which these variables are used in code stays the same.
For instance, we do not use a custom data structure to call operator++()
to increment progress.
As a result, signature of process_document()
does not change.
We start and stop the display and barkeep is out of the way.
Since displaying thread typically works concurrently, reads of progress variables (i
, total_chars
, total_tokens
) is always racing with your own modifications.
Even though theoretically it is possible that a read can interleave a write in the middle such that you read e.g. a 4 byte float where 2 byte of is fresh and 2 byte is stale, this kind of concurrent access seems to be almost always okay in practice (see, e.g. this, and this thread).
It has always been okay in my own anecdotal experience.
If not, a race condition would result in momentarily displaying a garbage value.
Given the practical rarity of encountering this, its minimal impact outcome, and the desire to be as non-intrusive as possible, barkeep does not introduce any lock guards (which would require a custom type as the progress variables instead of, e.g. an int
or float
).
If you still want to be extra safe and guarantee non-racing read and writes, you can use std::atomic<T>
for your progress variables, as can be seen in some of the examples above.
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