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 }