how-u-doin: A progress reporting abstraction
A common behaviour when building a CLI app is progress reporting.
I have come across this requirement many times, usually reaching for indicatif
or
tui
. These libraries work well with displaying progress, yet lately I have needed to
decouple the reporting of the progress from the display.
The requirement arose from two scenarios;
- A long-running CLI app was being invoked from a server, and
- A long-running algorithm was being run on another thread.
With the second scenario, a progress reporting structure was being passed through to the function. I ended up generalising the reporting methods into a trait so the display of the progress could be done by various implementations. The abstraction did not transfer well to solving scenario #1. It resulted in defining a bunch of serialisable structures which would be serialised and printed to stdout. At this point I refactored the system, looking to build an abstraction not over the consumption, but the producer of progress.
So how-u-doin
was born.
how-u-doin
is a progress reporting abstraction in a similar fashion as the log
crate. It provides an unobtrusive interface for producing progress reports, which is
decoupled from the consumption and display of the progress. It works by using a global
static transmitter and a consumer loop. If there is no consumer loop initialised, progress
reports are cheap void operations. Here's an example of producing progress:
// start a new report
let rpt = howudoin::new().label("Progress").set_len(1000);
for _ in 0..1000 {
rpt.inc(); // increment the position
// check for cancellation flag
if rpt.cancelled() {
break;
}
}
// finish the report
rpt.finish();
Notice that there is no reference to the consumer of the reports. For use of the
progress reports, an initialisation must occur somewhere in the program. This
initialisation links the transmitter and receiver and starts a consumption loop.
Importantly, when a consumer loop is running, all progress reports are stored in a structure
and can be queried statically. The design of the consumer loop uses the channel pattern
I have previously discussed. There are a few predefined consumers, for example a consumer
loop can be initialised with the TermLine
consumer
howudoin::init(howudoin::consumers::TermLine::default());
Running cargo run --all-features --example term-line
gives an example of the TermLine
consumer:
So how does how-u-doin
solve the aforementioned use cases?
The abstraction of the progress reporting makes it agnostic to the consumer, and since it
uses a static transmitter, it does not need to propagate around a structure. Reports can
be made and used locally.
For scenario #2, this helped greatly to avoid muddying the function signatures and fighting
the borrow checker, spawning a function on another thread.
Consumers get to choose the display mechanism. My CLI apps commonly output some sort of
progress by default, but for scenario #1 I created a JsonPrinter
which serialises the
progress structure. how-u-doin
defines this structure publicly, so there is no need to
create structures just to serialise them.
how-u-doin
also supports fetching the progress structure statically. This feature was
added to support requesting patterns, such as a REST API.
how-u-doin
is published on crates.io.