All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.build.util.AnsiConsoleInstaller Maven / Gradle / Ivy

/*
 * Copyright (c) 2020 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.build.util;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.fusesource.jansi.Ansi;
import picocli.jansi.graalvm.AnsiConsole;

import static org.fusesource.jansi.Ansi.ansi;

/**
 * Installer for {@link System#out} and {@link System#err} streams that support {@link Ansi} escapes, if possible.
 * Supports {@code GraalVM} native executables.
 */
public class AnsiConsoleInstaller {

    /**
     * The Helidon child process property name.
     */
    public static final String HELIDON_CHILD_PROCESS_PROPERTY = "helidon.child.process";

    /**
     * The system property that, if {@code true}, will bypass the default check
     * to see if stdin is a terminal.
     */
    private static final String JANSI_FORCE_PROPERTY = "jansi.force";

    /**
     * The system property that, if {@code true}, will install streams that strip Ansi escapes.
     */
    private static final String JANSI_STRIP_PROPERTY = "jansi.strip";

    /**
     * The system property that, if {@code true}, will not install streams.
     */
    private static final String JANSI_PASS_THROUGH_PROPERTY = "jansi.passthrough";

    // Note: Class instances are not used here since this class is used within a maven plugin
    //       that might have a different version of Jansi
    private static final String JANSI_PACKAGE_PREFIX = "org.fusesource.jansi";
    private static final String JANSI_STRIP_STREAM_CLASS_NAME = "org.fusesource.jansi.AnsiPrintStream";
    private static final boolean IS_HELIDON_CHILD_PROCESS = Boolean.getBoolean(HELIDON_CHILD_PROCESS_PROPERTY);
    private static final boolean DEBUG = "debug".equals(System.getProperty("log.level"));
    private static final AtomicBoolean INSTALLED = new AtomicBoolean();
    private static final AtomicReference CONSOLE_TYPE = new AtomicReference<>();
    private static final AtomicBoolean ENABLED = new AtomicBoolean();

    /**
     * Console types.
     */
    public enum ConsoleType {
        /**
         * Support Ansi escapes.
         */
        ANSI(JANSI_FORCE_PROPERTY),

        /**
         * Strip Ansi escapes.
         */
        STRIP_ANSI(JANSI_STRIP_PROPERTY),

        /**
         * Do not explicitly support or strip Ansi escapes.
         */
        DEFAULT(JANSI_PASS_THROUGH_PROPERTY);

        private final String argument;

        ConsoleType(String propertyName) {
            this.argument = "-D" + HELIDON_CHILD_PROCESS_PROPERTY + "=true" + " -D" + propertyName + "=true";
        }

        /**
         * Returns the command-line argument that forces Ansi escapes to be handled in a child process
         * the same way as they are in this one.
         *
         * @return The argument.
         */
        public String childProcessArgument() {
            return argument;
        }
    }

    /**
     * Returns the command-line argument that forces Ansi escapes to be handled in a child process
     * the same way as they are in this one.
     *
     * @return The argument.
     */
    public static String childProcessArgument() {
        return consoleType().childProcessArgument();
    }

    /**
     * Returns whether or not this process is a child of another Helidon process.
     *
     * @return {@code true} if this process is a child of another Helidon process.
     */
    public static boolean isHelidonChildProcess() {
        return IS_HELIDON_CHILD_PROCESS;
    }

    /**
     * Returns whether or not Ansi escapes are enabled. Calls {@link #ensureInstalled()}.
     *
     * @return {@code true} if enabled.
     */
    public static ConsoleType consoleType() {
        ensureInstalled();
        return CONSOLE_TYPE.get();
    }

    /**
     * Returns whether or not Ansi escapes are enabled. Calls {@link #ensureInstalled()}.
     *
     * @return {@code true} if enabled.
     */
    public static boolean areAnsiEscapesEnabled() {
        return ensureInstalled();
    }

    /**
     * Clears the screen if Ansi escapes are enabled.
     *
     * @return {@code true} if Ansi escapes are enabled.
     */
    public static boolean clearScreen() {
        return clearScreen(0);
    }

    /**
     * Clears the screen from the given row if Ansi escapes are enabled.
     *
     * @param startRow The row at which to start clearing.
     * @return {@code true} if Ansi escapes are enabled.
     */
    public static boolean clearScreen(int startRow) {
        if (areAnsiEscapesEnabled()) {
            System.out.print(ansi().cursor(startRow, 0).eraseScreen());
            System.out.flush();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Ensures that the system streams have been installed.
     *
     * @return {@code true} if Ansi escapes are enabled.
     */
    public static boolean ensureInstalled() {
        if (!INSTALLED.getAndSet(true)) {
            ConsoleType desiredType = desiredConsoleType();
            AnsiConsole.systemInstall();
            ConsoleType installedType = installedConsoleType(desiredType);
            CONSOLE_TYPE.set(installedType);
            ENABLED.set(installedType == ConsoleType.ANSI);
        }
        return ENABLED.get();
    }

    private static ConsoleType desiredConsoleType() {
        if (Boolean.getBoolean(JANSI_FORCE_PROPERTY)) {
            debug("Jansi streams requested: %s=true", JANSI_FORCE_PROPERTY);
            return ConsoleType.ANSI;
        } else if (Boolean.getBoolean(JANSI_STRIP_PROPERTY)) {
            debug("Jansi strip streams requested: %s=true", JANSI_STRIP_PROPERTY);
            return ConsoleType.STRIP_ANSI;
        } else if (Boolean.getBoolean(JANSI_PASS_THROUGH_PROPERTY)) {
            debug("Jansi pass through streams requested: %s=true", JANSI_PASS_THROUGH_PROPERTY);
            return ConsoleType.STRIP_ANSI;
        } else if (System.console() != null) {
            debug("No Jansi request, but Console is available");
            return ConsoleType.ANSI;
        } else {
            debug("No Jansi request and Console is not available");
            return ConsoleType.DEFAULT;
        }
    }

    private static ConsoleType installedConsoleType(ConsoleType desiredType) {
        final String systemOutClass = System.out.getClass().getName();
        if (systemOutClass.startsWith(JANSI_PACKAGE_PREFIX)) {
            // We have a Jansi type installed, check if it is the type that strips escapes
            if (systemOutClass.equals(JANSI_STRIP_STREAM_CLASS_NAME)) {
                if (desiredType != ConsoleType.STRIP_ANSI) {
                    debug("Desired = %s, but Ansi escapes will be stripped by system streams.", desiredType);
                }
                return ConsoleType.STRIP_ANSI;
            } else {
                if (desiredType != ConsoleType.ANSI) {
                    debug("Desired = %s, but Ansi escapes should be supported by system streams.", desiredType);
                }
                return ConsoleType.ANSI;
            }
        } else {
            if (desiredType != ConsoleType.DEFAULT) {
                debug("Desired = %s, but System.out not a Jansi type (%s) ao Ansi escapes should not be stripped",
                        desiredType, systemOutClass);
            }
            return ConsoleType.DEFAULT;
        }
    }

    private static void debug(String message, Object... args) {
        // Do NOT write to Log.debug() if it does not already have a writer, as that will result in a cycle
        if (Log.hasWriter()) {
            Log.debug(message, args);
        } else if (DEBUG) {
            System.out.println(String.format(message, args));
        }
    }

    private AnsiConsoleInstaller() {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy