miniextendr provides an adapter that routes indicatif progress bar output through Rโ€™s console, so progress bars render correctly in R terminals, RStudio, and other R frontends.

Feature flag: indicatif (implies nonapi)

[dependencies]
miniextendr-api = { version = "0.1", features = ["indicatif"] }

๐Ÿ”—Table of Contents

๐Ÿ”—Why a Custom Terminal Adapter

Rโ€™s console output is not stdout or stderr. R uses its own output functions (Rprintf, REprintf, or the ptr_R_WriteConsoleEx hook) to route text to the active frontend (terminal, RStudio, Jupyter, etc.). Writing directly to stdout/stderr would bypass R entirely โ€“ output might not appear in RStudioโ€™s console, or could interleave incorrectly with Rโ€™s own output.

indicatifโ€™s ProgressBar needs a TermLike implementation to know how to write text, move the cursor, and clear lines. RTerm implements TermLike by routing all output through Rโ€™s console hooks, making indicatif work seamlessly inside R.

๐Ÿ”—Quick Start

use miniextendr_api::progress::term_like_stderr;
use indicatif::{ProgressBar, ProgressStyle};
use miniextendr_api::miniextendr;

#[miniextendr]
pub fn process_data(x: &[f64]) -> Vec<f64> {
    let pb = ProgressBar::with_draw_target(Some(x.len() as u64), term_like_stderr(80));
    pb.set_style(
        ProgressStyle::with_template("{bar:40} {pos}/{len} [{elapsed_precise}]").unwrap()
    );

    let result: Vec<f64> = x.iter().map(|&val| {
        pb.inc(1);
        val.sqrt()
    }).collect();

    pb.finish_with_message("done");
    result
}

From R:

result <- process_data(runif(100000))
#> ======================================== 100000/100000 [00:00:01]

๐Ÿ”—RTerm

RTerm is the TermLike implementation. It takes an R connection and a fixed column width: RStdout or RStderr for Rโ€™s console, or any other connection object passed in from R.

use miniextendr_api::connection::RStderr;
use miniextendr_api::progress::RTerm;
use indicatif::ProgressDrawTarget;

let term = RTerm::new(RStderr, 80);
let target = ProgressDrawTarget::term_like(Box::new(term));

Use stderr() for progress bars (the conventional choice), so they donโ€™t interfere with data piped to stdout. The width parameter controls how indicatif formats the progress bar. Use 80 as a safe default. If you need dynamic width detection, query getOption("width") from R and pass it in.

๐Ÿ”—Output Routing

Rโ€™s terminal connections (stdout(), stderr()) go through Rโ€™s console:

  1. Primary: ptr_R_WriteConsoleEx โ€“ the hook used by R frontends (RStudio, Jupyter, etc.)
  2. Fallback: Rprintf / REprintf โ€“ standard R output functions (used when the hook is unavailable)

All console output is chunked to respect i32::MAX byte limits per call, and NUL bytes are stripped from the fallback path. The console does not consult sink(), so a bar drawn to stderr() stays on the console.

Any other connection (file(), rawConnection(), a custom Rust connection) gets the bytes through its write callback: R_WriteConnection, or before R 4.6 writeBin() / writeChar() (see CONNECTIONS.md, โ€œR versions before 4.6โ€). Rโ€™s errors are caught there: when the connection has been closed, or its write fails, the draw returns an io::Error and indicatif skips it. The bar, and any other bar, keeps working. textConnection() has no write callback, so drawing to one fails the same way.

๐Ÿ”—Convenience Constructors

For common use cases, these functions create ProgressDrawTarget values directly:

use miniextendr_api::progress::*;

// Basic targets (default refresh rate)
let target = term_like_stdout(80);
let target = term_like_stderr(80);
let target = term_like_connection(conn, 80); // `conn`: a connection from R

// With custom refresh rate
let target = term_like_stdout_with_hz(80, 10);
let target = term_like_stderr_with_hz(80, 10);
let target = term_like_connection_with_hz(conn, 80, 10);

All return ProgressDrawTarget, ready to pass to ProgressBar::with_draw_target().

๐Ÿ”—Refresh Rate Tuning

The _with_hz variants let you control how often indicatif redraws the progress bar:

// Update at 5 Hz (every 200ms) - less overhead for slow connections
let target = term_like_stderr_with_hz(80, 5);

// Update at 20 Hz (every 50ms) - smoother appearance for fast operations
let target = term_like_stderr_with_hz(80, 20);
ScenarioSuggested Hz
Long-running computation (minutes)2-5
Medium operations (seconds)10 (default)
Fast operations where smoothness matters15-20
Remote / slow terminal connections2-3

Higher refresh rates mean more R console calls, which adds overhead. For tight loops where each iteration is very fast, consider using pb.inc(batch_size) to update in batches rather than increasing the refresh rate.

๐Ÿ”—Main Thread Safety

All RTerm output is silently dropped when called from a non-main thread:

fn write_console(&self, bytes: &[u8]) -> io::Result<()> {
    if !crate::worker::is_r_main_thread() {
        return Ok(());  // Silent no-op
    }
    // ... actual output ...
}

This means:

  • Progress bars drawn from non-main threads (worker thread, rayon threads) produce no output.
  • Only code running on the main R thread can display progress.

For #[miniextendr] functions, this works naturally โ€“ by default your function runs on the main thread, so progress bar output works directly. If using the worker-thread feature, operations that need R console access should run the progress bar update on the main thread. In practice, since indicatif batches draws, the simplest approach is to create the progress bar and call pb.inc() from your function directly.

See THREADS.md for details on the worker thread model.

๐Ÿ”—ANSI Cursor Support

RTerm implements all cursor movement operations via ANSI escape sequences:

OperationANSI SequenceMethod
Move cursor up N lines\x1b[NAmove_cursor_up(n)
Move cursor down N lines\x1b[NBmove_cursor_down(n)
Move cursor right N columns\x1b[NCmove_cursor_right(n)
Move cursor left N columns\x1b[NDmove_cursor_left(n)
Clear current line\r\x1b[2Kclear_line()

These work in terminals that support ANSI escape codes (most modern terminals, including macOS Terminal, iTerm2, and the RStudio terminal pane). The flush() method calls R_FlushConsole() to ensure output is displayed immediately.

๐Ÿ”—Complete Example

A #[miniextendr] function with a styled progress bar:

use miniextendr_api::miniextendr;
use miniextendr_api::progress::term_like_stderr;
use indicatif::{ProgressBar, ProgressStyle};

#[miniextendr]
pub fn compute_with_progress(n: i32) -> Vec<f64> {
    let n = n as usize;
    let pb = ProgressBar::with_draw_target(Some(n as u64), term_like_stderr(80));
    pb.set_style(
        ProgressStyle::with_template(
            "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})"
        )
        .unwrap()
        .progress_chars("=>-")
    );

    let mut results = Vec::with_capacity(n);
    for i in 0..n {
        // Simulate expensive computation
        let value = ((i as f64) * 0.001).sin().powi(2);
        results.push(value);
        pb.inc(1);
    }

    pb.finish_with_message("computation complete");
    results
}

From R:

result <- compute_with_progress(1000000L)
#> / [00:00:02] [========================================] 1000000/1000000 (0s)

๐Ÿ”—Multi-Progress Bar Example

indicatifโ€™s MultiProgress works too:

use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use miniextendr_api::connection::RStderr;
use miniextendr_api::progress::RTerm;

#[miniextendr]
pub fn parallel_progress() {
    let term = RTerm::new(RStderr, 80);
    let mp = MultiProgress::with_draw_target(
        indicatif::ProgressDrawTarget::term_like(Box::new(term))
    );

    let style = ProgressStyle::with_template("{prefix:.bold} {bar:30} {pos}/{len}").unwrap();

    let pb1 = mp.add(ProgressBar::new(100));
    pb1.set_style(style.clone());
    pb1.set_prefix("Task 1");

    let pb2 = mp.add(ProgressBar::new(200));
    pb2.set_style(style);
    pb2.set_prefix("Task 2");

    for i in 0..200 {
        if i < 100 { pb1.inc(1); }
        pb2.inc(1);
    }

    pb1.finish();
    pb2.finish();
}

๐Ÿ”—Limitations

  • Main thread only. Output from non-main threads is silently dropped. This is by design โ€“ Rโ€™s console functions are not thread-safe.
  • Fixed width. RTerm uses a fixed column width (no dynamic terminal size detection). Pass the desired width when constructing.
  • ANSI support varies. RStudioโ€™s console pane has limited ANSI support. Multi-line progress bars may not render correctly in all R frontends. Single-line progress bars work everywhere.
  • No input. RTerm is output-only. It cannot read user input (e.g., for interactive prompts).
  • Non-interactive sessions. In Rscript or batch mode (R CMD BATCH), console output may be buffered or suppressed depending on the frontend. Progress bars still work but may not display in real-time.
  • Implies nonapi. The indicatif feature enables the nonapi feature because ptr_R_WriteConsoleEx is a non-API symbol. This means R CMD check will note the use of non-API entry points.

๐Ÿ”—See Also