001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.cli.processor.ansi; 007 008import java.util.concurrent.atomic.AtomicBoolean; 009 010import edu.umd.cs.findbugs.annotations.NonNull; 011 012/** 013 * A minimal ANSI escape-code builder used by the CLI to emit colored and 014 * formatted terminal output. 015 * <p> 016 * Emits a subset of 017 * <a href= "https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">Select 018 * Graphic Rendition</a> codes for foreground colors, bold, and reset. When 019 * globally disabled via {@link #setEnabled(boolean)}, escape sequences are 020 * suppressed while appended literal text is preserved so output remains 021 * readable on terminals that do not interpret ANSI codes. 022 */ 023public final class Ansi { 024 private static final String ESC = "\u001B["; 025 private static final String RESET_SEQ = ESC + "0m"; 026 private static final String BOLD_SEQ = ESC + "1m"; 027 private static final String BOLD_OFF_SEQ = ESC + "22m"; 028 029 private static final AtomicBoolean ENABLED = new AtomicBoolean(true); 030 031 @NonNull 032 private final StringBuilder buffer = new StringBuilder(); 033 034 private Ansi() { 035 // use ansi() factory 036 } 037 038 /** 039 * Create a new builder. 040 * 041 * @return a fresh builder with empty contents 042 */ 043 @NonNull 044 public static Ansi ansi() { 045 return new Ansi(); 046 } 047 048 /** 049 * Globally enable or disable emission of ANSI escape codes. 050 * <p> 051 * When disabled, all color and style methods are no-ops; literal appended text 052 * is still emitted. 053 * 054 * @param enable 055 * {@code true} to emit escape sequences, {@code false} to suppress 056 * them 057 */ 058 public static void setEnabled(boolean enable) { 059 ENABLED.set(enable); 060 } 061 062 /** 063 * Indicates whether ANSI escape code emission is currently enabled. 064 * 065 * @return {@code true} if enabled 066 */ 067 public static boolean isEnabled() { 068 return ENABLED.get(); 069 } 070 071 @NonNull 072 private Ansi emit(@NonNull String sequence) { 073 if (ENABLED.get()) { 074 buffer.append(sequence); 075 } 076 return this; 077 } 078 079 /** 080 * Append a single literal character. 081 * 082 * @param ch 083 * the character to append 084 * @return {@code this} for chaining 085 */ 086 @NonNull 087 public Ansi a(char ch) { 088 buffer.append(ch); 089 return this; 090 } 091 092 /** 093 * Append a literal string. 094 * 095 * @param text 096 * the text to append 097 * @return {@code this} for chaining 098 */ 099 @NonNull 100 public Ansi a(@NonNull CharSequence text) { 101 buffer.append(text); 102 return this; 103 } 104 105 /** 106 * Append formatted text using {@link String#format(String, Object...)} 107 * semantics. 108 * 109 * @param format 110 * the format string 111 * @param args 112 * the format arguments 113 * @return {@code this} for chaining 114 */ 115 @NonNull 116 public Ansi format(@NonNull String format, Object... args) { 117 buffer.append(String.format(format, args)); 118 return this; 119 } 120 121 /** 122 * Emit the ANSI reset sequence, clearing any active color or style. 123 * 124 * @return {@code this} for chaining 125 */ 126 @NonNull 127 public Ansi reset() { 128 return emit(RESET_SEQ); 129 } 130 131 /** 132 * Enable bold rendering for subsequent appended text. 133 * 134 * @return {@code this} for chaining 135 */ 136 @NonNull 137 public Ansi bold() { 138 return emit(BOLD_SEQ); 139 } 140 141 /** 142 * Disable bold rendering for subsequent appended text. 143 * 144 * @return {@code this} for chaining 145 */ 146 @NonNull 147 public Ansi boldOff() { 148 return emit(BOLD_OFF_SEQ); 149 } 150 151 /** 152 * Set the foreground color to red. 153 * 154 * @return {@code this} for chaining 155 */ 156 @NonNull 157 public Ansi fgRed() { 158 return emit(ESC + "31m"); 159 } 160 161 /** 162 * Set the foreground color to bright red. 163 * 164 * @return {@code this} for chaining 165 */ 166 @NonNull 167 public Ansi fgBrightRed() { 168 return emit(ESC + "91m"); 169 } 170 171 /** 172 * Set the foreground color to bright yellow. 173 * 174 * @return {@code this} for chaining 175 */ 176 @NonNull 177 public Ansi fgBrightYellow() { 178 return emit(ESC + "93m"); 179 } 180 181 /** 182 * Set the foreground color to bright blue. 183 * 184 * @return {@code this} for chaining 185 */ 186 @NonNull 187 public Ansi fgBrightBlue() { 188 return emit(ESC + "94m"); 189 } 190 191 /** 192 * Set the foreground color to bright cyan. 193 * 194 * @return {@code this} for chaining 195 */ 196 @NonNull 197 public Ansi fgBrightCyan() { 198 return emit(ESC + "96m"); 199 } 200 201 /** 202 * Set the foreground color to the bright variant of the supplied color. 203 * 204 * @param color 205 * the base color 206 * @return {@code this} for chaining 207 */ 208 @NonNull 209 public Ansi fgBright(@NonNull Color color) { 210 return emit(ESC + (90 + color.ordinal()) + "m"); 211 } 212 213 @Override 214 public String toString() { 215 return buffer.toString(); 216 } 217 218 /** 219 * Standard 8 ANSI foreground colors. Ordinals align with the standard color 220 * codes (0-7) so bright variants are derived by adding 90. 221 */ 222 public enum Color { 223 /** Black (code 30, bright 90). */ 224 BLACK, 225 /** Red (code 31, bright 91). */ 226 RED, 227 /** Green (code 32, bright 92). */ 228 GREEN, 229 /** Yellow (code 33, bright 93). */ 230 YELLOW, 231 /** Blue (code 34, bright 94). */ 232 BLUE, 233 /** Magenta (code 35, bright 95). */ 234 MAGENTA, 235 /** Cyan (code 36, bright 96). */ 236 CYAN, 237 /** White (code 37, bright 97). */ 238 WHITE; 239 } 240}