diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/AnsiColorFormatter.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/AnsiColorFormatter.java index 8a812fa54bb..dbd9a66fa8c 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/AnsiColorFormatter.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/AnsiColorFormatter.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.Objects; -import java.util.function.Consumer; /** * Styles text using ANSI color codes. @@ -29,91 +28,51 @@ public enum AnsiColorFormatter implements ColorFormatter { * Does not write any color. */ NO_COLOR { - @Override - public String style(String text, Style... styles) { - return text; - } - - @Override - public void style(Appendable appendable, String text, Style... styles) { - try { - appendable.append(text); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - @Override public boolean isColorEnabled() { return false; } @Override - public void style(T appendable, Consumer consumer, Style... styles) { - consumer.accept(appendable); - } + public void startStyle(Appendable appendable, Style... style) { } + + @Override + public void endStyle(Appendable appendable) { } }, /** * Writes with ANSI colors. */ FORCE_COLOR { - @Override - public String style(String text, Style... styles) { - StringBuilder builder = new StringBuilder(); - style(builder, text, styles); - return builder.toString(); - } - - @Override - public void style(Appendable appendable, String text, Style... styles) { - try { - startStyle(appendable, styles); - appendable.append(text); - } catch (IOException e) { - throw new UncheckedIOException(e); - } finally { - closeStyle(appendable, styles); - } - } - @Override public boolean isColorEnabled() { return true; } @Override - public void style(T appendable, Consumer consumer, Style... styles) { - try { - startStyle(appendable, styles); - consumer.accept(appendable); - } catch (IOException e) { - throw new UncheckedIOException(e); - } finally { - closeStyle(appendable, styles); - } - } - - private void startStyle(Appendable appendable, Style... styles) throws IOException { + public void startStyle(Appendable appendable, Style... styles) { if (styles.length > 0) { - appendable.append("\033["); - boolean isAfterFirst = false; - for (Style style : styles) { - if (isAfterFirst) { - appendable.append(';'); + try { + appendable.append("\033["); + boolean isAfterFirst = false; + for (Style style : styles) { + if (isAfterFirst) { + appendable.append(';'); + } + appendable.append(style.getAnsiColorCode()); + isAfterFirst = true; } - appendable.append(style.getAnsiColorCode()); - isAfterFirst = true; + appendable.append('m'); + } catch (IOException e) { + throw new UncheckedIOException(e); } - appendable.append('m'); } } - private void closeStyle(Appendable appendable, Style... styles) { + @Override + public void endStyle(Appendable appendable) { try { - if (styles.length > 0) { - appendable.append("\033[0m"); - } + appendable.append("\033[0m"); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -131,6 +90,11 @@ public String style(String text, Style... styles) { return delegate.style(text, styles); } + @Override + public void println(Appendable appendable, String text, Style... styles) { + delegate.println(appendable, text, styles); + } + @Override public void style(Appendable appendable, String text, Style... styles) { delegate.style(appendable, text, styles); @@ -142,8 +106,13 @@ public boolean isColorEnabled() { } @Override - public void style(T appendable, Consumer consumer, Style... styles) { - delegate.style(appendable, consumer, styles); + public void startStyle(Appendable appendable, Style... style) { + delegate.startStyle(appendable, style); + } + + @Override + public void endStyle(Appendable appendable) { + delegate.endStyle(appendable); } }; diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/Cli.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/Cli.java index 7527c1f8699..111086cdd45 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/Cli.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/Cli.java @@ -17,7 +17,6 @@ import java.io.PrintWriter; import java.io.StringWriter; -import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -111,10 +110,10 @@ public void stderr(CliPrinter stderrPrinter) { } private void printException(Throwable e, boolean stacktrace) { - if (!stacktrace) { - colorFormatter.println(stderrPrinter, e.getMessage(), Style.RED); - } else { - try (ColorFormatter.PrinterBuffer buffer = colorFormatter.printerBuffer(stderrPrinter)) { + try (ColorBuffer buffer = ColorBuffer.of(colorFormatter, stderrPrinter)) { + if (!stacktrace) { + colorFormatter.println(stderrPrinter, e.getMessage(), Style.RED); + } else { StringWriter writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); String result = writer.toString(); @@ -143,8 +142,18 @@ public boolean isColorEnabled() { } @Override - public void style(T appendable, Consumer consumer, Style... styles) { - delegateSupplier.get().style(appendable, consumer, styles); + public void println(Appendable appendable, String text, Style... styles) { + delegateSupplier.get().println(appendable, text, styles); + } + + @Override + public void startStyle(Appendable appendable, Style... style) { + delegateSupplier.get().startStyle(appendable, style); + } + + @Override + public void endStyle(Appendable appendable) { + delegateSupplier.get().endStyle(appendable); } }; } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/CliPrinter.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/CliPrinter.java index dbbd3d941fd..35c49c7e8f3 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/CliPrinter.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/CliPrinter.java @@ -15,11 +15,11 @@ package software.amazon.smithy.cli; -import java.io.BufferedWriter; import java.io.Flushable; +import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.PrintWriter; +import java.io.UncheckedIOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -27,22 +27,31 @@ * Handles text output of the CLI. */ @FunctionalInterface -public interface CliPrinter extends Flushable { +public interface CliPrinter extends Appendable, Flushable { - /** - * Prints text to the writer. - * - * @param text Text to write. - */ - void print(String text); + @Override + CliPrinter append(char c); + + @Override + default CliPrinter append(CharSequence csq) { + return append(csq, 0, csq.length()); + } + + @Override + default CliPrinter append(CharSequence csq, int start, int end) { + for (int i = start; i < end; i++) { + append(csq.charAt(i)); + } + return this; + } /** * Prints text to the writer and appends a new line. * * @param text Text to print. */ - default void println(String text) { - print(text + System.lineSeparator()); + default CliPrinter println(String text) { + return append(text + System.lineSeparator()); } /** @@ -51,39 +60,54 @@ default void println(String text) { default void flush() {} /** - * Create a new CliPrinter from a PrintWriter. + * Create a new CliPrinter from an OutputStream. * - * @param printWriter PrintWriter to write to. + * @param stream OutputStream to write to. * @return Returns the created CliPrinter. */ - static CliPrinter fromPrintWriter(PrintWriter printWriter) { + static CliPrinter fromOutputStream(OutputStream stream) { + Charset charset = StandardCharsets.UTF_8; + OutputStreamWriter writer = new OutputStreamWriter(stream, charset); + return new CliPrinter() { @Override - public void println(String text) { - printWriter.println(text); + public CliPrinter append(char c) { + try { + writer.append(c); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; } @Override - public void print(String text) { - printWriter.print(text); + public CliPrinter append(CharSequence csq) { + try { + writer.append(csq); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + @Override + public CliPrinter append(CharSequence csq, int start, int end) { + try { + writer.append(csq, start, end); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; } @Override public void flush() { - printWriter.flush(); + try { + writer.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } }; } - - /** - * Create a new CliPrinter from an OutputStream. - * - * @param stream OutputStream to write to. - * @return Returns the created CliPrinter. - */ - static CliPrinter fromOutputStream(OutputStream stream) { - Charset charset = StandardCharsets.UTF_8; - PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(stream, charset)), false); - return fromPrintWriter(writer); - } } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorBuffer.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorBuffer.java new file mode 100644 index 00000000000..7cd0a9a06f2 --- /dev/null +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorBuffer.java @@ -0,0 +1,155 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.cli; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * An {@link Appendable} that supports color provided by a {@link ColorFormatter}. + * + *

A {@code ColorBuffer} is not thread-safe and is meant to write short contiguous output text that will + * eventually be written to other things like a {@link CliPrinter}. + * + *

When wrapping a {@link CliPrinter}, ensure you use {@link #close()} to write to the printer. Alternatively, + * wrap the buffer in a try-with-resources block. + */ +public final class ColorBuffer implements Appendable, AutoCloseable { + private final ColorFormatter colors; + private final Appendable buffer; + private final Consumer closer; + + private ColorBuffer(ColorFormatter colors, Appendable buffer, Consumer closer) { + this.colors = Objects.requireNonNull(colors); + this.buffer = Objects.requireNonNull(buffer); + this.closer = Objects.requireNonNull(closer); + } + + /** + * Create a new ColorBuffer that directly writes to the given {@code sink}. + * + *

No additional buffering is used when buffering over an {@code Appendable}. Each call to write to the + * buffer will write to the appendable. + * + * @param colors ColorFormatter used to provide colors and style. + * @param sink Where to write. + * @return Returns the created buffer. + */ + public static ColorBuffer of(ColorFormatter colors, Appendable sink) { + return new ColorBuffer(colors, sink, s -> { }); + } + + /** + * Create a new ColorBuffer that stores all output to an internal buffer and only writes to the given + * {@link CliPrinter} when {@link #close()} is called. + * + * @param colors ColorFormatter used to provide colors and style. + * @param sink Where to write. + * @return Returns the created buffer. + */ + public static ColorBuffer of(ColorFormatter colors, CliPrinter sink) { + StringBuilder buffer = new StringBuilder(); + return new ColorBuffer(colors, buffer, s -> { + sink.append(s.toString()); + }); + } + + @Override + public String toString() { + return buffer.toString(); + } + + @Override + public ColorBuffer append(CharSequence csq) { + try { + buffer.append(csq); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + @Override + public ColorBuffer append(CharSequence csq, int start, int end) { + try { + buffer.append(csq, start, end); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + @Override + public ColorBuffer append(char c) { + try { + buffer.append(c); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + /** + * Writes styled text to the builder using the CliPrinter's color settings. + * + * @param text Text to write. + * @param styles Styles to apply to the text. + * @return Returns self. + */ + public ColorBuffer print(String text, Style... styles) { + colors.style(buffer, text, styles); + return this; + } + + /** + * Prints a line of styled text to the buffer. + * + * @param text Text to print. + * @param styles Styles to apply. + * @return Returns self. + */ + public ColorBuffer println(String text, Style... styles) { + return print(text, styles).append(System.lineSeparator()); + } + + /** + * Writes a system-dependent new line. + * + * @return Returns the buffer. + */ + public ColorBuffer println() { + return append(System.lineSeparator()); + } + + public ColorBuffer style(Consumer bufferConsumer, Style... styles) { + try { + colors.startStyle(buffer, styles); + bufferConsumer.accept(this); + return this; + } finally { + if (styles.length > 0) { + colors.endStyle(buffer); + } + } + } + + @Override + public void close() { + closer.accept(buffer); + } +} diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorFormatter.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorFormatter.java index f0ccbf4e928..65140c66492 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorFormatter.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorFormatter.java @@ -15,7 +15,8 @@ package software.amazon.smithy.cli; -import java.util.function.Consumer; +import java.io.IOException; +import java.io.UncheckedIOException; /** * Styles text using color codes. @@ -23,6 +24,7 @@ * @see AnsiColorFormatter for the ANSI implementation. */ public interface ColorFormatter { + /** * Styles text using the given styles. * @@ -30,7 +32,15 @@ public interface ColorFormatter { * @param styles Styles to apply. * @return Returns the styled text. */ - String style(String text, Style... styles); + default String style(String text, Style... styles) { + if (!isColorEnabled()) { + return text; + } else { + StringBuilder builder = new StringBuilder(); + style(builder, text, styles); + return builder.toString(); + } + } /** * Styles text using the given styles and writes it to an Appendable. @@ -39,141 +49,36 @@ public interface ColorFormatter { * @param text Text to write. * @param styles Styles to apply. */ - void style(Appendable appendable, String text, Style... styles); - - /** - * Adds styles around text written to an appendable by a consumer. - * - * @param appendable Appendable to write to. - * @param consumer Consumer to write to the appendable using style. - * @param styles Styles to apply to the text written by the consumer. - * @param Appendable type. - */ - void style(T appendable, Consumer consumer, Style... styles); - - /** - * @return Returns true if this formatter supports color output. - */ - boolean isColorEnabled(); + default void style(Appendable appendable, String text, Style... styles) { + try { + startStyle(appendable, styles); + appendable.append(text); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + if (styles.length > 0) { + endStyle(appendable); + } + } + } /** - * Print a styled line of text to the given {@code printer}. + * Print a styled line of text to the given {@code appendable}. * - * @param printer Printer to write to. + * @param appendable Where to write. * @param text Text to write. * @param styles Styles to apply. */ - default void println(CliPrinter printer, String text, Style... styles) { - printer.println(style(text, styles)); + default void println(Appendable appendable, String text, Style... styles) { + style(appendable, text + System.lineSeparator(), styles); } /** - * Creates a {@link PrinterBuffer} used to build up a long string of styled text. - * - *

Call {@link PrinterBuffer#close()} or use try-with-resources to write to the printer. - * - * @return Returns the buffer. - */ - default PrinterBuffer printerBuffer(CliPrinter printer) { - return new PrinterBuffer(this, printer); - } - - /** - * A buffer associated with a {@link CliPrinter} used to build up a string of colored text. - * - *

Use {@link #close()} to write to the associated {@link CliPrinter}, or wrap the buffer in a - * try-with-resources block. + * @return Returns true if this formatter supports color output. */ - final class PrinterBuffer implements Appendable, AutoCloseable { - private final ColorFormatter colors; - private final CliPrinter printer; - private final StringBuilder builder = new StringBuilder(); - private boolean pendingNewline; - private final String newline = System.lineSeparator(); - - private PrinterBuffer(ColorFormatter colors, CliPrinter printer) { - this.colors = colors; - this.printer = printer; - } - - @Override - public String toString() { - String result = builder.toString(); - if (pendingNewline) { - result += newline; - } - return result; - } - - @Override - public PrinterBuffer append(CharSequence csq) { - handleNewline(); - builder.append(csq); - return this; - } - - private void handleNewline() { - if (pendingNewline) { - builder.append(newline); - pendingNewline = false; - } - } - - @Override - public PrinterBuffer append(CharSequence csq, int start, int end) { - handleNewline(); - builder.append(csq, start, end); - return this; - } - - @Override - public PrinterBuffer append(char c) { - handleNewline(); - builder.append(c); - return this; - } - - /** - * Writes styled text to the builder using the CliPrinter's color settings. - * - * @param text Text to write. - * @param styles Styles to apply to the text. - * @return Returns self. - */ - public PrinterBuffer print(String text, Style... styles) { - handleNewline(); - colors.style(this, text, styles); - return this; - } - - /** - * Prints a line of styled text to the buffer. - * - * @param text Text to print. - * @param styles Styles to apply. - * @return Returns self. - */ - public PrinterBuffer println(String text, Style... styles) { - print(text, styles); - return println(); - } + boolean isColorEnabled(); - /** - * Writes a system-dependent new line. - * - * @return Returns the buffer. - */ - public PrinterBuffer println() { - handleNewline(); - pendingNewline = true; - return this; - } + void startStyle(Appendable appendable, Style... style); - @Override - public void close() { - printer.println(builder.toString()); - builder.setLength(0); - pendingNewline = false; - } - } + void endStyle(Appendable appendable); } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java index fbf18254c87..38eb38dd402 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java @@ -35,6 +35,7 @@ import software.amazon.smithy.cli.Arguments; import software.amazon.smithy.cli.CliError; import software.amazon.smithy.cli.CliPrinter; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.ColorFormatter; import software.amazon.smithy.cli.Command; import software.amazon.smithy.cli.HelpPrinter; @@ -200,44 +201,40 @@ public void accept(String name, Throwable exception) { @Override public void accept(ProjectionResult result) { - try (ColorFormatter.PrinterBuffer buffer = colors.printerBuffer(printer)) { - printProjectionResult(buffer, result); - } - } - - private void printProjectionResult(ColorFormatter.PrinterBuffer buffer, ProjectionResult result) { - if (result.isBroken()) { - // Write out validation errors as they occur. - failedProjections.add(result.getProjectionName()); - buffer - .println() - .print(result.getProjectionName(), Style.RED) - .println(" has a model that failed validation"); - result.getEvents().forEach(event -> { - if (event.getSeverity() == Severity.DANGER || event.getSeverity() == Severity.ERROR) { - buffer.println(event.toString(), Style.RED); - } - }); - } else { - // Only increment the projection count if it succeeded. - projectionCount.incrementAndGet(); - } - - pluginCount.addAndGet(result.getPluginManifests().size()); - - // Get the base directory of the projection. - Iterator manifestIterator = result.getPluginManifests().values().iterator(); - Path root = manifestIterator.hasNext() ? manifestIterator.next().getBaseDir().getParent() : null; - - if (!quiet) { - String message = String.format("Completed projection %s (%d shapes): %s", - result.getProjectionName(), result.getModel().toSet().size(), root); - buffer.println(message, Style.GREEN); - } - - // Increment the total number of artifacts written. - for (FileManifest manifest : result.getPluginManifests().values()) { - artifactCount.addAndGet(manifest.getFiles().size()); + try (ColorBuffer buffer = ColorBuffer.of(colors, printer)) { + if (result.isBroken()) { + // Write out validation errors as they occur. + failedProjections.add(result.getProjectionName()); + buffer + .println() + .print(result.getProjectionName(), Style.RED) + .println(" has a model that failed validation"); + result.getEvents().forEach(event -> { + if (event.getSeverity() == Severity.DANGER || event.getSeverity() == Severity.ERROR) { + buffer.println(event.toString(), Style.RED); + } + }); + } else { + // Only increment the projection count if it succeeded. + projectionCount.incrementAndGet(); + } + + pluginCount.addAndGet(result.getPluginManifests().size()); + + // Get the base directory of the projection. + Iterator manifestIterator = result.getPluginManifests().values().iterator(); + Path root = manifestIterator.hasNext() ? manifestIterator.next().getBaseDir().getParent() : null; + + if (!quiet) { + String message = String.format("Completed projection %s (%d shapes): %s", + result.getProjectionName(), result.getModel().toSet().size(), root); + buffer.println(message, Style.GREEN); + } + + // Increment the total number of artifacts written. + for (FileManifest manifest : result.getPluginManifests().values()) { + artifactCount.addAndGet(manifest.getFiles().size()); + } } } } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CodeFormatter.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CodeFormatter.java index 22eca4e4703..e64b08f0f3c 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CodeFormatter.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CodeFormatter.java @@ -15,11 +15,9 @@ package software.amazon.smithy.cli.commands; -import java.io.IOException; -import java.io.UncheckedIOException; import java.util.Collection; import java.util.Iterator; -import software.amazon.smithy.cli.ColorFormatter; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.Style; import software.amazon.smithy.model.loader.sourcecontext.SourceContextLoader; @@ -28,13 +26,11 @@ */ final class CodeFormatter { - private final Appendable writer; - private final ColorFormatter colors; + private final ColorBuffer writer; private final int maxWidth; - CodeFormatter(Appendable writer, ColorFormatter colors, int maxWidth) { + CodeFormatter(ColorBuffer writer, int maxWidth) { this.writer = writer; - this.colors = colors; this.maxWidth = maxWidth; } @@ -46,89 +42,67 @@ void writeCode(int cursorLine, int cursorColumn, Collection lineIterator = lines.iterator(); - int lastLine = -1; + Iterator lineIterator = lines.iterator(); + int lastLine = -1; - while (lineIterator.hasNext()) { - SourceContextLoader.Line line = lineIterator.next(); + while (lineIterator.hasNext()) { + SourceContextLoader.Line line = lineIterator.next(); - if (line.getLineNumber() != lastLine + 1 && lastLine != -1) { - writeColumnAndContent(numberLength, -1, ""); - } - - writeColumnAndContent(numberLength, line.getLineNumber(), line.getContent()); + if (line.getLineNumber() != lastLine + 1 && lastLine != -1) { + writeColumnAndContent(numberLength, -1, ""); + } - if (line.getLineNumber() == cursorLine) { - writePointer(numberLength, cursorColumn); - } + writeColumnAndContent(numberLength, line.getLineNumber(), line.getContent()); - lastLine = line.getLineNumber(); + if (line.getLineNumber() == cursorLine) { + writePointer(numberLength, cursorColumn); } - writer.append(System.lineSeparator()); - } catch (IOException e) { - throw new UncheckedIOException("Error write source code: " + e.getMessage(), e); + lastLine = line.getLineNumber(); } + + writer.append(System.lineSeparator()); } - private void writeColumnAndContent(int numberLength, int lineNumber, CharSequence content) throws IOException { - if (lineNumber == -1) { - colors.style(writer, w -> { - try { - for (int i = 0; i < numberLength; i++) { - w.append("·"); - } - w.append("|"); - } catch (IOException e) { - throw new UncheckedIOException(e); + private void writeColumnAndContent(int numberLength, int lineNumber, CharSequence content) { + writer.style(w -> { + if (lineNumber == -1) { + for (int i = 0; i < numberLength; i++) { + writer.append("·"); } - }, Style.BRIGHT_BLACK); - } else { - colors.style(writer, w -> { - try { - String lineString = String.valueOf(lineNumber); - int thisLineLength = lineString.length(); - writer.append(lineString); - // Write the appropriate amount of padding. - for (int i = 0; i < numberLength - thisLineLength; i++) { - writer.append(' '); - } - colors.style(writer, "| ", Style.BRIGHT_BLACK); - } catch (IOException e) { - throw new UncheckedIOException(e); + writer.append("|"); + } else { + String lineString = String.valueOf(lineNumber); + int thisLineLength = lineString.length(); + writer.append(lineString); + // Write the appropriate amount of padding. + for (int i = 0; i < numberLength - thisLineLength; i++) { + writer.append(' '); } - }, Style.BRIGHT_BLACK); - } + writer.append("| "); + } + }, Style.BRIGHT_BLACK); if (content.length() > 0) { writeStringWithMaxWidth(content, numberLength); } - writer.append(System.lineSeparator()); + writer.println(); } private void writePointer(int numberLength, int cursorColumn) { - colors.style(writer, w -> { - try { - for (int j = 0; j < numberLength; j++) { - writer.append(' '); - } - writer.append("|"); - } catch (IOException e) { - throw new UncheckedIOException(e); + writer.style(w -> { + for (int j = 0; j < numberLength; j++) { + w.append(' '); } + w.append("|"); }, Style.BRIGHT_BLACK); - try { - for (int j = 0; j < cursorColumn; j++) { - writer.append(' '); - } - colors.style(writer, "^", Style.RED); - writer.append(System.lineSeparator()); - } catch (IOException e) { - throw new UncheckedIOException(e); + for (int j = 0; j < cursorColumn; j++) { + writer.append(' '); } + writer.print("^", Style.RED); + writer.println(); } private int findLongestNumber(Collection lines) { @@ -139,7 +113,7 @@ private int findLongestNumber(Collection lines) { return String.valueOf(maxLineNumber).length(); } - private void writeStringWithMaxWidth(CharSequence line, int offsetSize) throws IOException { + private void writeStringWithMaxWidth(CharSequence line, int offsetSize) { int allowedSize = maxWidth - offsetSize; writer.append(line, 0, Math.min(line.length(), allowedSize)); if (line.length() >= allowedSize) { diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java index 9ad7ba59841..26d341b3fcf 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java @@ -24,7 +24,7 @@ import software.amazon.smithy.cli.ArgumentReceiver; import software.amazon.smithy.cli.Arguments; import software.amazon.smithy.cli.CliError; -import software.amazon.smithy.cli.ColorFormatter; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.Command; import software.amazon.smithy.cli.HelpPrinter; import software.amazon.smithy.cli.StandardOptions; @@ -140,7 +140,7 @@ int runWithClassLoader(SmithyBuildConfig config, Arguments arguments, Env env) { // Print the "framing" style output to stderr only if !quiet. if (!standardOptions.quiet()) { - try (ColorFormatter.PrinterBuffer buffer = env.colors().printerBuffer(env.stderr())) { + try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stderr())) { if (hasDanger) { buffer.println("Smithy diff detected danger", Style.BRIGHT_RED, Style.BOLD); } else if (hasWarning) { diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java index 2495541cd9c..5cbe13d9115 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java @@ -91,10 +91,8 @@ public String getSummary() { @Override public int execute(Arguments arguments, Env env) { if (!arguments.getReceiver(StandardOptions.class).quiet()) { - String warning = env.colors().style( - "upgrade-1-to-2 is deprecated. Use the migrate command instead.", - Style.BG_YELLOW, Style.BLACK); - env.stderr().println(warning + System.lineSeparator()); + env.colors().style(env.stderr(), "upgrade-1-to-2 is deprecated. Use the migrate command instead." + + System.lineSeparator(), Style.BG_YELLOW, Style.BLACK); env.stderr().flush(); } return command.execute(arguments, env); diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java index ea0a722a598..fcf62b802d7 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java @@ -178,7 +178,7 @@ static Consumer createStatusUpdater( if (encountered > 1) { line = '\r' + line; } - stderr.print(line); + stderr.append(line); stderr.flush(); } }; @@ -187,7 +187,7 @@ static Consumer createStatusUpdater( // If a status update was printed, then clear it out. static void clearStatusUpdateIfPresent(AtomicInteger issueCount, CliPrinter stderr) { if (issueCount.get() > 0) { - stderr.print(CLEAR_LINE_ESCAPE); + stderr.append(CLEAR_LINE_ESCAPE); stderr.flush(); } } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatter.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatter.java index b1b350fe697..4a9cfcf1aa4 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatter.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatter.java @@ -19,6 +19,7 @@ import java.nio.file.Paths; import java.util.Collection; import java.util.regex.Pattern; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.ColorFormatter; import software.amazon.smithy.cli.Style; import software.amazon.smithy.model.SourceLocation; @@ -43,79 +44,77 @@ final class PrettyAnsiValidationFormatter implements ValidationEventFormatter { @Override public String format(ValidationEvent event) { String ls = System.lineSeparator(); - StringBuilder writer = new StringBuilder(); - writer.append(ls); - - switch (event.getSeverity()) { - case WARNING: - printTitle(writer, event, Style.YELLOW, Style.BG_YELLOW, Style.BLACK); - break; - case ERROR: - printTitle(writer, event, Style.RED, Style.BG_RED, Style.BLACK); - break; - case DANGER: - printTitle(writer, event, Style.MAGENTA, Style.BG_MAGENTA, Style.BLACK); - break; - case NOTE: - printTitle(writer, event, Style.CYAN, Style.BG_CYAN, Style.BLACK); - break; - case SUPPRESSED: - default: - printTitle(writer, event, Style.GREEN, Style.BG_GREEN, Style.BLACK); - } - - // Only write an event ID if there is an associated ID. - event.getShapeId().ifPresent(id -> { - colors.style(writer, "Shape: ", Style.BRIGHT_BLACK); - colors.style(writer, id.toString(), Style.BLUE); + try (ColorBuffer writer = ColorBuffer.of(colors, new StringBuilder())) { writer.append(ls); - }); - if (event.getSourceLocation() == SourceLocation.NONE) { - writer.append(ls); - } else { - String humanReadableFilename = getHumanReadableFilename(event.getSourceLocation().getFilename()); - int line = event.getSourceLocation().getLine(); - int column = event.getSourceLocation().getColumn(); - colors.style(writer, "File: ", Style.BRIGHT_BLACK); - colors.style(writer, humanReadableFilename + ':' + line + ':' + column, Style.BLUE); - writer.append(ls).append(ls); - - try { - Collection lines = sourceContextLoader.loadContext(event); - if (!lines.isEmpty()) { - new CodeFormatter(writer, colors, LINE_LENGTH).writeCode(line, column, lines); - } - } catch (UncheckedIOException e) { - colors.style(writer, "Invalid source file", Style.UNDERLINE); - writer.append(": "); - writeMessage(writer, e.getCause().getMessage()); + switch (event.getSeverity()) { + case WARNING: + printTitle(writer, event, Style.YELLOW, Style.BG_YELLOW, Style.BLACK); + break; + case ERROR: + printTitle(writer, event, Style.RED, Style.BG_RED, Style.BLACK); + break; + case DANGER: + printTitle(writer, event, Style.MAGENTA, Style.BG_MAGENTA, Style.BLACK); + break; + case NOTE: + printTitle(writer, event, Style.CYAN, Style.BG_CYAN, Style.BLACK); + break; + case SUPPRESSED: + default: + printTitle(writer, event, Style.GREEN, Style.BG_GREEN, Style.BLACK); + } + + // Only write an event ID if there is an associated ID. + event.getShapeId().ifPresent(id -> { + colors.style(writer, "Shape: ", Style.BRIGHT_BLACK); + colors.style(writer, id.toString(), Style.BLUE); + writer.append(ls); + }); + + if (event.getSourceLocation() == SourceLocation.NONE) { + writer.append(ls); + } else { + String humanReadableFilename = getHumanReadableFilename(event.getSourceLocation().getFilename()); + int line = event.getSourceLocation().getLine(); + int column = event.getSourceLocation().getColumn(); + colors.style(writer, "File: ", Style.BRIGHT_BLACK); + colors.style(writer, humanReadableFilename + ':' + line + ':' + column, Style.BLUE); writer.append(ls).append(ls); + + try { + Collection lines = sourceContextLoader.loadContext(event); + if (!lines.isEmpty()) { + new CodeFormatter(writer, LINE_LENGTH).writeCode(line, column, lines); + } + } catch (UncheckedIOException e) { + colors.style(writer, "Invalid source file", Style.UNDERLINE); + writer.append(": "); + writeMessage(writer, e.getCause().getMessage()); + writer.append(ls).append(ls); + } } - } - writeMessage(writer, event.getMessage()); - writer.append(ls); + writeMessage(writer, event.getMessage()); + writer.append(ls); - return writer.toString(); + return writer.toString(); + } } - private void printTitle(StringBuilder writer, ValidationEvent event, Style borderColor, Style... styles) { + private void printTitle(ColorBuffer writer, ValidationEvent event, Style borderColor, Style... styles) { colors.style(writer, "── ", borderColor); String severity = ' ' + event.getSeverity().toString() + ' '; colors.style(writer, severity, styles); - colors.style(writer, w -> { + writer.style(w -> { w.append(" ──"); - int currentLength = severity.length() + 3 + 3 + 1; // severity, dash+padding, padding+dash, padding. int remainingLength = LINE_LENGTH - currentLength; int padding = remainingLength - event.getId().length(); - for (int i = 0; i < padding; i++) { w.append("─"); } - w.append(' '); }, borderColor); @@ -123,7 +122,7 @@ private void printTitle(StringBuilder writer, ValidationEvent event, Style borde } // Converts Markdown style ticks to use color highlights if colors are enabled. - private void writeMessage(StringBuilder writer, String message) { + private void writeMessage(ColorBuffer writer, String message) { String content = StringUtils.wrap(message, 80, System.lineSeparator(), false); if (colors.isColorEnabled()) { diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Validator.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Validator.java index 99f5906d4dc..4fa24d971ea 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Validator.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Validator.java @@ -18,6 +18,7 @@ import java.util.StringJoiner; import software.amazon.smithy.cli.CliError; import software.amazon.smithy.cli.CliPrinter; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.ColorFormatter; import software.amazon.smithy.cli.StandardOptions; import software.amazon.smithy.cli.Style; @@ -49,42 +50,43 @@ static void validate(boolean quiet, ColorFormatter colors, CliPrinter printer, V boolean isFailed = errors > 0 || dangers > 0; boolean hasEvents = warnings > 0 || notes > 0 || isFailed; - StringBuilder output = new StringBuilder(); - if (isFailed) { - output.append(colors.style("FAILURE: ", Style.RED, Style.BOLD)); - } else { - output.append(colors.style("SUCCESS: ", Style.GREEN, Style.BOLD)); - } - output.append("Validated ").append(shapeCount).append(" shapes"); - - if (hasEvents) { - output.append(' ').append('('); - StringJoiner joiner = new StringJoiner(", "); - if (errors > 0) { - appendSummaryCount(joiner, "ERROR", errors); + try (ColorBuffer output = ColorBuffer.of(colors, new StringBuilder())) { + if (isFailed) { + output.append(colors.style("FAILURE: ", Style.RED, Style.BOLD)); + } else { + output.append(colors.style("SUCCESS: ", Style.GREEN, Style.BOLD)); } + output.append("Validated " + shapeCount).append(" shapes"); - if (dangers > 0) { - appendSummaryCount(joiner, "DANGER", dangers); - } + if (hasEvents) { + output.append(' ').append('('); + StringJoiner joiner = new StringJoiner(", "); + if (errors > 0) { + appendSummaryCount(joiner, "ERROR", errors); + } - if (warnings > 0) { - appendSummaryCount(joiner, "WARNING", warnings); - } + if (dangers > 0) { + appendSummaryCount(joiner, "DANGER", dangers); + } - if (notes > 0) { - appendSummaryCount(joiner, "NOTE", notes); + if (warnings > 0) { + appendSummaryCount(joiner, "WARNING", warnings); + } + + if (notes > 0) { + appendSummaryCount(joiner, "NOTE", notes); + } + output.append(joiner.toString()); + output.append(')'); } - output.append(joiner); - output.append(')'); - } - output.append(System.lineSeparator()); + output.append(System.lineSeparator()); - if (!result.getResult().isPresent() || errors + dangers > 0) { - throw new CliError(output.toString()); - } else if (!quiet) { - printer.println(output.toString()); + if (!result.getResult().isPresent() || errors + dangers > 0) { + throw new CliError(output.toString()); + } else if (!quiet) { + printer.println(output.toString()); + } } } diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/AnsiColorFormatterTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/AnsiColorFormatterTest.java index 1c592e4a148..e43fd76a676 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/AnsiColorFormatterTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/AnsiColorFormatterTest.java @@ -16,10 +16,8 @@ package software.amazon.smithy.cli; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class AnsiColorFormatterTest { @@ -28,35 +26,4 @@ public void detectsIfColorIsEnabled() { assertThat(AnsiColorFormatter.NO_COLOR.isColorEnabled(), is(false)); assertThat(AnsiColorFormatter.FORCE_COLOR.isColorEnabled(), is(true)); } - - @Test - public void wrapsConsumerWithColor() { - ColorFormatter formatter = AnsiColorFormatter.FORCE_COLOR; - StringBuilder builder = new StringBuilder(); - - formatter.style(builder, b -> { - b.append("Hello"); - }, Style.RED); - - String result = builder.toString(); - - assertThat(result, equalTo("\033[31mHello\033[0m")); - } - - @Test - public void wrapsConsumerWithColorAndClosesColorIfThrows() { - ColorFormatter formatter = AnsiColorFormatter.FORCE_COLOR; - StringBuilder builder = new StringBuilder(); - - Assertions.assertThrows(RuntimeException.class, () -> { - formatter.style(builder, b -> { - b.append("Hello"); - throw new RuntimeException("A"); - }, Style.RED); - }); - - String result = builder.toString(); - - assertThat(result, equalTo("\033[31mHello\033[0m")); - } } diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/BufferPrinter.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/BufferPrinter.java index ae369e71076..5d72b41553a 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/BufferPrinter.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/BufferPrinter.java @@ -5,14 +5,21 @@ final class BufferPrinter implements CliPrinter { private final StringBuilder builder = new StringBuilder(); @Override - public void println(String text) { - print(text + "\n"); + public CliPrinter append(char c) { + builder.append(c); + return this; } @Override - public void print(String text) { + public BufferPrinter println(String text) { + return append(text + "\n"); + } + + @Override + public BufferPrinter append(CharSequence text) { synchronized (this) { builder.append(text); + return this; } } diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/CliPrinterTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/CliPrinterTest.java index 8646bf11277..0e323d7e5c9 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/CliPrinterTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/CliPrinterTest.java @@ -24,7 +24,10 @@ public class CliPrinterTest { @Test public void printsWithNewlineByDefault() { StringBuilder builder = new StringBuilder(); - CliPrinter printer = builder::append; + CliPrinter printer = c -> { + builder.append(c); + return null; + }; printer.println("Hi"); assertThat(builder.toString(), equalTo("Hi" + System.lineSeparator())); diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/ColorFormatterTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/ColorFormatterTest.java index 84fbe367d3c..6493870a62f 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/ColorFormatterTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/ColorFormatterTest.java @@ -10,9 +10,9 @@ public class ColorFormatterTest { public void writesToPrinterWhenClosed() { BufferPrinter printer = new BufferPrinter(); ColorFormatter formatter = AnsiColorFormatter.FORCE_COLOR; - String expected = String.format("abc\nHello\n\n\033[31mRed\033[0m\n\n\n0\n"); + String expected = String.format("abc\nHello\n\n\033[31mRed\033[0m\n\n\n0"); - try (ColorFormatter.PrinterBuffer buffer = formatter.printerBuffer(printer)) { + try (ColorBuffer buffer = ColorBuffer.of(formatter, printer)) { buffer.append('a'); buffer.append("bc"); buffer.println(); @@ -35,7 +35,7 @@ public void appendsNewlineIfNeededInToString() { ColorFormatter formatter = AnsiColorFormatter.FORCE_COLOR; String expected ="abc\n"; - try (ColorFormatter.PrinterBuffer buffer = formatter.printerBuffer(printer)) { + try (ColorBuffer buffer = ColorBuffer.of(formatter, printer)) { buffer.println("abc"); assertThat(normalizeNewlines(buffer.toString()), equalTo(expected)); } diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/CodeFormatterTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/CodeFormatterTest.java index a2590078192..0d25a31cb1e 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/CodeFormatterTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/CodeFormatterTest.java @@ -23,6 +23,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import software.amazon.smithy.cli.AnsiColorFormatter; +import software.amazon.smithy.cli.ColorBuffer; import software.amazon.smithy.cli.ColorFormatter; import software.amazon.smithy.model.loader.sourcecontext.SourceContextLoader; @@ -35,7 +36,7 @@ public class CodeFormatterTest { public void outputsSequentialLinesWithNoCursor() { StringBuilder builder = new StringBuilder(); ColorFormatter colors = AnsiColorFormatter.NO_COLOR; - CodeFormatter formatter = new CodeFormatter(builder, colors, 80); + CodeFormatter formatter = new CodeFormatter(ColorBuffer.of(colors, builder), 80); List lines = Arrays.asList( new SourceContextLoader.Line(2, "A"), new SourceContextLoader.Line(3, "B"), @@ -54,7 +55,7 @@ public void outputsSequentialLinesWithNoCursor() { public void outputsSequentialLinesWithCursor() { StringBuilder builder = new StringBuilder(); ColorFormatter colors = AnsiColorFormatter.NO_COLOR; - CodeFormatter formatter = new CodeFormatter(builder, colors, 80); + CodeFormatter formatter = new CodeFormatter(ColorBuffer.of(colors, builder), 80); List lines = Arrays.asList( new SourceContextLoader.Line(2, "Aa"), new SourceContextLoader.Line(3, "Bb"), @@ -74,7 +75,7 @@ public void outputsSequentialLinesWithCursor() { public void detectsLineSkips() { StringBuilder builder = new StringBuilder(); ColorFormatter colors = AnsiColorFormatter.NO_COLOR; - CodeFormatter formatter = new CodeFormatter(builder, colors, 80); + CodeFormatter formatter = new CodeFormatter(ColorBuffer.of(colors, builder), 80); List lines = Arrays.asList( new SourceContextLoader.Line(2, "Aa"), new SourceContextLoader.Line(8, "Bb"), @@ -96,7 +97,7 @@ public void detectsLineSkips() { public void truncatesLongLines() { StringBuilder builder = new StringBuilder(); ColorFormatter colors = AnsiColorFormatter.NO_COLOR; - CodeFormatter formatter = new CodeFormatter(builder, colors, 10); + CodeFormatter formatter = new CodeFormatter(ColorBuffer.of(colors, builder), 10); List lines = Collections.singletonList( new SourceContextLoader.Line(1, "abcdefghijklmnopqrstuvwxyz")); @@ -109,7 +110,7 @@ public void truncatesLongLines() { public void ignoresEmptyLines() { StringBuilder builder = new StringBuilder(); ColorFormatter colors = AnsiColorFormatter.NO_COLOR; - CodeFormatter formatter = new CodeFormatter(builder, colors, 80); + CodeFormatter formatter = new CodeFormatter(ColorBuffer.of(colors, builder), 80); List lines = Collections.emptyList(); formatter.writeCode(0, 0, lines); diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatterTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatterTest.java index 1288cabdef1..937dcff6f51 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatterTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/PrettyAnsiValidationFormatterTest.java @@ -60,8 +60,8 @@ public void formatsEventsWithColors() { + "\u001B[90mShape: \u001B[0m\u001B[34msmithy.example#Foo\u001B[0m\n" + "\u001B[90mFile: \u001B[0m\u001B[34mbuild/resources/test/software/amazon/smithy/cli/commands/valid-model.smithy:5:1\u001B[0m\n" + "\n" - + "\u001B[90m4\u001B[90m| \u001B[0m\u001B[0m\n" - + "\u001B[90m5\u001B[90m| \u001B[0m\u001B[0mresource Foo {\n" + + "\u001B[90m4| \u001B[0m\n" + + "\u001B[90m5| \u001B[0mresource Foo {\n" + "\u001B[90m |\u001B[0m \u001B[31m^\u001B[0m\n" + "\n" + "Hello, \u001B[36mthere\u001B[0m\n"));