
io.helidon.build.util.Style Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2020, 2022 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[1024];
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