Skip to content

oir/barkeep

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

97 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

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

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