Skip to content

oir/barkeep

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Build status Coverage status c++20
Build status pypi

  • 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 calling show(), 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 call Composite() 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 to message() 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).

See demo.cpp for more examples.

Non intrusive design

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.

Caveat

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.

Advanced formatting

You can enable advanced formatting by either

  • defining the BARKEEP_ENABLE_FMT_FORMAT compile-time flag, at the expense of introducing a dependency to fmt (which has an optional header-only mode), or
  • defining the BARKEEP_ENABLE_STD_FORMAT flag, which uses the standard std::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, Counters and ProgressBars 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}) with fmt (<format>).
  • With bars, you can use {value} ({0}), {bar} ({1}), {percent} ({2}), {total} ({3}), and {speed} ({4}) with fmt (<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} with fmt.

  • With the standard <format> you can use the following, based on whether you are specifying a Counter or a ProgressBar:

    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.

Notes

  • 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).

Building

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.

No tooling

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:
    git clone --recursive https://github.com/oir/barkeep
    cd barkeep
    Or if you already cloned without the 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:
    g++ -std=c++20 -I./ tests/demo.cpp -o demo.out
    ./demo.out
    (You can replace g++ with your choice of compiler like clang.)
  • 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.

Minimal tooling: Make

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.

Build system: Meson

Meson has its own subproject staging logic, thus cloning the submodules is not needed.

  • Get Meson and ninja, e.g.:

    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 of configure 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 during configure 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

Similar projects