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

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

/*
 * Copyright (c) 2020, 2021 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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Attribute;
import org.fusesource.jansi.Ansi.Color;

import static java.util.Objects.requireNonNull;
import static org.fusesource.jansi.Ansi.ansi;

/**
 * Rich text styles.
 */
@SuppressWarnings("StaticInitializerReferencesSubClass")
public class Style {
    private static final Style NONE = new Style();
    private static final Style PLAIN = new Emphasis(Attribute.RESET);
    private static final Style BOLD = new Emphasis(Attribute.INTENSITY_BOLD);
    private static final Style ITALIC = new Emphasis(Attribute.ITALIC);
    private static final Style FAINT = new Emphasis(Attribute.INTENSITY_FAINT);
    private static final Style BOLD_ITALIC = new StyleList(BOLD).add(ITALIC);
    private static final Style NEGATIVE = new Emphasis(Attribute.NEGATIVE_ON);
    private static final boolean ENABLED = AnsiConsoleInstaller.install();
    private static final Map STYLES = stylesByName();

    private static final char ESC_CH1 = '\033';
    private static final char ESC_CH2 = '[';
    private static final char CMD_CH2 = ']';
    private static final char BEL = 7;
    private static final char ST_CH2 = '\\';
    private static final char CHARSET0_CH2 = '(';
    private static final char CHARSET1_CH2 = ')';
    private static final String ANSI_ESCAPE_BEGIN = "" + ESC_CH1 + ESC_CH2;

    private enum AnsiState {
        ESC1,
        ESC2,
        NEXT_ARG,
        STR_ARG_END,
        INT_ARG_END,
        CMD,
        CMD_END,
        CMD_PARAM,
        ST,
        CHARSET
    }

    /**
     * Return all styles, by name.
     *
     * @return The styles. Not immutable, so may be (carefully!) modified.
     */
    public static Map styles() {
        return STYLES;
    }

    /**
     * Returns a no-op style.
     *
     * @return The style.
     */
    public static Style none() {
        return NONE;
    }

    /**
     * Returns the style for the given name.
     * 

*

Text Color Names

*
    *
  • {@code red}
  • *
  • {@code yellow}
  • *
  • {@code green}
  • *
  • {@code cyan}
  • *
  • {@code blue}
  • *
  • {@code magenta}
  • *
  • {@code white}
  • *
  • {@code black}
  • *
  • {@code default}
  • *
  • {@code bold}
  • *
  • {@code negative}
  • *
*

* See Portability below for more on {@code default}, {@code bold} and {@code negative}. *

*

Background Color Names

*
    *
  • {@code bg_red}
  • *
  • {@code bg_yellow}
  • *
  • {@code bg_green}
  • *
  • {@code bg_cyan}
  • *
  • {@code bg_blue}
  • *
  • {@code bg_magenta}
  • *
  • {@code bg_white}
  • *
  • {@code bg_black}
  • *
  • {@code bg_default}
  • *
  • {@code bg_negative}
  • *
*

*

Emphasis Names

*
    *
  • {@code italic}
  • *
  • {@code bold}
  • *
  • {@code faint}
  • *
  • {@code plain}
  • *
  • {@code underline}
  • *
  • {@code strikethrough}
  • *
  • {@code negative}
  • *
  • {@code conceal}
  • *
  • {@code blink}
  • *
*

*

Aliases

*

* Every text color has the following aliases: *
    *
  • Bold variant with an uppercase name (e.g. {@code RED})
  • *
  • Bold variant with {@code '*'} prefix and suffix (e.g. {@code *red*})
  • *
  • Italic variant with {@code '_'} prefix and suffix (e.g. {@code _red_})
  • *
  • Bold italic variant with {@code '_*'} prefix and {@code '*_'} suffix (e.g. {@code _*red*_} or {@code *_red_*})
  • *
  • Bright variants of the color and all the above with a {@code '!'} suffix * (e.g. {@code red!}, {@code RED!}, {@code *red*!}, {@code _red_!}
  • *
*

* Every background color has the following aliases: *
    *
  • Bright variants with a {@code '!'} suffix (e.g. {@code bg_yellow!})
  • *
*

* The {@code bold,italic} combination has the following aliases: *
    *
  • {@code _bold_}
  • *
  • {@code *italic*}
  • *
  • {@code ITALIC}
  • *
*

* When {@code bold} is used without any other color it is an alias for {@code default,bold}. *

* The {@code negative} text color and the {@code bg_negative} background color are identical: they invert *both* the default * text color and the background color. *

*

Portability

*

* Most terminals provide mappings between the standard color names used here and what they actually render. So, for example, * you may declare {@code red} but a terminal could be configured to render it as blue; generally, though, themes * will use a reasonably close variant of the pure color. *

* Where things get interesting is when a color matches (or closely matches) the terminal background color: any use of that * color will fade or disappear entirely. The common cases are with {@code white} or {@code bg_white} on a light theme and * {@code black} or {@code bg_black} on a dark theme. While explicit use of {@code white} may work well in your * terminal, it won't work for everyone; if this matters in your use case... *

* The portability problem can be addressed by using these special colors in place of any white or black style: *
    *
  • {@code default} selects the default text color in the current theme
  • *
  • {@code bold} selects the bold variant of the default text color
  • *
  • {@code negative} inverts the default text and background colors
  • *
  • {@code bg_negative} an alias for {@code negative}
  • *
  • {@code bg_default} selects the default background color in the current theme
  • *
*

* Finally, {@code strikethrough}, (the really annoying) {@code blink} and {@code conceal} may not be enabled or supported in * every terminal and may do nothing. For {@code conceal}, presumably you can just leave out whatever you don't want shown; for * the other two best to assume they don't work and use them only as additional emphasis. * * @param name The name. * @return The style or {@link #none} if styles are disabled. */ public static Style named(String name) { return named(name, false); } /** * Returns the style for the given name, or optionally fails if not present. * * @param name The name. * @param required {@code true} if required. * @return The style, or {@link #none()} if styles are disabled of if name not found and not required. * @throws IllegalArgumentException If required and name is not found. */ public static Style named(String name, boolean required) { final Style style = STYLES.get(name); if (style == null) { if (required) { throw new IllegalArgumentException("Unknown style: " + name); } else { return NONE; } } return ENABLED ? style : NONE; } /** * Returns a list of all color names. * * @return The names. */ public static List colorNames() { return List.of("red", "yellow", "green", "cyan", "blue", "magenta", "white", "black", "default", "bold", "negative"); } /** * Returns a list of all background color names. * * @return The names. */ public static List backgroundColorNames() { return List.of("bg_red", "bg_yellow", "bg_green", "bg_cyan", "bg_blue", "bg_magenta", "bg_white", "bg_black", "bg_default", "bg_negative"); } /** * Returns a list of all emphasis names. * * @return The names. */ public static List emphasisNames() { return List.of("italic", "bold", "faint", "plain", "underline", "strikethrough", "negative", "conceal", "blink"); } /** * Returns a style composed from the given names, or {@link #none} if empty. * * @param names The names. * @return The style. */ public static Style of(String... names) { if (names.length == 0) { return NONE; } else if (names.length == 1) { return Style.named(names[0]); } else { return new StyleList(names); } } /** * Returns a style from the given color and attributes. * * @param color The color. * @param background {@code true} if background color. * @param bright {@code true} if bright color. * @return The style. */ public static Style of(Color color, boolean background, boolean bright) { return new Hue(color, background, bright); } /** * Returns a style composed from the given attributes, or {@link #none} if empty. * * @param attributes The attributes. * @return The style. */ public static Style of(Attribute... attributes) { if (attributes.length == 0) { return NONE; } else if (attributes.length == 1) { return new Emphasis(attributes[0]); } else { return new StyleList(attributes); } } /** * Returns a style composed from the given styles, or {@link #none} if empty. * * @param styles The styles. * @return The style. */ public static Style of(Style... styles) { if (styles.length == 0) { return NONE; } else if (styles.length == 1) { return styles[0]; } else { return new StyleList(styles); } } /** * Tests whether or not the given text contains an Ansi escape sequence. * * @param text The text. * @return {@code true} if an Ansi escape sequence found. */ public static boolean isStyled(String text) { return text != null && text.contains(ANSI_ESCAPE_BEGIN); } /** * Strips any styles from the given string. * * @param input The string. * @return The stripped string. */ public static String strip(String input) { AnsiState state = AnsiState.ESC1; StringBuilder sb = new StringBuilder(); char[] buffer = new char[100]; int pos = 0; int index; for (index = 0; index < input.length(); index++) { char c = input.charAt(index); switch (state) { case ESC1: if (c == ESC_CH1) { buffer[pos++] = c; state = AnsiState.ESC2; } else { sb.append(c); } break; case ESC2: buffer[pos++] = c; if (c == ESC_CH2) { state = AnsiState.NEXT_ARG; } else if (c == CMD_CH2) { state = AnsiState.CMD; } else if (c == CHARSET0_CH2) { state = AnsiState.CHARSET; } else if (c == CHARSET1_CH2) { state = AnsiState.CHARSET; } else { sb.append(buffer, 0, pos); pos = 0; state = AnsiState.ESC1; } break; case NEXT_ARG: buffer[pos++] = c; if ('"' == c) { state = AnsiState.STR_ARG_END; } else if ('0' <= c && c <= '9') { state = AnsiState.INT_ARG_END; } else if (c != ';' && c != '?' && c != '=') { pos = 0; state = AnsiState.ESC1; } break; case INT_ARG_END: buffer[pos++] = c; if (!('0' <= c && c <= '9')) { if (c == ';') { state = AnsiState.NEXT_ARG; } else { pos = 0; state = AnsiState.ESC1; } } break; case STR_ARG_END: buffer[pos++] = c; if ('"' != c) { if (c == ';') { state = AnsiState.NEXT_ARG; } else { pos = 0; state = AnsiState.ESC1; } } break; case CMD: buffer[pos++] = c; if ('0' <= c && c <= '9') { state = AnsiState.CMD_END; } else { sb.append(buffer, 0, pos); pos = 0; state = AnsiState.ESC1; } break; case CMD_END: buffer[pos++] = c; if (';' == c) { state = AnsiState.CMD_PARAM; } else if (!('0' <= c && c <= '9')) { // oops, did not expect this sb.append(buffer, 0, pos); pos = 0; state = AnsiState.ESC1; } break; case CMD_PARAM: buffer[pos++] = c; if (BEL == c) { pos = 0; state = AnsiState.ESC1; } else if (ESC_CH1 == c) { state = AnsiState.ST; } break; case ST: buffer[pos++] = c; if (ST_CH2 == c) { pos = 0; state = AnsiState.ESC1; } else { state = AnsiState.CMD_PARAM; } break; case CHARSET: pos = 0; state = AnsiState.ESC1; break; default: break; } // Is it just too long? if (index >= buffer.length) { sb.append(buffer, 0, pos); pos = 0; state = AnsiState.ESC1; } } return sb.toString(); } /** * Log styles either as a complete list (including aliases) or a summary table. * * @param args The arguments: {@code --list | --table}. Defaults to table. */ public static void main(String... args) { boolean list = false; if (args.length == 1) { if (args[0].equals("--list")) { list = true; } else if (!args[0].equals("--table")) { throw new IllegalArgumentException("Unknown argument: " + args[0]); } } if (list) { styles().forEach((name, style) -> Log.info("%14s [ %s ]", name, style.apply("example"))); } else { logSummaryTables(); } } /** * Log a summary tables of text and background colors and styles. */ public static void logSummaryTables() { Log.info(); logTextSummaryTable(); Log.info(); logBackgroundSummaryTable(); Log.info(); } /** * Log a summary table of text colors and styles. */ public static void logTextSummaryTable() { logTable(colorNames(), false); } /** * Log a summary table of background colors and styles. */ public static void logBackgroundSummaryTable() { logTable(backgroundColorNames(), true); } private static void logTable(List names, boolean background) { String header = background ? "Background Color" : "Text Color"; String example = " Example 1234 !@#$% "; String rowFormat = "│ %-19s│ %22s │ %22s │ %22s │ %22s │"; Log.info("┌────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬───────────" + "───────────┐"); Log.info("│ %-19s│ Plain │ Italic │ Bold │ Italic & Bold │", header); Log.info("├────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼───────────" + "───────────┤"); names.forEach(name -> { String textColor = background ? "default" : name; String backgroundColor = background ? name : "bg_default"; String textColorBright = background ? textColor : textColor + "!"; String backgroundColorBright = background ? backgroundColor + "!" : backgroundColor; String plain = Style.of(backgroundColor, textColor).apply(example); String italic = Style.of(backgroundColor, textColor, "italic").apply(example); String bold = Style.of(backgroundColor, textColor, "bold").apply(example); String italicBold = Style.of(backgroundColor, textColor, "ITALIC").apply(example); String plainBright = Style.of(backgroundColorBright, textColorBright).apply(example); String italicBright = Style.of(backgroundColorBright, textColorBright, "italic").apply(example); String boldBright = Style.of(backgroundColorBright, textColorBright, "bold").apply(example); String italicBoldBright = Style.of(backgroundColorBright, textColorBright, "ITALIC").apply(example); Log.info(rowFormat, name, plain, italic, bold, italicBold); Log.info(rowFormat, name + "!", plainBright, italicBright, boldBright, italicBoldBright); }); Log.info("└────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴────────────" + "──────────┘"); } /** * Returns this style applied to the given text. * * @param text The text. * @return The new text. */ public String apply(Object text) { return apply(ansi()).a(text).reset().toString(); } /** * Applies this style to the given ansi instance. * * @param ansi The instance. * @return The instance, for chaining. */ public Ansi apply(Ansi ansi) { return ansi; } /** * Reset an ansi instance. * * @param ansi The instance. * @return The instance, for chaining. */ public Ansi reset(Ansi ansi) { return ansi; } @Override public String toString() { return "none"; } static class StyleList extends Style { private final List