How to Implement File Logging with TUI Display Using TuiLoggerFile

Use TuiLoggerFile to configure a file dump, then register it with set_log_file() so the background thread writes formatted log lines to disk while simultaneously displaying them in the TUI widget.

The tui-logger crate allows Rust applications to capture log events in a circular buffer for display inside a terminal user interface (TUI) widget. By implementing TuiLoggerFile, you can persist those same log records to a file on disk without blocking the main thread. This guide explains the architecture and implementation based on the gin66/tui-logger source code.

What is TuiLoggerFile?

TuiLoggerFile is a configuration struct defined in src/file.rs that manages the on-disk persistence of log records. It holds the File handle and formatting toggles that control how each log line appears on disk, including timestamp format, field separators, and whether to include the target name, source file, or line number.

When you register a TuiLoggerFile instance via set_log_file() (located in src/logger/api.rs), the global logger stores it in TUI_LOGGER.inner.dump. From that point forward, every log entry that is moved from the hot buffer to the main buffer by the background thread is also formatted and written to the configured file.

How File Logging Works Under the Hood

The crate uses a dual-buffer architecture with a background mover thread to ensure that file I/O never blocks the UI rendering or application logic.

  1. Hot Buffer: Log events emitted via info!, warn!, or other macros first land in a fast, lock-free hot buffer.
  2. Background Thread: Started automatically by init_logger(), this thread calls move_events() every 10 milliseconds or whenever the hot buffer reaches 50% capacity.
  3. File Dump: Inside src/logger/inner.rs, the move_events() function pulls events from the hot buffer, formats them according to the TuiLoggerFile options, and writes them to disk using writeln!.
  4. TUI Display: The same events are simultaneously stored in the main buffer for the widget to render.

This design ensures that expensive filesystem operations happen on a separate thread while the TUI remains responsive.

Step-by-Step Implementation

Below is a complete, runnable example that initializes the logger, configures a TuiLoggerFile, and emits logs that appear both in the TUI widget and in app.log.

use log::{info, warn, LevelFilter};
use tui_logger::{init_logger, set_default_level, set_log_file, TuiLoggerFile};

fn main() {
    // 1. Initialize the logger with the maximum level you want to capture.
    init_logger(LevelFilter::Trace).expect("Failed to initialise logger");

    // 2. Configure the file logger with custom formatting.
    let file_logger = TuiLoggerFile::new("app.log")
        .output_timestamp(Some("[%Y:%m:%d %H:%M:%S]".to_string()))
        .output_separator('|')
        .output_level(Some(tui_logger::TuiLoggerLevelOutput::Long))
        .output_target(true)
        .output_file(true)
        .output_line(true);

    // 3. Register the file logger with the global instance.
    set_log_file(file_logger);

    // 4. (Optional) Set a default level for targets without explicit filters.
    set_default_level(LevelFilter::Trace);

    // 5. Emit logs that appear in both the TUI and the file.
    info!("Application started");
    warn!(target: "network", "Connection latency high");
}

The init_logger() function spawns the background thread that handles the actual writing. You do not need to manage file flushing manually; the thread ensures data is written periodically or when the buffer threshold is met.

Customizing the File Output

TuiLoggerFile provides a fluent builder API defined in src/file.rs to control the output format. Each method returns Self, allowing chained configuration:

  • output_timestamp(Some(fmt)): Sets the timestamp format using chrono syntax. Pass None to disable timestamps entirely.
  • output_separator(char): Defines the character inserted between fields (default is :).
  • output_level(Some(style)): Choose TuiLoggerLevelOutput::Long for full level names like ERROR, or Abbreviated for single-letter codes. Use None to omit levels.
  • output_target(bool): Include or exclude the log target (module path).
  • output_file(bool): Include or exclude the source filename.
  • output_line(bool): Include or exclude the source line number.

For example, to create a compact log format without timestamps or file information:

let compact = TuiLoggerFile::new("compact.log")
    .output_timestamp(None)
    .output_file(false)
    .output_line(false)
    .output_separator(' ');

Running the Demo

The repository includes a working demonstration in examples/demo.rs that shows file logging alongside the TUI widget. Run it with your preferred backend:

cargo run --example demo --features crossterm

# or

cargo run --example demo --features termion

This demo illustrates how the same log events are processed through move_events() and written to disk while being rendered in the terminal interface.

Summary

  • TuiLoggerFile in src/file.rs configures file output via a builder pattern with options for timestamps, separators, and metadata fields.
  • Registration happens through set_log_file() in src/logger/api.rs, which stores the configuration in the global logger.
  • Processing occurs in move_events() inside src/logger/inner.rs, where a background thread formats and writes events to the file handle every 10ms or when the buffer is half-full.
  • The same log entries are simultaneously available to the TUI widget and the file, ensuring no data loss when the application crashes.

Frequently Asked Questions

How do I disable file logging after it has been enabled?

Once set_log_file() has been called, the global logger holds an Option<TuiLoggerFile>. To disable file logging at runtime, call set_log_file() again with a TuiLoggerFile that has been configured to use a different path, or reinitialize the logger if the application architecture permits. There is no explicit "unset" API, but replacing the configuration effectively stops writes to the previous file (the file handle is dropped).

Does file logging impact TUI rendering performance?

No. All file I/O is performed by the background mover thread in move_events(), not the main thread. The hot buffer acts as a lock-free channel between the application and the background thread, ensuring that log macro calls return immediately. The 10ms interval and 50% buffer threshold provide a balance between latency and throughput.

What file permissions are required for TuiLoggerFile?

TuiLoggerFile::new() attempts to create or open the file with append permissions. The application must have write access to the directory and the file path provided. If the file cannot be opened, the logger will panic during the set_log_file() call or when the background thread first attempts to write, depending on when the file system check occurs.

Can I log to multiple files simultaneously based on log level or target?

The current implementation in gin66/tui-logger supports only a single file dump via TuiLoggerFile. To split logs across multiple files (e.g., separate files for ERROR vs INFO), you would need to implement a custom filtering proxy or use a more complex logging framework like tracing with tracing-appender, as tui-logger is designed for a single shared buffer with optional single-file persistence.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →