Colorful Terminal Output with Jansi

Last update: 2025-12-10 11:30:28 +0800 +0800

Jansi is a Java library for generating and processing ANSI escape sequences. These sequences let a terminal display colors and styles, move the cursor, erase parts of the screen, and perform other terminal operations.

For example, the following bytes mean “use bold red text,” print FAILED, and then restore the terminal’s default attributes:

text
\u001B[1;31mFAILED\u001B[0m

Writing those codes by hand is difficult to read and easy to get wrong. Jansi provides a fluent Java API for constructing them and terminal-aware streams that can pass through, adapt, or remove them as appropriate.

Jansi is useful for command-line applications, installers, build tools, and small terminal dashboards. It is not a logging framework and is not a complete interactive-shell framework. Use JLine’s terminal and line-reader APIs when an application also needs command history, completion, input editing, signal handling, or a rich full-screen interface.

The Project Moved to JLine #

Many examples on the internet use these old Maven coordinates and imports:

text
org.fusesource.jansi:jansi
org.fusesource.jansi.Ansi
org.fusesource.jansi.AnsiConsole

The original FuseSource project is no longer maintained. Jansi was moved into the JLine project, and new applications should use:

text
org.jline:jansi
org.jline.jansi.Ansi
org.jline.jansi.AnsiConsole

The API remains familiar, but the package change matters when copying examples or integrating with a library that discovers Jansi by class name.

Add the maintained JLine artifact to Maven:

xml
<properties>
    <jline.version>4.0.0</jline.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.jline</groupId>
        <artifactId>jansi</artifactId>
        <version>${jline.version}</version>
    </dependency>
</dependencies>

Or with Gradle:

groovy
dependencies {
    implementation "org.jline:jansi:4.0.0"
}

Use the version managed by the application’s JLine bill of materials or dependency management when JLine is already present. This avoids loading incompatible versions of closely related JLine modules.

ANSI Escape Sequences #

An ANSI control sequence starts with the escape character, represented in Java as \u001B or \033, followed by instructions interpreted by the terminal. The Select Graphic Rendition, or SGR, family controls color and style:

java
String red = "\u001B[31m";
String bold = "\u001B[1m";
String reset = "\u001B[0m";

System.out.println(red + bold + "Danger" + reset);

This works in a compatible terminal, but direct string concatenation has several problems:

  • numeric control codes hide the intent;
  • forgetting the reset can color every later line;
  • terminals support different color depths and capabilities;
  • escape sequences are undesirable in redirected text, files, and machine- readable output;
  • cursor operations are stateful and can corrupt output when several threads write concurrently.

Jansi addresses the first four problems. Coordinating concurrent writers is still the application’s responsibility.

Building Styled Text #

The org.jline.jansi.Ansi builder appends text and control sequences to one object:

java
import org.jline.jansi.Ansi;

import static org.jline.jansi.Ansi.Color.GREEN;
import static org.jline.jansi.Ansi.Color.RED;

public final class StatusPrinter {

    private StatusPrinter() {
    }

    public static void printResult(boolean successful) {
        Ansi line = Ansi.ansi()
                .bold()
                .fg(successful ? GREEN : RED)
                .a(successful ? "SUCCESS" : "FAILED")
                .reset()
                .a(" Deployment finished");

        System.out.println(line);
    }
}

Calling toString() is implicit when the Ansi object is passed to println. The builder can be converted explicitly when an API requires a String:

java
String message = Ansi.ansi()
        .fgBright(Ansi.Color.CYAN)
        .a("Listening on port 8080")
        .reset()
        .toString();

The commonly used methods include:

MethodEffect
fg(color)Sets a foreground color
fgBright(color)Sets a bright foreground color
bg(color)Sets a background color
bgBright(color)Sets a bright background color
bold()Enables bold or increased intensity
underline()Enables underlining
a(value)Appends text or another value
newline()Appends the platform line separator
reset()Restores default colors and attributes

The exact appearance is controlled by the terminal and its theme. “Bright black,” for example, is commonly rendered as gray, while bold may be displayed as brighter text rather than a heavier font.

Always reset attributes at the end of independently printed styled content:

java
System.out.println(
        Ansi.ansi()
                .fg(Ansi.Color.YELLOW)
                .a("Warning:")
                .reset()
                .a(" cache is almost full")
);

Resetting before the unstyled suffix also makes the intended boundary clear.

Backgrounds and Nested Styles #

Foreground, background, and text attributes can be combined:

java
String banner = Ansi.ansi()
        .fg(Ansi.Color.WHITE)
        .bg(Ansi.Color.RED)
        .bold()
        .a(" STOP ")
        .reset()
        .toString();

reset() resets all attributes; it does not restore an earlier nested style. When composing reusable fragments, either let one component own the entire styled region or explicitly reapply the outer style after a reset. Do not assume that terminal formatting behaves like nested HTML elements.

Renderer Syntax #

Jansi also supports a compact markup syntax:

java
String result = Ansi.ansi()
        .render("@|bold,green Build passed|@")
        .toString();

The fluent API is usually clearer for conditional or composed output. Renderer syntax is convenient for short, fixed messages, but should not be applied blindly to untrusted text because @|...|@ has meaning to the renderer.

Installing Terminal-Aware Streams #

Generating an ANSI string and making it suitable for the current output destination are separate concerns. Ansi generates sequences. AnsiConsole supplies streams that process them.

The most direct application-wide setup replaces System.out and System.err:

java
import org.jline.jansi.Ansi;
import org.jline.jansi.AnsiConsole;

public final class CliApplication {

    private CliApplication() {
    }

    public static void main(String[] args) {
        AnsiConsole.systemInstall();
        try {
            System.out.println(
                    Ansi.ansi()
                            .fg(Ansi.Color.GREEN)
                            .a("Ready")
                            .reset()
            );
        } finally {
            AnsiConsole.systemUninstall();
        }
    }
}

Installation is reference-counted. Every call to systemInstall() must have a corresponding call to systemUninstall(). The finally block is therefore important even when the program normally exits immediately.

Replacing the global streams affects the entire process, including libraries and logging systems that write to System.out or System.err. A library should generally not do this on behalf of its host application. An application that wants a smaller scope can write to Jansi’s streams directly:

java
AnsiConsole.out().println(
        Ansi.ansi()
                .fg(Ansi.Color.MAGENTA)
                .a("Interactive mode")
                .reset()
);

AnsiConsole.out() and AnsiConsole.err() initialize and return terminal-aware streams without assigning them to Java’s global stream fields.

Terminal Detection and Redirection #

Color is appropriate for a person looking at a terminal, but usually not for a file, pipe, test capture, log collector, or JSON consumer:

shell
java -jar tool.jar > result.txt
java -jar tool.jar | jq .

In its default mode, Jansi uses the terminal type and output destination to select its behavior:

  • on a native terminal, ANSI sequences are retained and adapted to supported capabilities;
  • on unsupported or redirected output, ANSI sequences are stripped;
  • color depth can be inferred from terminal information such as TERM and COLORTERM.

The behavior can be overridden with Java system properties:

shell
# Always remove ANSI sequences.
java -Djansi.mode=strip -jar tool.jar

# Emit ANSI even when output is redirected.
java -Djansi.mode=force -jar tool.jar

# Force a particular color depth.
java -Djansi.colors=256 -jar tool.jar
java -Djansi.colors=truecolor -jar tool.jar

jansi.out.mode and jansi.err.mode override the mode for one stream, while jansi.out.colors and jansi.err.colors do the same for color depth.

Forcing color is useful when another program understands ANSI sequences. Stripping is appropriate for deterministic plain-text output. Defaults are normally best for an end-user CLI.

Do not use System.console() != null as the only color test. It is a useful hint, but IDE terminals, pseudo-terminals, containers, pipes, and test runners make terminal detection more nuanced. Let the terminal library make the decision and provide an explicit --color=auto|always|never option for users when predictable control matters.

Windows Support #

Historically, Jansi was especially useful because older Windows consoles did not interpret ANSI sequences. Jansi could translate terminal operations using platform-specific support.

Modern Windows 10 and later terminals generally support ANSI natively, including Windows Terminal and current cmd.exe and PowerShell environments. That means AnsiConsole.systemInstall() is no longer required merely to make basic colors work on most current systems. It remains useful when an application wants Jansi’s consistent stream processing, redirected-output handling, color-depth adaptation, or support for a wider deployment matrix.

Test the actual environments supported by the application. “Windows,” an IDE console, an SSH session, and a CI runner do not describe one uniform set of terminal capabilities.

Cursor and Screen Control #

Jansi can generate more than color codes:

java
Ansi progress = Ansi.ansi()
        .cursorUp(1)
        .eraseLine()
        .a("Progress: 75%");

System.out.print(progress);
System.out.flush();

Other operations include:

  • positioning the cursor with cursor(row, column);
  • moving it up, down, left, or right;
  • saving and restoring cursor position;
  • erasing a line or the screen;
  • scrolling terminal content.

These operations should be reserved for interactive terminal output. If the same bytes are written to a file or mixed with ordinary logs, the result is unreadable. Cursor-based displays should also have one owner that serializes updates. Background log messages can otherwise move the cursor between an erase and a redraw.

For a sophisticated interactive application, use JLine’s Terminal, LineReader, and display abstractions rather than building a terminal user interface entirely from cursor sequences.

Integrating with SLF4J and Logback #

SLF4J, Logback, and Jansi solve different problems:

ComponentResponsibility
SLF4JAPI through which application code creates logging events
LogbackSLF4J provider that filters, formats, and routes those events
ANSI pattern convertersInsert style sequences into formatted console text
JansiGenerate or process ANSI sequences for a terminal stream

SLF4J itself does not color output and does not know whether an event is going to a terminal, a rolling file, JSON, or a remote collector. Application code should therefore log semantic, uncolored values:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class ImportService {

    private static final Logger LOGGER =
            LoggerFactory.getLogger(ImportService.class);

    public void importFile(String name, int recordCount) {
        LOGGER.info("Imported {} records from {}", recordCount, name);
    }
}

Do not put Ansi.ansi() values into general log messages:

java
// Avoid this: every appender receives control characters in the message.
LOGGER.info("{} Import failed", Ansi.ansi().fg(Ansi.Color.RED).a("ERROR").reset());

The same logging event may be sent to both a console and a file. Styling in the message contaminates every destination and makes search, parsing, alerting, and tests harder.

A Colored Console and a Plain File #

Logback’s pattern converters can add ANSI only while formatting the console appender. %highlight chooses a color according to the log level. Converters such as %red, %yellow, %green, %blue, %magenta, %cyan, %white, and %boldRed apply a fixed style.

xml
<configuration>
    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoder>
            <pattern>
                %d{HH:mm:ss.SSS} %highlight(%-5level)
                %cyan(%logger{36}) - %msg%n
            </pattern>
        </encoder>
    </appender>

    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>
                logs/application.%d{yyyy-MM-dd}.log
            </fileNamePattern>
            <maxHistory>14</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level
                %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

The console pattern contains the color converters. The file pattern does not. The logging calls remain independent of presentation.

In a modern terminal, the ANSI sequences emitted by Logback usually work without Jansi. If the application globally installs the maintained Jansi stream before Logback’s console appender captures System.err, Jansi can process those sequences:

java
public static void main(String[] args) {
    AnsiConsole.systemInstall();
    try {
        runApplication(args);
    } finally {
        AnsiConsole.systemUninstall();
    }
}

Initialize the terminal stream before the logging system creates its console appender. Otherwise, the appender may retain a reference to the original stream.

The withJansi Compatibility Trap #

Logback’s ConsoleAppender has a <withJansi>true</withJansi> option. Current Logback source still discovers this legacy class by name:

text
org.fusesource.jansi.AnsiConsole

The maintained JLine artifact exposes:

text
org.jline.jansi.AnsiConsole

Consequently, <withJansi>true</withJansi> does not integrate with the relocated org.jline:jansi artifact. Adding the unmaintained org.fusesource.jansi:jansi artifact merely to satisfy that reflective lookup is not a good default for a new application.

Prefer one of these approaches:

  1. let a modern terminal render Logback’s ANSI converters directly;
  2. install the maintained org.jline.jansi.AnsiConsole streams in application bootstrap code when Jansi processing is required;
  3. avoid ANSI converters when output is destined for a collector or a format such as JSON.

Jansi does not decide log levels, format events, rotate files, or route logs. Those remain Logback responsibilities.

Spring Boot Logging #

Spring Boot already has its own ANSI support for Logback and Log4j2 configuration. Its %clr conversion word can color content by level or by an explicit style:

xml
<pattern>
    %clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint}
    %clr(%-5level)
    %clr(${PID:- }){magenta}
    %clr([%thread]){faint}
    %clr(%logger{39}){cyan} : %msg%n
</pattern>

Boot controls ANSI emission through:

properties
spring.output.ansi.enabled=detect

The supported policies are detect, always, and never. detect is the normal choice for applications used both interactively and through redirected output.

Spring Boot’s %clr converter and ANSI policy are separate from direct Jansi usage. A typical Boot service should use %clr for console logs and keep file or JSON appenders plain. Add Jansi only if the application also prints its own terminal UI or requires Jansi-specific stream processing.

Separating Human and Machine Output #

A reliable CLI treats its output channels as an interface:

  • use stdout for the command’s primary result;
  • use stderr for diagnostics, progress, and logs;
  • keep structured output modes free of ANSI sequences;
  • disable cursor movement when output is not interactive;
  • provide an explicit color policy when scripts need stable behavior.

For example, a command that supports JSON should not decorate the JSON:

java
if (jsonOutput) {
    System.out.println(json);
} else {
    AnsiConsole.out().println(
            Ansi.ansi()
                    .fg(Ansi.Color.GREEN)
                    .a("Created ")
                    .reset()
                    .a(resourceId)
    );
}

Color should improve human readability without changing the meaning or parseability of the underlying information. Never make color the only way to distinguish success from failure; also use words, symbols, exit codes, or structured fields.

Testing Terminal Output #

Most unit tests should verify the semantic text rather than depend on a real terminal. Put presentation behind a small formatter and make the color policy explicit:

java
import org.jline.jansi.Ansi;

public final class ResultFormatter {

    public String success(String message, boolean colorEnabled) {
        if (!colorEnabled) {
            return "SUCCESS " + message;
        }

        return Ansi.ansi()
                .fg(Ansi.Color.GREEN)
                .bold()
                .a("SUCCESS")
                .reset()
                .a(" ")
                .a(message)
                .toString();
    }
}

Then test both contracts:

java
assertEquals(
        "SUCCESS uploaded",
        formatter.success("uploaded", false)
);

assertTrue(
        formatter.success("uploaded", true).contains("\u001B[")
);

Exact ANSI strings can be asserted when the sequence itself is the contract, but such tests are tightly coupled to formatting choices. Integration tests for auto-detection should run in both a pseudo-terminal and a redirected process; capturing System.out in a byte array is not equivalent to attaching a terminal.

Avoid changing Jansi’s global enabled state or replacing System.out in tests that run concurrently. Prefer injected streams and local formatters where possible, and restore global state in finally when a process-level test really needs it.

Practical Guidelines #

Use Jansi deliberately:

  1. depend on the maintained org.jline:jansi artifact, not the old FuseSource coordinates;
  2. use the Ansi builder instead of handwritten escape codes;
  3. end independent styled regions with reset();
  4. let terminal-aware code strip color from redirected output by default;
  5. keep ANSI out of files, JSON, and general SLF4J message arguments;
  6. add color in the Logback console pattern, not in application logging calls;
  7. do not expect Logback’s legacy <withJansi> option to find the relocated JLine package;
  8. install and uninstall global Jansi streams symmetrically;
  9. serialize cursor-based output and keep it separate from ordinary logs;
  10. offer users auto, always, and never color policies when output is a public CLI contract.

With those boundaries, terminal output can be expressive for people while remaining predictable for files, pipelines, tests, and logging systems.

Comments

Read or comment on GitHub (sign-in required to comment)