How bat Manages Output to the Terminal and Pagers: A Deep Dive into the Rust Source Code

bat writes formatted output either directly to stdout or through a paging program by using a three-layer abstraction involving paging mode configuration, pager discovery, and an output handle system that hides the destination from the formatting logic.

The bat command-line tool by sharkdp/bat is a syntax-highlighting cat clone written in Rust that must gracefully handle output to both terminals and external pagers. Understanding how bat manages output to the terminal and pagers reveals a sophisticated architecture that separates output destination logic from syntax highlighting and formatting concerns.

The Three Core Concepts Behind bat's Output Management

bat's output system rests on three distinct concepts defined across specific source files: how it decides whether to page, which pager to use, and how to abstract the writing destination.

Paging Mode Configuration (src/paging.rs)

The PagingMode enum in src/paging.rs determines under what conditions bat invokes a pager. The three variants are:

  • Always – Always pipe output through the selected pager
  • QuitIfOneScreen – Only use a pager if the output exceeds one terminal screen
  • Never – Write directly to stdout, bypassing any pager logic

This mode is parsed from the --paging command-line flag or configuration files and passed downstream to the output construction logic.

Pager Discovery and Selection (src/pager.rs)

When paging is required, src/pager.rs handles the complex task of discovering which pager binary to execute. The selection follows a strict precedence order:

  1. Command-line --pager flag
  2. BAT_PAGER environment variable
  3. PAGER environment variable
  4. Fallback to "less"

The PagerKind::from_bin() function classifies the discovered binary into categories: Less, More, Builtin, Bat (self-reference), or Unknown. This classification triggers pager-specific argument rewriting. For example, when the pager is less, bat automatically injects -R (to preserve color codes), -F (to quit if one screen), and -S (to chop long lines), while also adding --no-init for older less versions to prevent screen clearing issues.

Output Abstraction Layer (src/output.rs)

The OutputType enum in src/output.rs provides the critical abstraction that allows the rest of bat to remain agnostic about where output is going. It defines three variants:

  • Stdout – Direct terminal output
  • Pager(Child) – External pager process with piped stdin
  • BuiltinPager – The integrated minus pager implementation

The handle() method returns an OutputHandle that implements the writer interface. Whether writing to a pipe connected to less or directly to stdout, the syntax highlighting logic calls the same write! macro, completely hiding the destination complexity.

Step-by-Step: How bat Decides Where Your Output Goes

When you execute bat, the output path is determined through an eight-step pipeline:

  1. Parse the paging mode – The CLI option --paging (or config equivalent) is converted into a PagingMode value (Always, QuitIfOneScreen, or Never).

  2. Create the output object – OutputType::from_mode(paging_mode, wrapping_mode, pager_from_config) is invoked in src/output.rs. If the mode is Never, it immediately returns stdout().

  3. Select a pager – For non-never modes, try_pager() calls pager::get_pager(pager_from_config) from src/pager.rs, following the precedence: --pager flag → BAT_PAGER → PAGER → "less".

  4. Classify the pager – PagerKind::from_bin() inspects the binary name to determine if it is Less, More, Builtin, Bat, or Unknown.

  5. Handle special cases – If the pager resolves to bat itself, an InvalidPagerValueBat error prevents infinite recursion. If it is the built-in minus pager, a BuiltinPager struct is constructed.

  6. Configure external pagers – For less, the code injects -R (raw control chars), -F (quit-if-one-screen), -S (line chopping), and -K (quit on interrupt), plus --no-init for older versions. Other pagers receive arguments exactly as supplied.

  7. Spawn the pager – A std::process::Command pipes stdin and spawns the child process, storing it in OutputType::Pager. If spawning fails, it gracefully falls back to Stdout.

  8. Write and cleanup – The rest of bat obtains a writer via output.handle()? and writes syntax-highlighted content. When OutputType is dropped, the child pager is waited on and the built-in pager thread is joined, ensuring proper terminal restoration.

Terminal Handling and ANSI Escape Sequences

Before output reaches the writer handle, syntax highlighting is converted to terminal-ready format in src/terminal.rs. The as_terminal_escaped function receives a syntect Style and produces ANSI-escaped strings, supporting true-color (24-bit) or fallback 256-color palettes depending on terminal capabilities.

This escaped text is then fed into the OutputHandle, which may be piping to less -R (which understands ANSI codes) or printing directly to a terminal emulator. This separation of concerns allows bat to support complex color schemes regardless of whether the final destination is a pager or the console.

Code Examples: Working with bat's Output System

The following examples demonstrate the same API that bat uses internally to abstract output destinations.

Manually Building an OutputType with User Configuration

// Example: manually building an OutputType respecting a user‑provided pager
let paging_mode = PagingMode::QuitIfOneScreen;
let wrapping_mode = WrappingMode::Normal;        // from src/wrapping.rs
let config_pager = Some("--pager=less -F");    // from bat config file

let mut out = OutputType::from_mode(paging_mode, wrapping_mode, config_pager)
    .expect("Failed to initialise output");

// Get a writer that works for either a pager or stdout
let mut handle = out.handle().expect("Unable to acquire output handle");

// Write coloured text (the same API used throughout bat)
write!(handle, "{}", as_terminal_escaped(style, "Hello, world!", true, true, false, None))
    .expect("Write failed");

// When `out` goes out of scope the pager (if any) is automatically waited on.

Using the Built-in Pager Directly

use bat::output::{OutputType, OutputHandle};

let mut out = OutputType::BuiltinPager(bat::output::BuiltinPager::new());
let mut handle = out.handle().unwrap();

handle.write_fmt(format_args!("{} lines of output\n", 42)).unwrap();

These snippets illustrate how bat's OutputType enum abstracts over stdout, external pager processes, and the built-in minus pager, allowing the syntax highlighting engine to write without knowing the final destination.

Summary

  • PagingMode in src/paging.rs determines whether bat uses a pager always, only when output exceeds one screen, or never.
  • Pager selection in src/pager.rs follows strict precedence (CLI flag → BAT_PAGER → PAGER → less) and automatically configures less with flags like -R and -F.
  • OutputType in src/output.rs provides a unified writer interface that hides whether output goes to stdout, an external pager process, or the built-in minus pager.
  • Terminal handling in src/terminal.rs generates ANSI escape sequences independently of the output destination, ensuring colors work in both pagers and direct terminal output.
  • The system gracefully falls back to stdout if pager spawning fails and prevents infinite recursion when the pager is set to bat itself.

Frequently Asked Questions

How does bat decide whether to use a pager or print directly to the terminal?

bat checks the PagingMode configuration, which can be set via the --paging command-line flag or the BAT_PAGING environment variable. If set to Never, it writes directly to stdout. If set to Always or QuitIfOneScreen, it attempts to spawn a pager, falling back to stdout if no pager is available or if spawning fails.

What pager does bat use if I don't specify one?

If no pager is specified via --pager, BAT_PAGER, or the standard PAGER environment variable, bat defaults to less. When using less, bat automatically injects the -R flag to preserve color codes and -F to quit if the content fits on a single screen, ensuring syntax highlighting works correctly in the pager.

Can bat use its own built-in pager instead of less?

Yes, bat includes an integrated pager based on the minus crate. When the pager selection logic in src/pager.rs detects a request for the built-in pager, bat constructs a BuiltinPager variant of the OutputType enum. This runs within the same process rather than spawning an external command, providing seamless scrolling without external dependencies.

How does bat prevent infinite recursion when the pager is set to bat itself?

The PagerKind::from_bin() function in src/pager.rs specifically checks if the resolved pager binary points to bat itself. If detected, bat returns an InvalidPagerValueBat error and aborts before spawning the process, preventing the recursive loop that would otherwise occur if bat tried to page its own output through itself.

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 →