Crate nutmeg

source ·
Expand description

Nutmeg draws multi-line terminal progress bars to an ANSI terminal.

By contrast to other Rust progress-bar libraries, Nutmeg has no built-in concept of what the progress bar or indicator should look like: the application has complete control.

Concept

Nutmeg has three key types: Model, View, and Options.

Model

A type implementing the Model trait holds whatever information is needed to draw the progress bars. This might be the start time of the operation, the number of things processed, the amount of data transmitted or received, the currently active tasks, whatever…

The Model can be any of these things, from simplest to most powerful:

  1. Any type that implements std::fmt::Display, such as a String or integer.
  2. One of the provided models.
  3. An application-defined struct (or enum or other type) that implements Model.

The model is responsible for rendering itself into a String, optionally with ANSI styling, by implementing Model::render (or std::fmt::Display). Applications might choose to use any of the Rust crates that can render ANSI control codes into a string, such as yansi.

The application is responsible for deciding whether or not to color its output, for example by consulting $CLICOLORS or its own command line.

Models can optionally provide a “final message” by implementing Model::final_message, which will be left on the screen when the view is finished.

If one overall operation represents several concurrent operations then the application can, for example, represent them in a collection within the Model, and render them into multiple lines, or multiple sections in a single line. (See examples/multithreaded.rs.)

View

To get the model on to the terminal the application must create a View, typically with View::new, passing the initial model. The view takes ownership of the model.

The application then updates the model state via View::update, which may decide to paint the view to the terminal, subject to rate-limiting and other constraints.

The view has an internal mutex and is Send and Sync, so it can be shared freely across threads.

The view automatically erases itself from the screen when it is dropped.

While the view is on the screen, the application can print messages interleaved with the progress bar by either calling View::message, or treating it as a std::io::Write destination, for example for std::writeln.

Errors in writing to the terminal cause a panic.

Options

A small Options type, passed to the View constructor, allows turning progress bars off, setting rate limits, etc.

In particular applications might choose to construct all Options from a single function that respects an application-level option for whether progress bars should be drawn.

Utility functions

This crate also provides a few free functions such as estimate_remaining, that can be helpful in implementing Model::render.

Example

use std::io::Write; // to support write!()

// 1. Define a struct holding all the application state necessary to
// render the progress bar.
#[derive(Default)]
struct Model {
    i: usize,
    total: usize,
    last_file_name: String,
}

// 2. Define how to render the progress bar as a String.
impl nutmeg::Model for Model {
    fn render(&mut self, _width: usize) -> String {
        format!("{}/{}: {}", self.i, self.total, self.last_file_name)
    }
}

fn main() -> std::io::Result<()> {
    // 3. Create a View when you want to draw a progress bar.
    let mut view = nutmeg::View::new(Model::default(),
        nutmeg::Options::default());

    // 4. As the application runs, update the model via the view.
    let total_work = 100;
    view.update(|model| model.total = total_work);
    for i in 0..total_work {
        view.update(|model| {
            model.i += 1;
            model.last_file_name = format!("file{}.txt", i);
        });
        // 5. Interleave text output lines by writing messages to the view.
        if i % 10 == 3 {
            view.message(format!("reached {}", i));
        }
    }

    // 5. The bar is automatically erased when dropped.
    Ok(())
}

See the examples/ directory for more.

Performance

Nutmeg’s goal is that View::update is cheap enough that applications can call it fairly freely when there are small updates. The library takes care of rate-limiting updates to the terminal, as configured in the Options.

Each call to View::update will take a parking_lot mutex and check the system time, in addition to running the callback and some function-call overhead.

The model is only rendered to a string, and the string printed to a terminal, if sufficient time has passed since it was last painted.

The examples/bench.rs sends updates as fast as possible to a model containing a single u64, from a single thread. As of 2022-03-22, on a 2019 Core i9 Macbook Pro, it takes about 500ms to send 10e6 updates, or 50ns/update.

Integration with tracing

Nutmeg can be used to draw progress bars in a terminal interleaved with tracing messages. The progress bar is automatically temporarily removed to show messages, and repainted after the next update, subject to rate limiting and the holdoff time configured in Options.

Arc<View<M>> implicitly implements tracing_subscriber::fmt::writer::MakeWriter and so can be passed to tracing_subscriber::fmt::layer().with_writer().

For example:

    use std::sync::Arc;
    use tracing::Level;
    use tracing_subscriber::prelude::*;

    struct Model { count: usize }
    impl nutmeg::Model for Model {
         fn render(&mut self, _width: usize) -> String { todo!() }
    }

    let model = Model {
        count: 0,
    };
    let view = Arc::new(nutmeg::View::new(model, nutmeg::Options::new()));
    let layer = tracing_subscriber::fmt::layer()
        .with_ansi(true)
        .with_writer(Arc::clone(&view))
        .with_target(false)
        .with_timer(tracing_subscriber::fmt::time::uptime())
        .with_filter(tracing_subscriber::filter::LevelFilter::from_level(
            Level::INFO,
        ));
    tracing_subscriber::registry().with(layer).init();

    for i in 0..10 {
        if i % 10 == 0 {
            tracing::info!(i, "cats adored");
        }
        view.update(|m| m.count += 1);
        std::thread::sleep(std::time::Duration::from_millis(100));
    }

See examples/tracing for a runnable example.

Project status

Nutmeg is a young library. Although the API will not break gratuitously, it may evolve in response to experience and feedback in every pre-1.0 release.

If the core ideas prove useful and the API remains stable for an extended period then the author intends to promote it to 1.0, after which the API will respect Rust stability conventions.

Changes are described in the changelog in the top-level Rustdoc, below.

Constructive feedback on integrations that work well, or that don’t work well, is welcome.

Potential future features

  • Draw updates from a background thread, so that it will keep ticking even if not actively updated, and to better handle applications that send a burst of updates followed by a long pause. The background thread will eventually paint the last drawn update.

  • Also set the window title from the progress model, perhaps by a different render function?

Modules

Structs

  • Options controlling a View.
  • A view that draws and coordinates a progress bar on the terminal.

Enums

Traits

  • An application-defined type that holds whatever state is relevant to the progress bar, and that can render it into one or more lines of text.

Functions

  • Estimate by linear extrapolation the time remaining for a task with a given start time, number of completed items and number of total items.
  • Return a string representation of the percentage of work completed.