1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package dev.metaschema.cli.processor.ansi;
7
8 import java.util.concurrent.atomic.AtomicBoolean;
9
10 import edu.umd.cs.findbugs.annotations.NonNull;
11
12 /**
13 * A minimal ANSI escape-code builder used by the CLI to emit colored and
14 * formatted terminal output.
15 * <p>
16 * Emits a subset of
17 * <a href= "https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">Select
18 * Graphic Rendition</a> codes for foreground colors, bold, and reset. When
19 * globally disabled via {@link #setEnabled(boolean)}, escape sequences are
20 * suppressed while appended literal text is preserved so output remains
21 * readable on terminals that do not interpret ANSI codes.
22 */
23 public final class Ansi {
24 private static final String ESC = "\u001B[";
25 private static final String RESET_SEQ = ESC + "0m";
26 private static final String BOLD_SEQ = ESC + "1m";
27 private static final String BOLD_OFF_SEQ = ESC + "22m";
28
29 private static final AtomicBoolean ENABLED = new AtomicBoolean(true);
30
31 @NonNull
32 private final StringBuilder buffer = new StringBuilder();
33
34 private Ansi() {
35 // use ansi() factory
36 }
37
38 /**
39 * Create a new builder.
40 *
41 * @return a fresh builder with empty contents
42 */
43 @NonNull
44 public static Ansi ansi() {
45 return new Ansi();
46 }
47
48 /**
49 * Globally enable or disable emission of ANSI escape codes.
50 * <p>
51 * When disabled, all color and style methods are no-ops; literal appended text
52 * is still emitted.
53 *
54 * @param enable
55 * {@code true} to emit escape sequences, {@code false} to suppress
56 * them
57 */
58 public static void setEnabled(boolean enable) {
59 ENABLED.set(enable);
60 }
61
62 /**
63 * Indicates whether ANSI escape code emission is currently enabled.
64 *
65 * @return {@code true} if enabled
66 */
67 public static boolean isEnabled() {
68 return ENABLED.get();
69 }
70
71 @NonNull
72 private Ansi emit(@NonNull String sequence) {
73 if (ENABLED.get()) {
74 buffer.append(sequence);
75 }
76 return this;
77 }
78
79 /**
80 * Append a single literal character.
81 *
82 * @param ch
83 * the character to append
84 * @return {@code this} for chaining
85 */
86 @NonNull
87 public Ansi a(char ch) {
88 buffer.append(ch);
89 return this;
90 }
91
92 /**
93 * Append a literal string.
94 *
95 * @param text
96 * the text to append
97 * @return {@code this} for chaining
98 */
99 @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 }