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}