001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core;
015
016import java.io.OutputStream;
017
018import org.jline.jansi.AnsiConsole;
019
020import ch.qos.logback.core.joran.spi.ConsoleTarget;
021
022/**
023 * A {@link ConsoleAppender} that always writes through JLine's
024 * {@link AnsiConsole}, enabling ANSI sequences on platforms that need Jansi
025 * (notably Windows).
026 * <p>
027 * Unlike {@link ConsoleAppender}'s deprecated {@code withJansi} path, this
028 * class overrides {@link #wrapTarget(OutputStream)} and calls
029 * {@link AnsiConsole} directly (no reflection). It requires
030 * {@code org.jline:jansi-core} on the classpath.
031 * </p>
032 * <p>
033 * {@link AnsiConsole#systemInstall()} is paired with
034 * {@link AnsiConsole#systemUninstall()} on {@link #stop()} when this appender
035 * performed the install. Console streams are still only flushed on stop (not
036 * closed); see {@link ConsoleAppender#closeOutputStream()}.
037 * </p>
038 *
039 * @param <E> the type of logging events
040 * @author Ceki G&uuml;lc&uuml;
041 * @since 1.6.3
042 * @see AnsiConsole
043 * @see ConsoleAppender#wrapTarget(OutputStream)
044 */
045public class JansiConsoleAppender<E> extends ConsoleAppender<E> {
046
047    /**
048     * True after this instance has successfully called
049     * {@link AnsiConsole#systemInstall()} and until the matching
050     * {@link AnsiConsole#systemUninstall()} on {@link #stop()}.
051     */
052    private boolean installedByThisAppender;
053
054    /**
055     * Flushes the console stream (via {@link ConsoleAppender#stop()}), then
056     * undoes {@link AnsiConsole#systemInstall()} if this appender performed it.
057     */
058    @Override
059    public void stop() {
060        try {
061            super.stop();
062        } finally {
063            uninstallAnsiConsoleIfInstalledByThisAppender();
064        }
065    }
066
067    /**
068     * Installs Jansi and returns {@link AnsiConsole#out()} or
069     * {@link AnsiConsole#err()} according to the configured target.
070     * <p>
071     * Does not use the deprecated {@code withJansi} / {@code wrapWithJansi}
072     * path. {@link AnsiConsole#systemInstall()} is invoked at most once per
073     * install ownership of this instance.
074     * </p>
075     */
076    @Override
077    protected OutputStream wrapTarget(OutputStream targetStream) {
078        try {
079            addInfo("Enabling JANSI AnsiPrintStream via org.jline.jansi.AnsiConsole.");
080            if (!installedByThisAppender) {
081                AnsiConsole.systemInstall();
082                installedByThisAppender = true;
083            }
084            if (target == ConsoleTarget.SystemErr) {
085                return AnsiConsole.err();
086            } else {
087                return AnsiConsole.out();
088            }
089        } catch (Exception e) {
090            addWarn("Failed to create AnsiPrintStream. Falling back on the default stream.", e);
091            return targetStream;
092        }
093    }
094
095    private void uninstallAnsiConsoleIfInstalledByThisAppender() {
096        if (!installedByThisAppender) {
097            return;
098        }
099        installedByThisAppender = false;
100        try {
101            AnsiConsole.systemUninstall();
102            addInfo("Uninstalled JANSI AnsiConsole previously installed by this appender.");
103        } catch (RuntimeException e) {
104            addWarn("Failed to uninstall AnsiConsole.", e);
105        }
106    }
107
108}