# How to Implement a Custom LogFormatter for TUI-Logger in Rust

> Learn to implement a custom LogFormatter for tui-logger in Rust. This guide shows you how to change log output in the TUI widget using trait implementation and widget injection.

- Repository: [gin66/tui-logger](https://github.com/gin66/tui-logger)
- Tags: how-to-guide
- Published: 2026-03-02

---

**You can implement a custom `LogFormatter` for tui-logger by creating a struct that implements the `LogFormatter` trait with `min_width()` and `format()` methods, then inject it into `TuiLoggerWidget` via the `.formatter()` or `.opt_formatter()` methods.**

The `gin66/tui-logger` crate provides a terminal user interface widget for displaying logs in Rust applications. While it includes a default output style, you gain full control over log rendering—colors, indentation, timestamps, and wrapping—by supplying your own formatter that conforms to the `LogFormatter` trait defined in [`src/widget/logformatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/logformatter.rs).

## Understanding the LogFormatter Trait

The `LogFormatter` trait serves as the extension point for customizing log output appearance. According to the source code in [`src/widget/logformatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/logformatter.rs), any custom formatter must implement two required methods:

- **`min_width(&self) -> u16`**: Returns the minimum widget width your layout requires. The widget uses this to determine if it has enough space to render your format.
- **`format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>>`**: Transforms a single log record into one or more `ratatui::text::Line` objects. You handle text wrapping, styling, and indentation within this method.

The trait requires `Send + Sync` bounds, allowing the widget to store your formatter as `Option<Box<dyn LogFormatter>>` in the `TuiLoggerWidget` struct (see [`src/widget/standard.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/standard.rs)).

## Implementing a Custom Formatter

### Minimal Custom Formatter Example

The following example creates a formatter that displays only the timestamp and message in bold, handling basic word wrapping manually:

```rust
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use tui_logger::widget::logformatter::LogFormatter;
use tui_logger::ExtLogRecord;

/// Simple formatter that prints only the message in bold.
pub struct BoldMessageFormatter {
    style: Style,
}

impl BoldMessageFormatter {
    pub fn new(style: Style) -> Self {
        Self { style }
    }
}

impl LogFormatter for BoldMessageFormatter {
    fn min_width(&self) -> u16 {
        // We need at least room for the timestamp + a space.
        12
    }

    fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
        // Build a single-line output: "[timestamp] MESSAGE"
        let ts = evt.timestamp.format("%H:%M:%S");
        let content = format!("[{}] {}", ts, evt.msg());

        // Wrap manually if the line exceeds `width`.
        let mut lines = Vec::new();
        let mut cur = String::new();
        for word in content.split_whitespace() {
            if cur.len() + word.len() + 1 > width {
                lines.push(Line::from(Span::styled(cur.clone(), self.style)));
                cur.clear();
            }
            if !cur.is_empty() {
                cur.push(' ');
            }
            cur.push_str(word);
        }
        if !cur.is_empty() {
            lines.push(Line::from(Span::styled(cur, self.style)));
        }

        lines
    }
}

```

### Advanced Formatter with Colored Levels and Wrapping

For a more sophisticated implementation, this formatter adds color-coded level prefixes and handles line continuation with proper indentation:

```rust
use ratatui::{
    style::{Color, Style},
    text::{Line, Span},
};
use tui_logger::widget::logformatter::LogFormatter;
use tui_logger::ExtLogRecord;

pub struct ColoredLevelFormatter {
    msg_style: Style,
    level_styles: [(log::Level, Style); 5],
}

impl ColoredLevelFormatter {
    pub fn new() -> Self {
        Self {
            msg_style: Style::default(),
            level_styles: [
                (log::Level::Error, Style::default().fg(Color::Red)),
                (log::Level::Warn,  Style::default().fg(Color::Yellow)),
                (log::Level::Info,  Style::default().fg(Color::Green)),
                (log::Level::Debug, Style::default().fg(Color::Cyan)),
                (log::Level::Trace, Style::default().fg(Color::Magenta)),
            ],
        }
    }

    fn style_for(&self, lvl: log::Level) -> Style {
        self.level_styles
            .iter()
            .find(|(l, _)| *l == lvl)
            .map(|(_, s)| *s)
            .unwrap_or(self.msg_style)
    }
}

impl LogFormatter for ColoredLevelFormatter {
    fn min_width(&self) -> u16 {
        // "[LEVEL] " takes 9 bytes, plus a space.
        10
    }

    fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
        let level_str = format!("[{:5}]", evt.level);
        let level_span = Span::styled(level_str, self.style_for(evt.level));

        let mut lines = Vec::new();
        let mut remaining = evt.msg();

        while !remaining.is_empty() {
            let avail = width.saturating_sub(9); // 9 = len("[LEVEL] ")
            let (take, rest) = if remaining.len() > avail {
                let split = remaining[..avail]
                    .rfind(' ')
                    .unwrap_or(avail);
                remaining.split_at(split)
            } else {
                (remaining, "")
            };

            let mut spans = vec![level_span.clone()];
            spans.push(Span::styled(take.trim_start(), self.msg_style));
            lines.push(Line::from(spans));
            remaining = rest.trim_start();
        }

        lines
    }
}

```

## Wiring the Formatter to TuiLoggerWidget

Once implemented, inject your formatter using either the builder pattern during widget construction or dynamically at runtime:

```rust
use tui_logger::{TuiLoggerWidget, TuiLoggerLevelOutput};
use ratatui::style::{Style, Modifier};

let bold_style = Style::default().add_modifier(Modifier::BOLD);
let my_formatter = BoldMessageFormatter::new(bold_style);

let widget = TuiLoggerWidget::default()
    .formatter(Box::new(my_formatter))
    .output_level(Some(TuiLoggerLevelOutput::Long))
    .block(Block::default().title("My logs"));

```

Alternatively, use `.opt_formatter()` to set or replace the formatter later:

```rust
widget = widget.opt_formatter(Some(Box::new(ColoredLevelFormatter::new())));

```

As implemented in [`src/widget/standard.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/standard.rs), the widget stores the formatter in the field `logformatter: Option<Box<dyn LogFormatter>>`. During rendering, it extracts the formatter using `self.logformatter.take()`, falling back to the built-in `LogStandardFormatter` (defined in [`src/widget/standard_formatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/standard_formatter.rs)) if none is provided.

## Summary

- **Implement `LogFormatter`**: Define `min_width()` and `format()` in a struct to control line rendering, colors, and wrapping.
- **Handle `ExtLogRecord`**: Access timestamp, level, and message content through the `evt` parameter in `format()`.
- **Inject via builder**: Use `.formatter()` or `.opt_formatter()` on `TuiLoggerWidget` to supply your custom implementation.
- **Reference source files**: Study [`src/widget/standard_formatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/standard_formatter.rs) for the default wrapping logic and [`src/widget/logformatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/logformatter.rs) for the trait definition.

## Frequently Asked Questions

### What methods must I implement for the LogFormatter trait?

You must implement `min_width(&self) -> u16`, which specifies the minimum widget width your format requires, and `format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>>`, which converts a log record into styled lines for ratatui rendering.

### Can I change the formatter after creating the TuiLoggerWidget?

Yes. The widget provides the `.opt_formatter()` method that accepts `Option<Box<dyn LogFormatter>>`, allowing you to swap formatters dynamically at runtime. The widget stores the formatter as an `Option<Box<dyn LogFormatter>>` and takes ownership during rendering.

### How does the widget handle word wrapping in custom formatters?

The widget delegates all wrapping logic to your formatter's `format()` method. You receive the available `width` as a parameter and must split the `ExtLogRecord` message into appropriate `Vec<Line<'_>>` segments yourself. The reference implementation in [`src/widget/standard_formatter.rs`](https://github.com/gin66/tui-logger/blob/main/src/widget/standard_formatter.rs) demonstrates splitting on whitespace to avoid breaking words.

### Does the custom formatter need to be Send and Sync?

Yes. The `LogFormatter` trait requires `Send + Sync` bounds because `TuiLoggerWidget` stores the formatter as `Option<Box<dyn LogFormatter>>` and may access it across asynchronous rendering contexts. Ensure your formatter struct implements these traits or uses types that do.