How do you println-debug your TUI programs?

Using a normal println will cause artifacts when rendering

1 Like

That is expected behavior. You are using the backend, such as crossterm or terminion to print to the screen, and it isn’t aware of the println outputs that are also being sent to the terminal, so it messes up the window.
To get around this, we recommend using the tracing crate See the tracing recipe for a quickstart
The reason to use tracing, is that you can configure the outputs of the logs. The basic method is just to dump your logs into a file, but at the bottom of the page I linked, there is a reference to tui-logger, which allows you to create a ratatui-compatible widget that reads those logfiles and integrates the log output into your tui, which is pretty neat!

2 Likes

I have used tracing + tracing_appender.

1 Like

Logging is definitely useful, and I’d recommend that when possible. There’s another approach when you really want something a bit more real time. Render the pretty debug view of whatever element I’m working on somewhere. E.g.:

if self.show_debug {
    Paragraph::new(format!("{self:#?}")
        .wrap(Wrap { trim: false })
        .render(debug_area, buf);
}

Example:

2 Likes

The cli-log crate is a simple option.

As the first line of your main call cli_log::init_cli_log!(); and then elsewhere you can use debug!, info!, warn! as replacements for things like println!. The logging goes nowhere by default, but setting an environment variable turns it on. E.g.

MYCRATE_LOG=debug cargo run

Would turn logging on for the default binary of the “mycrate” crate. The default log filename in this case would be mycrate.log in the current directory.

1 Like

The approach using tracing isn’t a huge amount different. This is how it looks with some extra config to ensure that the logging is off the main thread:

/// Initialize the tracing subscriber to log to a file
///
/// This function initializes the tracing subscriber to log to a file named `tracing.log` in the
/// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the
/// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are
/// written in a non-blocking fashion to ensure that the logs do not block the main thread.
fn init_tracing() -> Result<WorkerGuard> {
    let file = File::create("tracing.log").wrap_err("failed to create tracing.log")?;
    let (non_blocking, guard) = non_blocking(file);

    // By default, the subscriber is configured to log all events with a level of `DEBUG` or higher,
    // but this can be changed by setting the `RUST_LOG` environment variable.
    let env_filter = EnvFilter::builder()
        .with_default_directive(Level::DEBUG.into())
        .from_env_lossy();

    tracing_subscriber::fmt()
        .with_writer(non_blocking)
        .with_env_filter(env_filter)
        .init();
    Ok(guard)
}

Made with VHS

1 Like

thanks for the answers, i think i’ll go with cli-log for now, it seems to fit my simple use case, if too limiting i’ll try tracing