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
022import static ch.qos.logback.core.util.Loader.isClassLoadable;
023
024/**
025 * A {@link ConsoleAppender} that always writes through JLine's
026 * {@link AnsiConsole}, enabling ANSI sequences on platforms that need Jansi
027 * (notably Windows).
028 * <p>
029 * Unlike {@link ConsoleAppender}'s deprecated {@code withJansi} path, this
030 * class overrides {@link #wrapTarget(OutputStream)} and calls
031 * {@link AnsiConsole} directly (no reflection). It requires
032 * {@code org.jline:jansi-core} on the classpath.
033 * </p>
034 * <p>
035 * {@link AnsiConsole#systemInstall()} is paired with
036 * {@link AnsiConsole#systemUninstall()} on {@link #stop()} when this appender
037 * performed the install. Console streams are still only flushed on stop (not
038 * closed); see {@link ConsoleAppender#closeOutputStream()}.
039 * </p>
040 *
041 * @param <E> the type of logging events
042 * @author Ceki G&uuml;lc&uuml;
043 * @since 1.6.3
044 * @see AnsiConsole
045 * @see ConsoleAppender#wrapTarget(OutputStream)
046 */
047public class JansiConsoleAppender<E> extends ConsoleAppender<E> {
048
049
050    static final String JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.jline.jansi.AnsiConsole";
051    /**
052     * Status message emitted when {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME}
053     * cannot be loaded. The appender then falls back on the raw console stream.
054     */
055    static final String JANSI_NOT_LOADABLE_MSG0 = "Could not find " + JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME
056            + " on the class path. Falling back on the default stream.";
057
058    static final String JANSI_NOT_LOADABLE_MSG1= "To enable JANSI, add org.jline:jansi-core to the class path.";
059    static final String JANSI_NOT_LOADABLE_MSG2= "See also "+CoreConstants.CODES_URL+"#missingJlineJansi";
060
061    /**
062     * True after this instance has successfully called
063     * {@link AnsiConsole#systemInstall()} and until the matching
064     * {@link AnsiConsole#systemUninstall()} on {@link #stop()}.
065     */
066    private boolean installedByThisAppender;
067
068    /**
069     * Flushes the console stream (via {@link ConsoleAppender#stop()}), then
070     * undoes {@link AnsiConsole#systemInstall()} if this appender performed it.
071     */
072    @Override
073    public void stop() {
074        try {
075            super.stop();
076        } finally {
077            uninstallAnsiConsoleIfInstalledByThisAppender();
078        }
079    }
080
081    /**
082     * Installs Jansi and returns {@link AnsiConsole#out()} or
083     * {@link AnsiConsole#err()} according to the configured target.
084     * <p>
085     * Does not use the deprecated {@code withJansi} / {@code wrapWithJansi}
086     * path. {@link AnsiConsole#systemInstall()} is invoked at most once per
087     * install ownership of this instance.
088     * </p>
089     * <p>
090     * If {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} is not loadable, a warning
091     * is emitted and {@code targetStream} is returned unchanged.
092     * </p>
093     */
094    @Override
095    protected OutputStream wrapTarget(OutputStream targetStream) {
096        boolean jansiLoadable = isClassLoadable(JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME, getContext());
097        if (!jansiLoadable) {
098            addWarn(JANSI_NOT_LOADABLE_MSG0);
099            addWarn(JANSI_NOT_LOADABLE_MSG1);
100            addWarn(JANSI_NOT_LOADABLE_MSG2);
101            return targetStream;
102        }
103        try {
104            addInfo("Enabling JANSI AnsiPrintStream via " + JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME + ".");
105            if (!installedByThisAppender) {
106                AnsiConsole.systemInstall();
107                installedByThisAppender = true;
108            }
109            if (target == ConsoleTarget.SystemErr) {
110                return AnsiConsole.err();
111            } else {
112                return AnsiConsole.out();
113            }
114        } catch (Exception e) {
115            addWarn("Failed to create AnsiPrintStream. Falling back on the default stream.", e);
116            return targetStream;
117        }
118    }
119
120    private void uninstallAnsiConsoleIfInstalledByThisAppender() {
121        if (!installedByThisAppender) {
122            return;
123        }
124        installedByThisAppender = false;
125        try {
126            AnsiConsole.systemUninstall();
127            addInfo("Uninstalled JANSI AnsiConsole previously installed by this appender.");
128        } catch (RuntimeException e) {
129            addWarn("Failed to uninstall AnsiConsole.", e);
130        }
131    }
132
133}