From a525ff8af318939890118ac784e8c85bc103c9c1 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 5 Oct 2023 21:06:09 +0200 Subject: [PATCH] Jansi implementation --- LICENSE.txt | 2 +- jansi-core/pom.xml | 36 + .../main/java/org/fusesource/jansi/Ansi.java | 966 ++++++++++++++++++ .../java/org/fusesource/jansi/AnsiColors.java | 30 + .../org/fusesource/jansi/AnsiConsole.java | 492 +++++++++ .../java/org/fusesource/jansi/AnsiMain.java | 339 ++++++ .../java/org/fusesource/jansi/AnsiMode.java | 30 + .../org/fusesource/jansi/AnsiPrintStream.java | 89 ++ .../org/fusesource/jansi/AnsiRenderer.java | 251 +++++ .../java/org/fusesource/jansi/AnsiType.java | 32 + .../org/fusesource/jansi/WindowsSupport.java | 23 + .../fusesource/jansi/io/AnsiOutputStream.java | 352 +++++++ .../fusesource/jansi/io/AnsiProcessor.java | 552 ++++++++++ .../java/org/fusesource/jansi/io/Colors.java | 30 + .../jansi/io/ColorsAnsiProcessor.java | 138 +++ .../jansi/io/FastBufferedOutputStream.java | 61 ++ .../jansi/io/WindowsAnsiProcessor.java | 38 + .../jansi/native-image.properties | 1 + .../native-image/jansi/resource-config.json | 6 + .../org/fusesource/jansi/jansi.properties | 1 + .../resources/org/fusesource/jansi/jansi.txt | 8 + jansi/pom.xml | 230 +++++ .../org/jline/nativ/JLineNativeLoader.java | 2 +- pom.xml | 14 + .../org/jline/terminal/TerminalBuilder.java | 48 +- .../java/org/jline/terminal/impl/Diag.java | 6 +- .../jline/terminal/impl/ExternalTerminal.java | 17 +- .../impl/exec/ExecTerminalProvider.java | 2 +- .../jline/terminal/spi/TerminalProvider.java | 2 +- 29 files changed, 3746 insertions(+), 52 deletions(-) create mode 100644 jansi-core/pom.xml create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/Ansi.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiColors.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiConsole.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiMain.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiMode.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiPrintStream.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiRenderer.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/AnsiType.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/WindowsSupport.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/AnsiProcessor.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/Colors.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/ColorsAnsiProcessor.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java create mode 100644 jansi-core/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java create mode 100644 jansi-core/src/main/resources/META-INF/native-image/jansi/native-image.properties create mode 100644 jansi-core/src/main/resources/META-INF/native-image/jansi/resource-config.json create mode 100644 jansi-core/src/main/resources/org/fusesource/jansi/jansi.properties create mode 100644 jansi-core/src/main/resources/org/fusesource/jansi/jansi.txt create mode 100644 jansi/pom.xml diff --git a/LICENSE.txt b/LICENSE.txt index 7e11b67fb..b62fe4571 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2002-2018, the original author or authors. +Copyright (c) 2002-2023, the original author or authors. All rights reserved. https://opensource.org/licenses/BSD-3-Clause diff --git a/jansi-core/pom.xml b/jansi-core/pom.xml new file mode 100644 index 000000000..34851bb58 --- /dev/null +++ b/jansi-core/pom.xml @@ -0,0 +1,36 @@ + + + + + 4.0.0 + + + org.jline + jline-parent + 3.23.1-SNAPSHOT + + + jansi-core + Jansi Core + + + org.jansi.core + + + + + org.jline + jline-terminal + + + + diff --git a/jansi-core/src/main/java/org/fusesource/jansi/Ansi.java b/jansi-core/src/main/java/org/fusesource/jansi/Ansi.java new file mode 100644 index 000000000..55fc84d4b --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/Ansi.java @@ -0,0 +1,966 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +/** + * Provides a fluent API for generating + * ANSI escape sequences. + * + * @since 1.0 + */ +public class Ansi implements Appendable { + + private static final char FIRST_ESC_CHAR = 27; + private static final char SECOND_ESC_CHAR = '['; + + /** + * ANSI 8 colors for fluent API + */ + public enum Color { + BLACK(0, "BLACK"), + RED(1, "RED"), + GREEN(2, "GREEN"), + YELLOW(3, "YELLOW"), + BLUE(4, "BLUE"), + MAGENTA(5, "MAGENTA"), + CYAN(6, "CYAN"), + WHITE(7, "WHITE"), + DEFAULT(9, "DEFAULT"); + + private final int value; + private final String name; + + Color(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + + public int fg() { + return value + 30; + } + + public int bg() { + return value + 40; + } + + public int fgBright() { + return value + 90; + } + + public int bgBright() { + return value + 100; + } + } + + /** + * Display attributes, also know as + * SGR + * (Select Graphic Rendition) parameters. + */ + public enum Attribute { + RESET(0, "RESET"), + INTENSITY_BOLD(1, "INTENSITY_BOLD"), + INTENSITY_FAINT(2, "INTENSITY_FAINT"), + ITALIC(3, "ITALIC_ON"), + UNDERLINE(4, "UNDERLINE_ON"), + BLINK_SLOW(5, "BLINK_SLOW"), + BLINK_FAST(6, "BLINK_FAST"), + NEGATIVE_ON(7, "NEGATIVE_ON"), + CONCEAL_ON(8, "CONCEAL_ON"), + STRIKETHROUGH_ON(9, "STRIKETHROUGH_ON"), + UNDERLINE_DOUBLE(21, "UNDERLINE_DOUBLE"), + INTENSITY_BOLD_OFF(22, "INTENSITY_BOLD_OFF"), + ITALIC_OFF(23, "ITALIC_OFF"), + UNDERLINE_OFF(24, "UNDERLINE_OFF"), + BLINK_OFF(25, "BLINK_OFF"), + NEGATIVE_OFF(27, "NEGATIVE_OFF"), + CONCEAL_OFF(28, "CONCEAL_OFF"), + STRIKETHROUGH_OFF(29, "STRIKETHROUGH_OFF"); + + private final int value; + private final String name; + + Attribute(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + } + + /** + * ED (Erase in Display) / EL (Erase in Line) parameter (see + * CSI sequence J and K) + * @see Ansi#eraseScreen(Erase) + * @see Ansi#eraseLine(Erase) + */ + public enum Erase { + FORWARD(0, "FORWARD"), + BACKWARD(1, "BACKWARD"), + ALL(2, "ALL"); + + private final int value; + private final String name; + + Erase(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + } + + @FunctionalInterface + public interface Consumer { + void apply(Ansi ansi); + } + + public static final String DISABLE = Ansi.class.getName() + ".disable"; + + private static Callable detector = () -> !Boolean.getBoolean(DISABLE); + + public static void setDetector(final Callable detector) { + if (detector == null) throw new IllegalArgumentException(); + Ansi.detector = detector; + } + + public static boolean isDetected() { + try { + return detector.call(); + } catch (Exception e) { + return true; + } + } + + private static final InheritableThreadLocal holder = new InheritableThreadLocal() { + @Override + protected Boolean initialValue() { + return isDetected(); + } + }; + + public static void setEnabled(final boolean flag) { + holder.set(flag); + } + + public static boolean isEnabled() { + return holder.get(); + } + + public static Ansi ansi() { + if (isEnabled()) { + return new Ansi(); + } else { + return new NoAnsi(); + } + } + + public static Ansi ansi(StringBuilder builder) { + if (isEnabled()) { + return new Ansi(builder); + } else { + return new NoAnsi(builder); + } + } + + public static Ansi ansi(int size) { + if (isEnabled()) { + return new Ansi(size); + } else { + return new NoAnsi(size); + } + } + + private static class NoAnsi extends Ansi { + public NoAnsi() { + super(); + } + + public NoAnsi(int size) { + super(size); + } + + public NoAnsi(StringBuilder builder) { + super(builder); + } + + @Override + public Ansi fg(Color color) { + return this; + } + + @Override + public Ansi bg(Color color) { + return this; + } + + @Override + public Ansi fgBright(Color color) { + return this; + } + + @Override + public Ansi bgBright(Color color) { + return this; + } + + @Override + public Ansi fg(int color) { + return this; + } + + @Override + public Ansi fgRgb(int r, int g, int b) { + return this; + } + + @Override + public Ansi bg(int color) { + return this; + } + + @Override + public Ansi bgRgb(int r, int g, int b) { + return this; + } + + @Override + public Ansi a(Attribute attribute) { + return this; + } + + @Override + public Ansi cursor(int row, int column) { + return this; + } + + @Override + public Ansi cursorToColumn(int x) { + return this; + } + + @Override + public Ansi cursorUp(int y) { + return this; + } + + @Override + public Ansi cursorRight(int x) { + return this; + } + + @Override + public Ansi cursorDown(int y) { + return this; + } + + @Override + public Ansi cursorLeft(int x) { + return this; + } + + @Override + public Ansi cursorDownLine() { + return this; + } + + @Override + public Ansi cursorDownLine(final int n) { + return this; + } + + @Override + public Ansi cursorUpLine() { + return this; + } + + @Override + public Ansi cursorUpLine(final int n) { + return this; + } + + @Override + public Ansi eraseScreen() { + return this; + } + + @Override + public Ansi eraseScreen(Erase kind) { + return this; + } + + @Override + public Ansi eraseLine() { + return this; + } + + @Override + public Ansi eraseLine(Erase kind) { + return this; + } + + @Override + public Ansi scrollUp(int rows) { + return this; + } + + @Override + public Ansi scrollDown(int rows) { + return this; + } + + @Override + public Ansi saveCursorPosition() { + return this; + } + + @Override + @Deprecated + public Ansi restorCursorPosition() { + return this; + } + + @Override + public Ansi restoreCursorPosition() { + return this; + } + + @Override + public Ansi reset() { + return this; + } + } + + private final StringBuilder builder; + private final ArrayList attributeOptions = new ArrayList<>(5); + + public Ansi() { + this(new StringBuilder(80)); + } + + public Ansi(Ansi parent) { + this(new StringBuilder(parent.builder)); + attributeOptions.addAll(parent.attributeOptions); + } + + public Ansi(int size) { + this(new StringBuilder(size)); + } + + public Ansi(StringBuilder builder) { + this.builder = builder; + } + + public Ansi fg(Color color) { + attributeOptions.add(color.fg()); + return this; + } + + public Ansi fg(int color) { + attributeOptions.add(38); + attributeOptions.add(5); + attributeOptions.add(color & 0xff); + return this; + } + + public Ansi fgRgb(int color) { + return fgRgb(color >> 16, color >> 8, color); + } + + public Ansi fgRgb(int r, int g, int b) { + attributeOptions.add(38); + attributeOptions.add(2); + attributeOptions.add(r & 0xff); + attributeOptions.add(g & 0xff); + attributeOptions.add(b & 0xff); + return this; + } + + public Ansi fgBlack() { + return this.fg(Color.BLACK); + } + + public Ansi fgBlue() { + return this.fg(Color.BLUE); + } + + public Ansi fgCyan() { + return this.fg(Color.CYAN); + } + + public Ansi fgDefault() { + return this.fg(Color.DEFAULT); + } + + public Ansi fgGreen() { + return this.fg(Color.GREEN); + } + + public Ansi fgMagenta() { + return this.fg(Color.MAGENTA); + } + + public Ansi fgRed() { + return this.fg(Color.RED); + } + + public Ansi fgYellow() { + return this.fg(Color.YELLOW); + } + + public Ansi bg(Color color) { + attributeOptions.add(color.bg()); + return this; + } + + public Ansi bg(int color) { + attributeOptions.add(48); + attributeOptions.add(5); + attributeOptions.add(color & 0xff); + return this; + } + + public Ansi bgRgb(int color) { + return bgRgb(color >> 16, color >> 8, color); + } + + public Ansi bgRgb(int r, int g, int b) { + attributeOptions.add(48); + attributeOptions.add(2); + attributeOptions.add(r & 0xff); + attributeOptions.add(g & 0xff); + attributeOptions.add(b & 0xff); + return this; + } + + public Ansi bgCyan() { + return this.bg(Color.CYAN); + } + + public Ansi bgDefault() { + return this.bg(Color.DEFAULT); + } + + public Ansi bgGreen() { + return this.bg(Color.GREEN); + } + + public Ansi bgMagenta() { + return this.bg(Color.MAGENTA); + } + + public Ansi bgRed() { + return this.bg(Color.RED); + } + + public Ansi bgYellow() { + return this.bg(Color.YELLOW); + } + + public Ansi fgBright(Color color) { + attributeOptions.add(color.fgBright()); + return this; + } + + public Ansi fgBrightBlack() { + return this.fgBright(Color.BLACK); + } + + public Ansi fgBrightBlue() { + return this.fgBright(Color.BLUE); + } + + public Ansi fgBrightCyan() { + return this.fgBright(Color.CYAN); + } + + public Ansi fgBrightDefault() { + return this.fgBright(Color.DEFAULT); + } + + public Ansi fgBrightGreen() { + return this.fgBright(Color.GREEN); + } + + public Ansi fgBrightMagenta() { + return this.fgBright(Color.MAGENTA); + } + + public Ansi fgBrightRed() { + return this.fgBright(Color.RED); + } + + public Ansi fgBrightYellow() { + return this.fgBright(Color.YELLOW); + } + + public Ansi bgBright(Color color) { + attributeOptions.add(color.bgBright()); + return this; + } + + public Ansi bgBrightCyan() { + return this.bgBright(Color.CYAN); + } + + public Ansi bgBrightDefault() { + return this.bgBright(Color.DEFAULT); + } + + public Ansi bgBrightGreen() { + return this.bgBright(Color.GREEN); + } + + public Ansi bgBrightMagenta() { + return this.bgBright(Color.MAGENTA); + } + + public Ansi bgBrightRed() { + return this.bgBright(Color.RED); + } + + public Ansi bgBrightYellow() { + return this.bgBright(Color.YELLOW); + } + + public Ansi a(Attribute attribute) { + attributeOptions.add(attribute.value()); + return this; + } + + /** + * Moves the cursor to row n, column m. The values are 1-based. + * Any values less than 1 are mapped to 1. + * + * @param row row (1-based) from top + * @param column column (1 based) from left + * @return this Ansi instance + */ + public Ansi cursor(final int row, final int column) { + return appendEscapeSequence('H', Math.max(1, row), Math.max(1, column)); + } + + /** + * Moves the cursor to column n. The parameter n is 1-based. + * If n is less than 1 it is moved to the first column. + * + * @param x the index (1-based) of the column to move to + * @return this Ansi instance + */ + public Ansi cursorToColumn(final int x) { + return appendEscapeSequence('G', Math.max(1, x)); + } + + /** + * Moves the cursor up. If the parameter y is negative it moves the cursor down. + * + * @param y the number of lines to move up + * @return this Ansi instance + */ + public Ansi cursorUp(final int y) { + return y > 0 ? appendEscapeSequence('A', y) : y < 0 ? cursorDown(-y) : this; + } + + /** + * Moves the cursor down. If the parameter y is negative it moves the cursor up. + * + * @param y the number of lines to move down + * @return this Ansi instance + */ + public Ansi cursorDown(final int y) { + return y > 0 ? appendEscapeSequence('B', y) : y < 0 ? cursorUp(-y) : this; + } + + /** + * Moves the cursor right. If the parameter x is negative it moves the cursor left. + * + * @param x the number of characters to move right + * @return this Ansi instance + */ + public Ansi cursorRight(final int x) { + return x > 0 ? appendEscapeSequence('C', x) : x < 0 ? cursorLeft(-x) : this; + } + + /** + * Moves the cursor left. If the parameter x is negative it moves the cursor right. + * + * @param x the number of characters to move left + * @return this Ansi instance + */ + public Ansi cursorLeft(final int x) { + return x > 0 ? appendEscapeSequence('D', x) : x < 0 ? cursorRight(-x) : this; + } + + /** + * Moves the cursor relative to the current position. The cursor is moved right if x is + * positive, left if negative and down if y is positive and up if negative. + * + * @param x the number of characters to move horizontally + * @param y the number of lines to move vertically + * @return this Ansi instance + * @since 2.2 + */ + public Ansi cursorMove(final int x, final int y) { + return cursorRight(x).cursorDown(y); + } + + /** + * Moves the cursor to the beginning of the line below. + * + * @return this Ansi instance + */ + public Ansi cursorDownLine() { + return appendEscapeSequence('E'); + } + + /** + * Moves the cursor to the beginning of the n-th line below. If the parameter n is negative it + * moves the cursor to the beginning of the n-th line above. + * + * @param n the number of lines to move the cursor + * @return this Ansi instance + */ + public Ansi cursorDownLine(final int n) { + return n < 0 ? cursorUpLine(-n) : appendEscapeSequence('E', n); + } + + /** + * Moves the cursor to the beginning of the line above. + * + * @return this Ansi instance + */ + public Ansi cursorUpLine() { + return appendEscapeSequence('F'); + } + + /** + * Moves the cursor to the beginning of the n-th line above. If the parameter n is negative it + * moves the cursor to the beginning of the n-th line below. + * + * @param n the number of lines to move the cursor + * @return this Ansi instance + */ + public Ansi cursorUpLine(final int n) { + return n < 0 ? cursorDownLine(-n) : appendEscapeSequence('F', n); + } + + public Ansi eraseScreen() { + return appendEscapeSequence('J', Erase.ALL.value()); + } + + public Ansi eraseScreen(final Erase kind) { + return appendEscapeSequence('J', kind.value()); + } + + public Ansi eraseLine() { + return appendEscapeSequence('K'); + } + + public Ansi eraseLine(final Erase kind) { + return appendEscapeSequence('K', kind.value()); + } + + public Ansi scrollUp(final int rows) { + if (rows == Integer.MIN_VALUE) { + return scrollDown(Integer.MAX_VALUE); + } + return rows > 0 ? appendEscapeSequence('S', rows) : rows < 0 ? scrollDown(-rows) : this; + } + + public Ansi scrollDown(final int rows) { + if (rows == Integer.MIN_VALUE) { + return scrollUp(Integer.MAX_VALUE); + } + return rows > 0 ? appendEscapeSequence('T', rows) : rows < 0 ? scrollUp(-rows) : this; + } + + @Deprecated + public Ansi restorCursorPosition() { + return restoreCursorPosition(); + } + + public Ansi saveCursorPosition() { + saveCursorPositionSCO(); + return saveCursorPositionDEC(); + } + + // SCO command + public Ansi saveCursorPositionSCO() { + return appendEscapeSequence('s'); + } + + // DEC command + public Ansi saveCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('7'); + return this; + } + + public Ansi restoreCursorPosition() { + restoreCursorPositionSCO(); + return restoreCursorPositionDEC(); + } + + // SCO command + public Ansi restoreCursorPositionSCO() { + return appendEscapeSequence('u'); + } + + // DEC command + public Ansi restoreCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('8'); + return this; + } + + public Ansi reset() { + return a(Attribute.RESET); + } + + public Ansi bold() { + return a(Attribute.INTENSITY_BOLD); + } + + public Ansi boldOff() { + return a(Attribute.INTENSITY_BOLD_OFF); + } + + public Ansi a(String value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(boolean value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(char value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(char[] value, int offset, int len) { + flushAttributes(); + builder.append(value, offset, len); + return this; + } + + public Ansi a(char[] value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(CharSequence value, int start, int end) { + flushAttributes(); + builder.append(value, start, end); + return this; + } + + public Ansi a(CharSequence value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(double value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(float value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(int value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(long value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(Object value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(StringBuffer value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi newline() { + flushAttributes(); + builder.append(System.getProperty("line.separator")); + return this; + } + + public Ansi format(String pattern, Object... args) { + flushAttributes(); + builder.append(String.format(pattern, args)); + return this; + } + + /** + * Applies another function to this Ansi instance. + * + * @param fun the function to apply + * @return this Ansi instance + * @since 2.2 + */ + public Ansi apply(Consumer fun) { + fun.apply(this); + return this; + } + + /** + * Uses the {@link AnsiRenderer} + * to generate the ANSI escape sequences for the supplied text. + * + * @param text text + * @return this + * @since 2.2 + */ + public Ansi render(final String text) { + a(AnsiRenderer.render(text)); + return this; + } + + /** + * String formats and renders the supplied arguments. Uses the {@link AnsiRenderer} + * to generate the ANSI escape sequences. + * + * @param text format + * @param args arguments + * @return this + * @since 2.2 + */ + public Ansi render(final String text, Object... args) { + a(String.format(AnsiRenderer.render(text), args)); + return this; + } + + @Override + public String toString() { + flushAttributes(); + return builder.toString(); + } + + /////////////////////////////////////////////////////////////////// + // Private Helper Methods + /////////////////////////////////////////////////////////////////// + + private Ansi appendEscapeSequence(char command) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(command); + return this; + } + + private Ansi appendEscapeSequence(char command, int option) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(option); + builder.append(command); + return this; + } + + private Ansi appendEscapeSequence(char command, Object... options) { + flushAttributes(); + return _appendEscapeSequence(command, options); + } + + private void flushAttributes() { + if (attributeOptions.isEmpty()) return; + if (attributeOptions.size() == 1 && attributeOptions.get(0) == 0) { + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append('m'); + } else { + _appendEscapeSequence('m', attributeOptions.toArray()); + } + attributeOptions.clear(); + } + + private Ansi _appendEscapeSequence(char command, Object... options) { + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + int size = options.length; + for (int i = 0; i < size; i++) { + if (i != 0) { + builder.append(';'); + } + if (options[i] != null) { + builder.append(options[i]); + } + } + builder.append(command); + return this; + } + + @Override + public Ansi append(CharSequence csq) { + builder.append(csq); + return this; + } + + @Override + public Ansi append(CharSequence csq, int start, int end) { + builder.append(csq, start, end); + return this; + } + + @Override + public Ansi append(char c) { + builder.append(c); + return this; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiColors.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiColors.java new file mode 100644 index 000000000..d57f658e1 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiColors.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +/** + * Colors support. + * + * @since 2.1 + */ +public enum AnsiColors { + Colors16("16 colors"), + Colors256("256 colors"), + TrueColor("24-bit colors"); + + private final String description; + + AnsiColors(String description) { + this.description = description; + } + + String getDescription() { + return description; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiConsole.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiConsole.java new file mode 100644 index 000000000..d74e89349 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -0,0 +1,492 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOError; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import org.fusesource.jansi.io.AnsiOutputStream; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.FastBufferedOutputStream; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.impl.DumbTerminal; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalExt; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; + +/** + * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream + * if not on a terminal (see + * Jansi native + * CLibrary isatty(int)). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.0 + * @see #systemInstall() + * @see #out() + * @see #err() + * @see #ansiStream(boolean) for more details on ANSI mode selection + */ +public class AnsiConsole { + + /** + * The default mode which Jansi will use, can be either force, strip + * or default (the default). + * If this property is set, it will override jansi.passthrough, + * jansi.strip and jansi.force properties. + */ + public static final String JANSI_MODE = "jansi.mode"; + /** + * Jansi mode specific to the standard output stream. + */ + public static final String JANSI_OUT_MODE = "jansi.out.mode"; + /** + * Jansi mode specific to the standard error stream. + */ + public static final String JANSI_ERR_MODE = "jansi.err.mode"; + + /** + * Jansi mode value to strip all ansi sequences. + */ + public static final String JANSI_MODE_STRIP = "strip"; + /** + * Jansi mode value to force ansi sequences to the stream even if it's not a terminal. + */ + public static final String JANSI_MODE_FORCE = "force"; + /** + * Jansi mode value that output sequences if on a terminal, else strip them. + */ + public static final String JANSI_MODE_DEFAULT = "default"; + + /** + * The default color support that Jansi will use, can be either 16, + * 256 or truecolor. If not set, JANSI will try to + * autodetect the number of colors supported by the terminal by checking the + * COLORTERM and TERM variables. + */ + public static final String JANSI_COLORS = "jansi.colors"; + /** + * Jansi colors specific to the standard output stream. + */ + public static final String JANSI_OUT_COLORS = "jansi.out.colors"; + /** + * Jansi colors specific to the standard error stream. + */ + public static final String JANSI_ERR_COLORS = "jansi.err.colors"; + + /** + * Force the use of 16 colors. When using a 256-indexed color, or an RGB + * color, the color will be rounded to the nearest one from the 16 palette. + */ + public static final String JANSI_COLORS_16 = "16"; + /** + * Force the use of 256 colors. When using an RGB color, the color will be + * rounded to the nearest one from the standard 256 palette. + */ + public static final String JANSI_COLORS_256 = "256"; + /** + * Force the use of 24-bit colors. + */ + public static final String JANSI_COLORS_TRUECOLOR = "truecolor"; + + /** + * If the jansi.passthrough system property is set to true, will not perform any transformation + * and any ansi sequence will be conveyed without any modification. + * + * @deprecated use {@link #JANSI_MODE} instead + */ + @Deprecated + public static final String JANSI_PASSTHROUGH = "jansi.passthrough"; + /** + * If the jansi.strip system property is set to true, and jansi.passthrough + * is not enabled, all ansi sequences will be stripped before characters are written to the output streams. + * + * @deprecated use {@link #JANSI_MODE} instead + */ + @Deprecated + public static final String JANSI_STRIP = "jansi.strip"; + /** + * If the jansi.force system property is set to true, and neither jansi.passthrough + * nor jansi.strip are set, then ansi sequences will be printed to the output stream. + * This forces the behavior which is by default dependent on the output stream being a real console: if the + * output stream is redirected to a file or through a system pipe, ansi sequences are disabled by default. + * + * @deprecated use {@link #JANSI_MODE} instead + */ + @Deprecated + public static final String JANSI_FORCE = "jansi.force"; + /** + * If the jansi.eager system property is set to true, the system streams will be eagerly + * initialized, else the initialization is delayed until {@link #out()}, {@link #err()} or {@link #systemInstall()} + * is called. + * + * @deprecated this property has been added but only for backward compatibility. + * @since 2.1 + */ + @Deprecated() + public static final String JANSI_EAGER = "jansi.eager"; + /** + * If the jansi.noreset system property is set to true, the attributes won't be + * reset when the streams are uninstalled. + */ + public static final String JANSI_NORESET = "jansi.noreset"; + /** + * If the jansi.graceful system property is set to false, any exception that occurs + * during the initialization will cause the library to report this exception and fail. The default + * behavior is to behave gracefully and fall back to pure emulation on posix systems. + */ + public static final String JANSI_GRACEFUL = "jansi.graceful"; + + /** + * The {@code jansi.providers} system property can be set to control which internal provider + * will be used. If this property is not set, the {@code ffm} provider will be used if available, + * else the {@code jni} one will be used. If set, this property is interpreted as a comma + * separated list of provider names to try in order. + */ + public static final String JANSI_PROVIDERS = "jansi.providers"; + /** + * The name of the {@code jni} provider. + */ + public static final String JANSI_PROVIDER_JNI = "jni"; + /** + * The name of the {@code ffm} provider. + */ + public static final String JANSI_PROVIDER_FFM = "ffm"; + /** + * The name of the {@code native-image} provider. + *

This provider uses the + * Native Image C API + * to call native functions, so it is only available when building to native image. + * Additionally, this provider currently does not support Windows. + *

Note: This is not the only provider available on Native Image, + * and it is usually recommended to use ffm or jni provider. + * This provider is mainly used when building static native images linked to musl libc. + */ + public static final String JANSI_PROVIDER_NATIVE_IMAGE = "native-image"; + + /** + * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead + */ + @Deprecated + public static PrintStream system_out = System.out; + /** + * @deprecated this field will be made private in a future release, use {@link #out()} instead + */ + @Deprecated + public static PrintStream out; + /** + * @deprecated this field will be made private in a future release, use {@link #sysErr()} instead + */ + @Deprecated + public static PrintStream system_err = System.err; + /** + * @deprecated this field will be made private in a future release, use {@link #err()} instead + */ + @Deprecated + public static PrintStream err; + + /** + * Try to find the width of the console for this process. + * Both output and error streams will be checked to determine the width. + * A value of 0 is returned if the width can not be determined. + * @since 2.2 + */ + public static int getTerminalWidth() { + int w = out().getTerminalWidth(); + if (w <= 0) { + w = err().getTerminalWidth(); + } + return w; + } + + static final boolean IS_WINDOWS = OSUtils.IS_WINDOWS; + + static final boolean IS_CYGWIN = + IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); + + static final boolean IS_MSYSTEM = IS_WINDOWS + && System.getenv("MSYSTEM") != null + && (System.getenv("MSYSTEM").startsWith("MINGW") + || System.getenv("MSYSTEM").equals("MSYS")); + + static final boolean IS_CONEMU = IS_WINDOWS && System.getenv("ConEmuPID") != null; + + static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + + static int STDOUT_FILENO = 1; + + static int STDERR_FILENO = 2; + + static { + if (getBoolean(JANSI_EAGER)) { + doInstall(); + } + } + + private static int installed; + static Terminal terminal; + + private AnsiConsole() {} + + /** + * Initialize the out/err ansi-enabled streams + */ + static synchronized void doInstall() { + try { + if (terminal == null) { + TerminalBuilder builder = TerminalBuilder.builder() + .system(true) + .name("jansi") + .providers(System.getProperty(JANSI_PROVIDERS)); + String graceful = System.getProperty(JANSI_GRACEFUL); + if (graceful != null) { + builder.dumb(Boolean.parseBoolean(graceful)); + } + terminal = builder.build(); + out = ansiStream(true); + err = ansiStream(false); + } + } catch (IOException e) { + throw new IOError(e); + } + } + + static synchronized void doUninstall() { + try { + if (terminal != null) { + terminal.close(); + } + } catch (IOException e) { + throw new IOError(e); + } finally { + terminal = null; + out = null; + err = null; + } + } + + private static AnsiPrintStream ansiStream(boolean stdout) throws IOException { + final OutputStream out; + final AnsiOutputStream.WidthSupplier width; + final AnsiProcessor processor = null; + final AnsiType type; + final AnsiOutputStream.IoRunnable installer = null; + final AnsiOutputStream.IoRunnable uninstaller = null; + + final TerminalProvider provider = ((TerminalExt) terminal).getProvider(); + final boolean isatty = + provider != null && provider.isSystemStream(stdout ? SystemStream.Output : SystemStream.Error); + if (isatty) { + out = terminal.output(); + width = terminal::getWidth; + type = terminal instanceof DumbTerminal ? AnsiType.Unsupported : AnsiType.Native; + } else { + out = new FastBufferedOutputStream(new FileOutputStream(stdout ? FileDescriptor.out : FileDescriptor.err)); + width = new AnsiOutputStream.ZeroWidthSupplier(); + type = ((TerminalExt) terminal).getSystemStream() != null ? AnsiType.Redirected : AnsiType.Unsupported; + } + String enc = System.getProperty(stdout ? "stdout.encoding" : "stderr.encoding"); + if (enc == null) { + enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); + } + + AnsiMode mode; + + // If the jansi.mode property is set, use it + String jansiMode = System.getProperty(stdout ? JANSI_OUT_MODE : JANSI_ERR_MODE, System.getProperty(JANSI_MODE)); + if (JANSI_MODE_FORCE.equals(jansiMode)) { + mode = AnsiMode.Force; + } else if (JANSI_MODE_STRIP.equals(jansiMode)) { + mode = AnsiMode.Strip; + } else if (jansiMode != null) { + mode = isatty ? AnsiMode.Default : AnsiMode.Strip; + } + + // If the jansi.passthrough property is set, then don't interpret + // any of the ansi sequences. + else if (getBoolean(JANSI_PASSTHROUGH)) { + mode = AnsiMode.Force; + } + + // If the jansi.strip property is set, then we just strip the + // the ansi escapes. + else if (getBoolean(JANSI_STRIP)) { + mode = AnsiMode.Strip; + } + + // If the jansi.force property is set, then we force to output + // the ansi escapes for piping it into ansi color aware commands (e.g. less -r) + else if (getBoolean(JANSI_FORCE)) { + mode = AnsiMode.Force; + } else { + mode = isatty ? AnsiMode.Default : AnsiMode.Strip; + } + + AnsiColors colors; + + String colorterm, term; + // If the jansi.colors property is set, use it + String jansiColors = + System.getProperty(stdout ? JANSI_OUT_COLORS : JANSI_ERR_COLORS, System.getProperty(JANSI_COLORS)); + if (JANSI_COLORS_TRUECOLOR.equals(jansiColors)) { + colors = AnsiColors.TrueColor; + } else if (JANSI_COLORS_256.equals(jansiColors)) { + colors = AnsiColors.Colors256; + } else if (jansiColors != null) { + colors = AnsiColors.Colors16; + } + + // If the COLORTERM env variable contains "truecolor" or "24bit", assume true color support + // see https://gist.github.com/XVilka/8346728#true-color-detection + else if ((colorterm = System.getenv("COLORTERM")) != null + && (colorterm.contains("truecolor") || colorterm.contains("24bit"))) { + colors = AnsiColors.TrueColor; + } + + // check the if TERM contains -direct + else if ((term = System.getenv("TERM")) != null && term.contains("-direct")) { + colors = AnsiColors.TrueColor; + } + + // check the if TERM contains -256color + else if (term != null && term.contains("-256color")) { + colors = AnsiColors.Colors256; + } + + // else defaults to 16 colors + else { + colors = AnsiColors.Colors16; + } + + // If the jansi.noreset property is not set, reset the attributes + // when the stream is closed + boolean resetAtUninstall = type != AnsiType.Unsupported && !getBoolean(JANSI_NORESET); + + return newPrintStream( + new AnsiOutputStream( + out, + width, + mode, + processor, + type, + colors, + terminal.encoding(), + installer, + uninstaller, + resetAtUninstall), + terminal.encoding().name()); + } + + private static AnsiPrintStream newPrintStream(AnsiOutputStream out, String enc) { + if (enc != null) { + try { + return new AnsiPrintStream(out, true, enc); + } catch (UnsupportedEncodingException e) { + } + } + return new AnsiPrintStream(out, true); + } + + static boolean getBoolean(String name) { + boolean result = false; + try { + String val = System.getProperty(name); + result = val.isEmpty() || Boolean.parseBoolean(val); + } catch (IllegalArgumentException | NullPointerException ignored) { + } + return result; + } + + /** + * If the standard out natively supports ANSI escape codes, then this just + * returns System.out, otherwise it will provide an ANSI aware PrintStream + * which strips out the ANSI escape sequences or which implement the escape + * sequences. + * + * @return a PrintStream which is ANSI aware. + */ + public static AnsiPrintStream out() { + doInstall(); + return (AnsiPrintStream) out; + } + + /** + * Access to the original System.out stream before ansi streams were installed. + * + * @return the originial System.out print stream + */ + public static PrintStream sysOut() { + return system_out; + } + + /** + * If the standard out natively supports ANSI escape codes, then this just + * returns System.err, otherwise it will provide an ANSI aware PrintStream + * which strips out the ANSI escape sequences or which implement the escape + * sequences. + * + * @return a PrintStream which is ANSI aware. + */ + public static AnsiPrintStream err() { + doInstall(); + return (AnsiPrintStream) err; + } + + /** + * Access to the original System.err stream before ansi streams were installed. + * + * @return the originial System.err print stream + */ + public static PrintStream sysErr() { + return system_err; + } + + /** + * Install AnsiConsole.out() to System.out and + * AnsiConsole.err() to System.err. + * @see #systemUninstall() + */ + public static synchronized void systemInstall() { + if (installed == 0) { + doInstall(); + System.setOut(out); + System.setErr(err); + } + installed++; + } + + /** + * check if the streams have been installed or not + */ + public static synchronized boolean isInstalled() { + return installed > 0; + } + + /** + * undo a previous {@link #systemInstall()}. If {@link #systemInstall()} was called + * multiple times, {@link #systemUninstall()} must be called the same number of times before + * it is actually uninstalled. + */ + public static synchronized void systemUninstall() { + installed--; + if (installed == 0) { + doUninstall(); + System.setOut(system_out); + System.setErr(system_err); + } + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiMain.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiMain.java new file mode 100644 index 000000000..a40059900 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.util.Properties; + +import org.fusesource.jansi.Ansi.Attribute; +import org.jline.terminal.impl.Diag; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.fusesource.jansi.Ansi.ansi; + +/** + * Main class for the library, providing executable jar to diagnose Jansi setup. + *

If no system property is set and output is sent to a terminal (no redirect to a file): + *

    + *
  • any terminal on any Unix should get RESET_ANSI_AT_CLOSE mode,
  • + *
  • on Windows, Git-bash or Cygwin terminals should get RESET_ANSI_AT_CLOSE mode also, since they + * support natively ANSI escape sequences like any Unix terminal,
  • + *
  • on Windows, cmd.exe, PowerShell or Git-cmd terminals should get WINDOWS mode.
  • + *
+ * If stdout is redirected to a file (> out.txt), System.out should switch to STRIP_ANSI. + * Same for stderr redirection (2> err.txt) which should affect System.err mode. + *

The results will vary if you play with jansi.passthrough, jansi.strip or + * jansi.force system property, or if you redirect output to a file. + *

If you have a specific situation that is not covered, please report precise conditions to reproduce + * the issue and ideas on how to detect precisely the affected situation. + * @see AnsiConsole + */ +@SuppressWarnings("deprecation") +public class AnsiMain { + public static void main(String... args) throws IOException { + Diag.diag(System.out); + + System.out.println("Jansi " + getJansiVersion()); + + System.out.println(); + + System.out.println("jansi.providers= " + System.getProperty(AnsiConsole.JANSI_PROVIDERS, "")); + // String provider = ((TerminalExt) AnsiConsole.terminal).getProvider().name(); + // System.out.println("Selected provider: " + provider); + // + // if (AnsiConsole.JANSI_PROVIDER_JNI.equals(provider)) { + // // info on native library + // System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); + // System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); + // boolean loaded = JansiLoader.initialize(); + // if (loaded) { + // System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); + // if (JansiLoader.getNativeLibrarySourceUrl() != null) { + // System.out.println(" which was auto-extracted from " + + // JansiLoader.getNativeLibrarySourceUrl()); + // } + // } else { + // String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); + // try { + // System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); + // JansiLoader.initialize(); + // } catch (Throwable e) { + // e.printStackTrace(System.out); + // } finally { + // if (prev != null) { + // System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); + // } else { + // System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + // } + // } + // } + // } + + System.out.println(); + + System.out.println("os.name= " + System.getProperty("os.name") + ", " + + "os.version= " + System.getProperty("os.version") + ", " + + "os.arch= " + System.getProperty("os.arch")); + System.out.println("file.encoding= " + System.getProperty("file.encoding")); + System.out.println("sun.stdout.encoding= " + System.getProperty("sun.stdout.encoding") + ", " + + "sun.stderr.encoding= " + System.getProperty("sun.stderr.encoding")); + System.out.println("stdout.encoding= " + System.getProperty("stdout.encoding") + ", " + "stderr.encoding= " + + System.getProperty("stderr.encoding")); + System.out.println("java.version= " + System.getProperty("java.version") + ", " + + "java.vendor= " + System.getProperty("java.vendor") + "," + + " java.home= " + System.getProperty("java.home")); + System.out.println("Console: " + System.console()); + + System.out.println(); + + System.out.println(AnsiConsole.JANSI_GRACEFUL + "= " + System.getProperty(AnsiConsole.JANSI_GRACEFUL, "")); + System.out.println(AnsiConsole.JANSI_MODE + "= " + System.getProperty(AnsiConsole.JANSI_MODE, "")); + System.out.println(AnsiConsole.JANSI_OUT_MODE + "= " + System.getProperty(AnsiConsole.JANSI_OUT_MODE, "")); + System.out.println(AnsiConsole.JANSI_ERR_MODE + "= " + System.getProperty(AnsiConsole.JANSI_ERR_MODE, "")); + System.out.println(AnsiConsole.JANSI_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_COLORS, "")); + System.out.println(AnsiConsole.JANSI_OUT_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_OUT_COLORS, "")); + System.out.println(AnsiConsole.JANSI_ERR_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_ERR_COLORS, "")); + System.out.println( + AnsiConsole.JANSI_PASSTHROUGH + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_PASSTHROUGH)); + System.out.println(AnsiConsole.JANSI_STRIP + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_STRIP)); + System.out.println(AnsiConsole.JANSI_FORCE + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_FORCE)); + System.out.println(AnsiConsole.JANSI_NORESET + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_NORESET)); + System.out.println(Ansi.DISABLE + "= " + AnsiConsole.getBoolean(Ansi.DISABLE)); + + System.out.println(); + + System.out.println("IS_WINDOWS: " + AnsiConsole.IS_WINDOWS); + if (AnsiConsole.IS_WINDOWS) { + System.out.println("IS_CONEMU: " + AnsiConsole.IS_CONEMU); + System.out.println("IS_CYGWIN: " + AnsiConsole.IS_CYGWIN); + System.out.println("IS_MSYSTEM: " + AnsiConsole.IS_MSYSTEM); + } + + System.out.println(); + + diagnoseTty(false); // System.out + diagnoseTty(true); // System.err + + AnsiConsole.systemInstall(); + + System.out.println(); + + System.out.println("Resulting Jansi modes for stout/stderr streams:"); + System.out.println(" - System.out: " + AnsiConsole.out().toString()); + System.out.println(" - System.err: " + AnsiConsole.err().toString()); + System.out.println("Processor types description:"); + for (AnsiType type : AnsiType.values()) { + System.out.println(" - " + type + ": " + type.getDescription()); + } + System.out.println("Colors support description:"); + for (AnsiColors colors : AnsiColors.values()) { + System.out.println(" - " + colors + ": " + colors.getDescription()); + } + System.out.println("Modes description:"); + for (AnsiMode mode : AnsiMode.values()) { + System.out.println(" - " + mode + ": " + mode.getDescription()); + } + + try { + System.out.println(); + + testAnsi(false); + testAnsi(true); + + if (args.length == 0) { + printJansiLogoDemo(); + return; + } + + System.out.println(); + + if (args.length == 1) { + File f = new File(args[0]); + if (f.exists()) { + // write file content + System.out.println( + ansi().bold().a("\"" + args[0] + "\" content:").reset()); + writeFileContent(f); + return; + } + } + + // write args without Jansi then with Jansi AnsiConsole + System.out.println(ansi().bold().a("original args:").reset()); + int i = 1; + for (String arg : args) { + AnsiConsole.sysOut().print(i++ + ": "); + AnsiConsole.sysOut().println(arg); + } + + System.out.println(ansi().bold().a("Jansi filtered args:").reset()); + i = 1; + for (String arg : args) { + System.out.print(i++ + ": "); + System.out.println(arg); + } + } finally { + AnsiConsole.systemUninstall(); + } + } + + private static String getJansiVersion() { + Package p = AnsiMain.class.getPackage(); + return (p == null) ? null : p.getImplementationVersion(); + } + + private static void diagnoseTty(boolean stderr) { + // int isatty; + // int width; + // if (AnsiConsole.IS_WINDOWS) { + // long console = AnsiConsoleSupportHolder.getKernel32().getStdHandle(!stderr); + // isatty = AnsiConsoleSupportHolder.getKernel32().isTty(console); + // if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { + // MingwSupport mingw = new MingwSupport(); + // String name = mingw.getConsoleName(!stderr); + // if (name != null && !name.isEmpty()) { + // isatty = 1; + // width = mingw.getTerminalWidth(name); + // } else { + // isatty = 0; + // width = 0; + // } + // } else { + // width = AnsiConsoleSupportHolder.getKernel32().getTerminalWidth(console); + // } + // } else { + // int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : + // AnsiConsoleSupport.CLibrary.STDOUT_FILENO; + // isatty = AnsiConsoleSupportHolder.getCLibrary().isTty(fd); + // width = AnsiConsoleSupportHolder.getCLibrary().getTerminalWidth(fd); + // } + // + // System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." + // + (stderr ? "err" : "out") + " " + ((isatty == 0) ? "is *NOT*" : "is") + " a terminal"); + // System.out.println("width(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + width); + } + + private static void testAnsi(boolean stderr) { + @SuppressWarnings("resource") + PrintStream s = stderr ? System.err : System.out; + s.print("test on System." + (stderr ? "err" : "out") + ":"); + for (Ansi.Color c : Ansi.Color.values()) { + s.print(" " + ansi().fg(c) + c + ansi().reset()); + } + s.println(); + s.print(" bright:"); + for (Ansi.Color c : Ansi.Color.values()) { + s.print(" " + ansi().fgBright(c) + c + ansi().reset()); + } + s.println(); + s.print(" bold:"); + for (Ansi.Color c : Ansi.Color.values()) { + s.print(" " + ansi().bold().fg(c) + c + ansi().reset()); + } + s.println(); + s.print(" faint:"); + for (Ansi.Color c : Ansi.Color.values()) { + s.print(" " + ansi().a(Attribute.INTENSITY_FAINT).fg(c) + c + ansi().reset()); + } + s.println(); + s.print(" bold+faint:"); + for (Ansi.Color c : Ansi.Color.values()) { + s.print(" " + ansi().bold().a(Attribute.INTENSITY_FAINT).fg(c) + c + ansi().reset()); + } + s.println(); + Ansi ansi = ansi(); + ansi.a(" 256 colors: "); + for (int i = 0; i < 6 * 6 * 6; i++) { + if (i > 0 && i % 36 == 0) { + ansi.reset(); + ansi.newline(); + ansi.a(" "); + } else if (i > 0 && i % 6 == 0) { + ansi.reset(); + ansi.a(" "); + } + int a0 = i % 6; + int a1 = (i / 6) % 6; + int a2 = i / 36; + ansi.bg(16 + a0 + a2 * 6 + a1 * 36).a(' '); + } + ansi.reset(); + s.println(ansi); + ansi = ansi(); + ansi.a(" truecolor: "); + for (int i = 0; i < 256; i++) { + if (i > 0 && i % 48 == 0) { + ansi.reset(); + ansi.newline(); + ansi.a(" "); + } + int r = 255 - i; + int g = i * 2 > 255 ? 255 - 2 * i : 2 * i; + int b = i; + ansi.bgRgb(r, g, b).fgRgb(255 - r, 255 - g, 255 - b).a(i % 2 == 0 ? '/' : '\\'); + } + ansi.reset(); + s.println(ansi); + } + + private static String getPomPropertiesVersion(String path) throws IOException { + InputStream in = AnsiMain.class.getResourceAsStream("/META-INF/maven/" + path + "/pom.properties"); + if (in == null) { + return null; + } + try { + Properties p = new Properties(); + p.load(in); + return p.getProperty("version"); + } finally { + closeQuietly(in); + } + } + + private static void printJansiLogoDemo() throws IOException { + BufferedReader in = + new BufferedReader(new InputStreamReader(AnsiMain.class.getResourceAsStream("jansi.txt"), UTF_8)); + try { + String l; + while ((l = in.readLine()) != null) { + System.out.println(l); + } + } finally { + closeQuietly(in); + } + } + + private static void writeFileContent(File f) throws IOException { + InputStream in = new FileInputStream(f); + try { + byte[] buf = new byte[1024]; + int l = 0; + while ((l = in.read(buf)) >= 0) { + System.out.write(buf, 0, l); + } + } finally { + closeQuietly(in); + } + } + + private static void closeQuietly(Closeable c) { + try { + c.close(); + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiMode.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiMode.java new file mode 100644 index 000000000..b70312bcd --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiMode.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +/** + * Ansi mode. + * + * @since 2.1 + */ +public enum AnsiMode { + Strip("Strip all ansi sequences"), + Default("Print ansi sequences if the stream is a terminal"), + Force("Always print ansi sequences, even if the stream is redirected"); + + private final String description; + + AnsiMode(String description) { + this.description = description; + } + + String getDescription() { + return description; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiPrintStream.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiPrintStream.java new file mode 100644 index 000000000..d25efbef3 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiPrintStream.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import org.fusesource.jansi.io.AnsiOutputStream; + +/** + * Simple PrintStream holding an AnsiOutputStream. + * This allows changing the mode in which the underlying AnsiOutputStream operates. + */ +public class AnsiPrintStream extends PrintStream { + + public AnsiPrintStream(AnsiOutputStream out, boolean autoFlush) { + super(out, autoFlush); + } + + public AnsiPrintStream(AnsiOutputStream out, boolean autoFlush, String encoding) + throws UnsupportedEncodingException { + super(out, autoFlush, encoding); + } + + protected AnsiOutputStream getOut() { + return (AnsiOutputStream) out; + } + + public AnsiType getType() { + return getOut().getType(); + } + + public AnsiColors getColors() { + return getOut().getColors(); + } + + public AnsiMode getMode() { + return getOut().getMode(); + } + + public void setMode(AnsiMode ansiMode) { + getOut().setMode(ansiMode); + } + + public boolean isResetAtUninstall() { + return getOut().isResetAtUninstall(); + } + + public void setResetAtUninstall(boolean resetAtClose) { + getOut().setResetAtUninstall(resetAtClose); + } + + /** + * Returns the width of the terminal associated with this stream or 0. + * @since 2.2 + */ + public int getTerminalWidth() { + return getOut().getTerminalWidth(); + } + + public void install() throws IOException { + getOut().install(); + } + + public void uninstall() throws IOException { + // If the system output stream has been closed, out should be null, so avoid a NPE + AnsiOutputStream out = getOut(); + if (out != null) { + out.uninstall(); + } + } + + @Override + public String toString() { + return "AnsiPrintStream{" + + "type=" + getType() + + ", colors=" + getColors() + + ", mode=" + getMode() + + ", resetAtUninstall=" + isResetAtUninstall() + + "}"; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiRenderer.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiRenderer.java new file mode 100644 index 000000000..59995d41b --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiRenderer.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.util.Locale; + +import org.fusesource.jansi.Ansi.Attribute; +import org.fusesource.jansi.Ansi.Color; + +/** + * Renders ANSI color escape-codes in strings by parsing out some special syntax to pick up the correct fluff to use. + * + * The syntax for embedded ANSI codes is: + * + *

+ *   @|code(,code)* text|@
+ * 
+ * + * Examples: + * + *
+ *   @|bold Hello|@
+ * 
+ * + *
+ *   @|bold,red Warning!|@
+ * 
+ * + * @since 2.2 + */ +public class AnsiRenderer { + + public static final String BEGIN_TOKEN = "@|"; + + public static final String END_TOKEN = "|@"; + + public static final String CODE_TEXT_SEPARATOR = " "; + + public static final String CODE_LIST_SEPARATOR = ","; + + private static final int BEGIN_TOKEN_LEN = 2; + + private static final int END_TOKEN_LEN = 2; + + public static String render(final String input) throws IllegalArgumentException { + try { + return render(input, new StringBuilder()).toString(); + } catch (IOException e) { + // Cannot happen because StringBuilder does not throw IOException + throw new IllegalArgumentException(e); + } + } + + /** + * Renders the given input to the target Appendable. + * + * @param input + * source to render + * @param target + * render onto this target Appendable. + * @return the given Appendable + * @throws IOException + * If an I/O error occurs + */ + public static Appendable render(final String input, Appendable target) throws IOException { + + int i = 0; + int j, k; + + while (true) { + j = input.indexOf(BEGIN_TOKEN, i); + if (j == -1) { + if (i == 0) { + target.append(input); + return target; + } + target.append(input.substring(i)); + return target; + } + target.append(input.substring(i, j)); + k = input.indexOf(END_TOKEN, j); + + if (k == -1) { + target.append(input); + return target; + } + j += BEGIN_TOKEN_LEN; + + // Check for invalid string with END_TOKEN before BEGIN_TOKEN + if (k < j) { + throw new IllegalArgumentException("Invalid input string found."); + } + String spec = input.substring(j, k); + + String[] items = spec.split(CODE_TEXT_SEPARATOR, 2); + if (items.length == 1) { + target.append(input); + return target; + } + String replacement = render(items[1], items[0].split(CODE_LIST_SEPARATOR)); + + target.append(replacement); + + i = k + END_TOKEN_LEN; + } + } + + public static String render(final String text, final String... codes) { + return render(Ansi.ansi(), codes).a(text).reset().toString(); + } + + /** + * Renders {@link Code} names as an ANSI escape string. + * @param codes The code names to render + * @return an ANSI escape string. + */ + public static String renderCodes(final String... codes) { + return render(Ansi.ansi(), codes).toString(); + } + + /** + * Renders {@link Code} names as an ANSI escape string. + * @param codes A space separated list of code names to render + * @return an ANSI escape string. + */ + public static String renderCodes(final String codes) { + return renderCodes(codes.split("\\s")); + } + + private static Ansi render(Ansi ansi, String... names) { + for (String name : names) { + Code code = Code.valueOf(name.toUpperCase(Locale.ENGLISH)); + if (code.isColor()) { + if (code.isBackground()) { + ansi.bg(code.getColor()); + } else { + ansi.fg(code.getColor()); + } + } else if (code.isAttribute()) { + ansi.a(code.getAttribute()); + } + } + return ansi; + } + + public static boolean test(final String text) { + return text != null && text.contains(BEGIN_TOKEN); + } + + @SuppressWarnings("unused") + public enum Code { + // + // TODO: Find a better way to keep Code in sync with Color/Attribute/Erase + // + + // Colors + BLACK(Color.BLACK), + RED(Color.RED), + GREEN(Color.GREEN), + YELLOW(Color.YELLOW), + BLUE(Color.BLUE), + MAGENTA(Color.MAGENTA), + CYAN(Color.CYAN), + WHITE(Color.WHITE), + DEFAULT(Color.DEFAULT), + + // Foreground Colors + FG_BLACK(Color.BLACK, false), + FG_RED(Color.RED, false), + FG_GREEN(Color.GREEN, false), + FG_YELLOW(Color.YELLOW, false), + FG_BLUE(Color.BLUE, false), + FG_MAGENTA(Color.MAGENTA, false), + FG_CYAN(Color.CYAN, false), + FG_WHITE(Color.WHITE, false), + FG_DEFAULT(Color.DEFAULT, false), + + // Background Colors + BG_BLACK(Color.BLACK, true), + BG_RED(Color.RED, true), + BG_GREEN(Color.GREEN, true), + BG_YELLOW(Color.YELLOW, true), + BG_BLUE(Color.BLUE, true), + BG_MAGENTA(Color.MAGENTA, true), + BG_CYAN(Color.CYAN, true), + BG_WHITE(Color.WHITE, true), + BG_DEFAULT(Color.DEFAULT, true), + + // Attributes + RESET(Attribute.RESET), + INTENSITY_BOLD(Attribute.INTENSITY_BOLD), + INTENSITY_FAINT(Attribute.INTENSITY_FAINT), + ITALIC(Attribute.ITALIC), + UNDERLINE(Attribute.UNDERLINE), + BLINK_SLOW(Attribute.BLINK_SLOW), + BLINK_FAST(Attribute.BLINK_FAST), + BLINK_OFF(Attribute.BLINK_OFF), + NEGATIVE_ON(Attribute.NEGATIVE_ON), + NEGATIVE_OFF(Attribute.NEGATIVE_OFF), + CONCEAL_ON(Attribute.CONCEAL_ON), + CONCEAL_OFF(Attribute.CONCEAL_OFF), + UNDERLINE_DOUBLE(Attribute.UNDERLINE_DOUBLE), + UNDERLINE_OFF(Attribute.UNDERLINE_OFF), + + // Aliases + BOLD(Attribute.INTENSITY_BOLD), + FAINT(Attribute.INTENSITY_FAINT); + + private final Enum n; + + private final boolean background; + + Code(final Enum n, boolean background) { + this.n = n; + this.background = background; + } + + Code(final Enum n) { + this(n, false); + } + + public boolean isColor() { + return n instanceof Color; + } + + public Color getColor() { + return (Color) n; + } + + public boolean isAttribute() { + return n instanceof Attribute; + } + + public Attribute getAttribute() { + return (Attribute) n; + } + + public boolean isBackground() { + return background; + } + } + + private AnsiRenderer() {} +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/AnsiType.java b/jansi-core/src/main/java/org/fusesource/jansi/AnsiType.java new file mode 100644 index 000000000..be705a9fa --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/AnsiType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +/** + * Processor type. + * + * @since 2.1 + */ +public enum AnsiType { + Native("Supports ansi sequences natively"), + Unsupported("Ansi sequences are stripped out"), + VirtualTerminal("Supported through windows virtual terminal"), + Emulation("Emulated through using windows API console commands"), + Redirected("The stream is redirected to a file or a pipe"); + + private final String description; + + AnsiType(String description) { + this.description = description; + } + + String getDescription() { + return description; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/WindowsSupport.java b/jansi-core/src/main/java/org/fusesource/jansi/WindowsSupport.java new file mode 100644 index 000000000..15c12322c --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi; + +@Deprecated +public class WindowsSupport { + + @Deprecated + public static String getLastErrorMessage() { + throw new UnsupportedOperationException(); + } + + @Deprecated + public static String getErrorMessage(int errorCode) { + throw new UnsupportedOperationException(); + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java b/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java new file mode 100644 index 000000000..eb2e6f11d --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; + +import org.fusesource.jansi.AnsiColors; +import org.fusesource.jansi.AnsiMode; +import org.fusesource.jansi.AnsiType; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +/** + * A ANSI print stream extracts ANSI escape codes written to + * an output stream and calls corresponding AnsiProcessor.process* methods. + * This particular class is not synchronized for improved performances. + * + *

For more information about ANSI escape codes, see + * Wikipedia article + * + * @since 1.0 + * @see AnsiProcessor + */ +public class AnsiOutputStream extends FilterOutputStream { + + public static final byte[] RESET_CODE = "\033[0m".getBytes(US_ASCII); + + @FunctionalInterface + public interface IoRunnable { + void run() throws IOException; + } + + @FunctionalInterface + public interface WidthSupplier { + int getTerminalWidth(); + } + + public static class ZeroWidthSupplier implements WidthSupplier { + @Override + public int getTerminalWidth() { + return 0; + } + } + + private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0; + private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1; + private static final int LOOKING_FOR_NEXT_ARG = 2; + private static final int LOOKING_FOR_STR_ARG_END = 3; + private static final int LOOKING_FOR_INT_ARG_END = 4; + private static final int LOOKING_FOR_OSC_COMMAND = 5; + private static final int LOOKING_FOR_OSC_COMMAND_END = 6; + private static final int LOOKING_FOR_OSC_PARAM = 7; + private static final int LOOKING_FOR_ST = 8; + private static final int LOOKING_FOR_CHARSET = 9; + + private static final int FIRST_ESC_CHAR = 27; + private static final int SECOND_ESC_CHAR = '['; + private static final int SECOND_OSC_CHAR = ']'; + private static final int BEL = 7; + private static final int SECOND_ST_CHAR = '\\'; + private static final int SECOND_CHARSET0_CHAR = '('; + private static final int SECOND_CHARSET1_CHAR = ')'; + + private AnsiProcessor ap; + private static final int MAX_ESCAPE_SEQUENCE_LENGTH = 100; + private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH]; + private int pos = 0; + private int startOfValue; + private final ArrayList options = new ArrayList<>(); + private int state = LOOKING_FOR_FIRST_ESC_CHAR; + private final Charset cs; + + private final WidthSupplier width; + private final AnsiProcessor processor; + private final AnsiType type; + private final AnsiColors colors; + private final IoRunnable installer; + private final IoRunnable uninstaller; + private AnsiMode mode; + private boolean resetAtUninstall; + + public AnsiOutputStream( + OutputStream os, + WidthSupplier width, + AnsiMode mode, + AnsiProcessor processor, + AnsiType type, + AnsiColors colors, + Charset cs, + IoRunnable installer, + IoRunnable uninstaller, + boolean resetAtUninstall) { + super(os); + this.width = width; + this.processor = processor; + this.type = type; + this.colors = colors; + this.installer = installer; + this.uninstaller = uninstaller; + this.resetAtUninstall = resetAtUninstall; + this.cs = cs; + setMode(mode); + } + + public int getTerminalWidth() { + return width.getTerminalWidth(); + } + + public AnsiType getType() { + return type; + } + + public AnsiColors getColors() { + return colors; + } + + public AnsiMode getMode() { + return mode; + } + + public final void setMode(AnsiMode mode) { + ap = mode == AnsiMode.Strip + ? new AnsiProcessor(out) + : mode == AnsiMode.Force || processor == null ? new ColorsAnsiProcessor(out, colors) : processor; + this.mode = mode; + } + + public boolean isResetAtUninstall() { + return resetAtUninstall; + } + + public void setResetAtUninstall(boolean resetAtUninstall) { + this.resetAtUninstall = resetAtUninstall; + } + + /** + * {@inheritDoc} + */ + @Override + public void write(int data) throws IOException { + switch (state) { + case LOOKING_FOR_FIRST_ESC_CHAR: + if (data == FIRST_ESC_CHAR) { + buffer[pos++] = (byte) data; + state = LOOKING_FOR_SECOND_ESC_CHAR; + } else { + out.write(data); + } + break; + + case LOOKING_FOR_SECOND_ESC_CHAR: + buffer[pos++] = (byte) data; + if (data == SECOND_ESC_CHAR) { + state = LOOKING_FOR_NEXT_ARG; + } else if (data == SECOND_OSC_CHAR) { + state = LOOKING_FOR_OSC_COMMAND; + } else if (data == SECOND_CHARSET0_CHAR) { + options.add(0); + state = LOOKING_FOR_CHARSET; + } else if (data == SECOND_CHARSET1_CHAR) { + options.add(1); + state = LOOKING_FOR_CHARSET; + } else { + reset(false); + } + break; + + case LOOKING_FOR_NEXT_ARG: + buffer[pos++] = (byte) data; + if ('"' == data) { + startOfValue = pos - 1; + state = LOOKING_FOR_STR_ARG_END; + } else if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_INT_ARG_END; + } else if (';' == data) { + options.add(null); + } else if ('?' == data) { + options.add('?'); + } else if ('=' == data) { + options.add('='); + } else { + processEscapeCommand(data); + } + break; + default: + break; + + case LOOKING_FOR_INT_ARG_END: + buffer[pos++] = (byte) data; + if (!('0' <= data && data <= '9')) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + processEscapeCommand(data); + } + } + break; + + case LOOKING_FOR_STR_ARG_END: + buffer[pos++] = (byte) data; + if ('"' != data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + processEscapeCommand(data); + } + } + break; + + case LOOKING_FOR_OSC_COMMAND: + buffer[pos++] = (byte) data; + if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_OSC_COMMAND_END; + } else { + reset(false); + } + break; + + case LOOKING_FOR_OSC_COMMAND_END: + buffer[pos++] = (byte) data; + if (';' == data) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + startOfValue = pos; + state = LOOKING_FOR_OSC_PARAM; + } else if ('0' <= data && data <= '9') { + // already pushed digit to buffer, just keep looking + } else { + // oops, did not expect this + reset(false); + } + break; + + case LOOKING_FOR_OSC_PARAM: + buffer[pos++] = (byte) data; + if (BEL == data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs); + options.add(value); + processOperatingSystemCommand(); + } else if (FIRST_ESC_CHAR == data) { + state = LOOKING_FOR_ST; + } else { + // just keep looking while adding text + } + break; + + case LOOKING_FOR_ST: + buffer[pos++] = (byte) data; + if (SECOND_ST_CHAR == data) { + String value = new String(buffer, startOfValue, (pos - 2) - startOfValue, cs); + options.add(value); + processOperatingSystemCommand(); + } else { + state = LOOKING_FOR_OSC_PARAM; + } + break; + + case LOOKING_FOR_CHARSET: + options.add((char) data); + processCharsetSelect(); + break; + } + + // Is it just too long? + if (pos >= buffer.length) { + reset(false); + } + } + + private void processCharsetSelect() throws IOException { + try { + reset(ap != null && ap.processCharsetSelect(options)); + } catch (RuntimeException e) { + reset(true); + throw e; + } + } + + private void processOperatingSystemCommand() throws IOException { + try { + reset(ap != null && ap.processOperatingSystemCommand(options)); + } catch (RuntimeException e) { + reset(true); + throw e; + } + } + + private void processEscapeCommand(int data) throws IOException { + try { + reset(ap != null && ap.processEscapeCommand(options, data)); + } catch (RuntimeException e) { + reset(true); + throw e; + } + } + + /** + * Resets all state to continue with regular parsing + * @param skipBuffer if current buffer should be skipped or written to out + * @throws IOException + */ + private void reset(boolean skipBuffer) throws IOException { + if (!skipBuffer) { + out.write(buffer, 0, pos); + } + pos = 0; + startOfValue = 0; + options.clear(); + state = LOOKING_FOR_FIRST_ESC_CHAR; + } + + public void install() throws IOException { + if (installer != null) { + installer.run(); + } + } + + public void uninstall() throws IOException { + if (resetAtUninstall && type != AnsiType.Redirected && type != AnsiType.Unsupported) { + setMode(AnsiMode.Default); + write(RESET_CODE); + flush(); + } + if (uninstaller != null) { + uninstaller.run(); + } + } + + @Override + public void close() throws IOException { + uninstall(); + super.close(); + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiProcessor.java b/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiProcessor.java new file mode 100644 index 000000000..24c3825a4 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/AnsiProcessor.java @@ -0,0 +1,552 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * ANSI processor providing process* corresponding to ANSI escape codes. + * This class methods implementations are empty: subclasses should actually perform the + * ANSI escape behaviors by implementing active code in process* methods. + * + *

For more information about ANSI escape codes, see + * Wikipedia article + * + * @since 1.19 + */ +@SuppressWarnings("unused") +public class AnsiProcessor { + protected final OutputStream os; + + public AnsiProcessor(OutputStream os) { + this.os = os; + } + + /** + * Helper for processEscapeCommand() to iterate over integer options + * @param optionsIterator the underlying iterator + * @throws IOException if no more non-null values left + */ + protected int getNextOptionInt(Iterator optionsIterator) throws IOException { + for (; ; ) { + if (!optionsIterator.hasNext()) throw new IllegalArgumentException(); + Object arg = optionsIterator.next(); + if (arg != null) return (Integer) arg; + } + } + + /** + * @return true if the escape command was processed. + */ + protected boolean processEscapeCommand(ArrayList options, int command) throws IOException { + try { + switch (command) { + case 'A': + processCursorUp(optionInt(options, 0, 1)); + return true; + case 'B': + processCursorDown(optionInt(options, 0, 1)); + return true; + case 'C': + processCursorRight(optionInt(options, 0, 1)); + return true; + case 'D': + processCursorLeft(optionInt(options, 0, 1)); + return true; + case 'E': + processCursorDownLine(optionInt(options, 0, 1)); + return true; + case 'F': + processCursorUpLine(optionInt(options, 0, 1)); + return true; + case 'G': + processCursorToColumn(optionInt(options, 0)); + return true; + case 'H': + case 'f': + processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1)); + return true; + case 'J': + processEraseScreen(optionInt(options, 0, 0)); + return true; + case 'K': + processEraseLine(optionInt(options, 0, 0)); + return true; + case 'L': + processInsertLine(optionInt(options, 0, 1)); + return true; + case 'M': + processDeleteLine(optionInt(options, 0, 1)); + return true; + case 'S': + processScrollUp(optionInt(options, 0, 1)); + return true; + case 'T': + processScrollDown(optionInt(options, 0, 1)); + return true; + case 'm': + // Validate all options are ints... + for (Object next : options) { + if (next != null && next.getClass() != Integer.class) { + throw new IllegalArgumentException(); + } + } + + int count = 0; + Iterator optionsIterator = options.iterator(); + while (optionsIterator.hasNext()) { + Object next = optionsIterator.next(); + if (next != null) { + count++; + int value = (Integer) next; + if (30 <= value && value <= 37) { + processSetForegroundColor(value - 30); + } else if (40 <= value && value <= 47) { + processSetBackgroundColor(value - 40); + } else if (90 <= value && value <= 97) { + processSetForegroundColor(value - 90, true); + } else if (100 <= value && value <= 107) { + processSetBackgroundColor(value - 100, true); + } else if (value == 38 || value == 48) { + if (!optionsIterator.hasNext()) { + continue; + } + // extended color like `esc[38;5;m` or `esc[38;2;;;m` + int arg2or5 = getNextOptionInt(optionsIterator); + if (arg2or5 == 2) { + // 24 bit color style like `esc[38;2;;;m` + int r = getNextOptionInt(optionsIterator); + int g = getNextOptionInt(optionsIterator); + int b = getNextOptionInt(optionsIterator); + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + if (value == 38) processSetForegroundColorExt(r, g, b); + else processSetBackgroundColorExt(r, g, b); + } else { + throw new IllegalArgumentException(); + } + } else if (arg2or5 == 5) { + // 256 color style like `esc[38;5;m` + int paletteIndex = getNextOptionInt(optionsIterator); + if (paletteIndex >= 0 && paletteIndex <= 255) { + if (value == 38) processSetForegroundColorExt(paletteIndex); + else processSetBackgroundColorExt(paletteIndex); + } else { + throw new IllegalArgumentException(); + } + } else { + throw new IllegalArgumentException(); + } + } else { + switch (value) { + case 39: + processDefaultTextColor(); + break; + case 49: + processDefaultBackgroundColor(); + break; + case 0: + processAttributeReset(); + break; + default: + processSetAttribute(value); + } + } + } + } + if (count == 0) { + processAttributeReset(); + } + return true; + case 's': + processSaveCursorPosition(); + return true; + case 'u': + processRestoreCursorPosition(); + return true; + + default: + if ('a' <= command && command <= 'z') { + processUnknownExtension(options, command); + return true; + } + if ('A' <= command && command <= 'Z') { + processUnknownExtension(options, command); + return true; + } + return false; + } + } catch (IllegalArgumentException ignore) { + } + return false; + } + + /** + * @return true if the operating system command was processed. + */ + protected boolean processOperatingSystemCommand(ArrayList options) { + int command = optionInt(options, 0); + String label = (String) options.get(1); + // for command > 2 label could be composed (i.e. contain ';'), but we'll leave + // it to processUnknownOperatingSystemCommand implementations to handle that + try { + switch (command) { + case 0: + processChangeIconNameAndWindowTitle(label); + return true; + case 1: + processChangeIconName(label); + return true; + case 2: + processChangeWindowTitle(label); + return true; + + default: + // not exactly unknown, but not supported through dedicated process methods: + processUnknownOperatingSystemCommand(command, label); + return true; + } + } catch (IllegalArgumentException ignore) { + } + return false; + } + + /** + * Process character set sequence. + * @param options options + * @return true if the charcter set select command was processed. + */ + protected boolean processCharsetSelect(ArrayList options) { + int set = optionInt(options, 0); + char seq = (Character) options.get(1); + processCharsetSelect(set, seq); + return true; + } + + private int optionInt(ArrayList options, int index) { + if (options.size() <= index) throw new IllegalArgumentException(); + Object value = options.get(index); + if (value == null) throw new IllegalArgumentException(); + if (!value.getClass().equals(Integer.class)) throw new IllegalArgumentException(); + return (Integer) value; + } + + private int optionInt(ArrayList options, int index, int defaultValue) { + if (options.size() > index) { + Object value = options.get(index); + if (value == null) { + return defaultValue; + } + return (Integer) value; + } + return defaultValue; + } + + /** + * Process CSI u ANSI code, corresponding to RCP – Restore Cursor Position + * @throws IOException IOException + */ + protected void processRestoreCursorPosition() throws IOException {} + + /** + * Process CSI s ANSI code, corresponding to SCP – Save Cursor Position + * @throws IOException IOException + */ + protected void processSaveCursorPosition() throws IOException {} + + /** + * Process CSI L ANSI code, corresponding to IL – Insert Line + * @param optionInt option + * @throws IOException IOException + * @since 1.16 + */ + protected void processInsertLine(int optionInt) throws IOException {} + + /** + * Process CSI M ANSI code, corresponding to DL – Delete Line + * @param optionInt option + * @throws IOException IOException + * @since 1.16 + */ + protected void processDeleteLine(int optionInt) throws IOException {} + + /** + * Process CSI n T ANSI code, corresponding to SD – Scroll Down + * @param optionInt option + * @throws IOException IOException + */ + protected void processScrollDown(int optionInt) throws IOException {} + + /** + * Process CSI n U ANSI code, corresponding to SU – Scroll Up + * @param optionInt option + * @throws IOException IOException + */ + protected void processScrollUp(int optionInt) throws IOException {} + + protected static final int ERASE_SCREEN_TO_END = 0; + protected static final int ERASE_SCREEN_TO_BEGINING = 1; + protected static final int ERASE_SCREEN = 2; + + /** + * Process CSI n J ANSI code, corresponding to ED – Erase in Display + * @param eraseOption eraseOption + * @throws IOException IOException + */ + protected void processEraseScreen(int eraseOption) throws IOException {} + + protected static final int ERASE_LINE_TO_END = 0; + protected static final int ERASE_LINE_TO_BEGINING = 1; + protected static final int ERASE_LINE = 2; + + /** + * Process CSI n K ANSI code, corresponding to ED – Erase in Line + * @param eraseOption eraseOption + * @throws IOException IOException + */ + protected void processEraseLine(int eraseOption) throws IOException {} + + protected static final int ATTRIBUTE_INTENSITY_BOLD = 1; // Intensity: Bold + protected static final int ATTRIBUTE_INTENSITY_FAINT = 2; // Intensity; Faint not widely supported + protected static final int ATTRIBUTE_ITALIC = 3; // Italic; on not widely supported. Sometimes treated as inverse. + protected static final int ATTRIBUTE_UNDERLINE = 4; // Underline; Single + protected static final int ATTRIBUTE_BLINK_SLOW = 5; // Blink; Slow less than 150 per minute + protected static final int ATTRIBUTE_BLINK_FAST = 6; // Blink; Rapid MS-DOS ANSI.SYS; 150 per minute or more + protected static final int ATTRIBUTE_NEGATIVE_ON = + 7; // Image; Negative inverse or reverse; swap foreground and background + protected static final int ATTRIBUTE_CONCEAL_ON = 8; // Conceal on + protected static final int ATTRIBUTE_UNDERLINE_DOUBLE = 21; // Underline; Double not widely supported + protected static final int ATTRIBUTE_INTENSITY_NORMAL = 22; // Intensity; Normal not bold and not faint + protected static final int ATTRIBUTE_UNDERLINE_OFF = 24; // Underline; None + protected static final int ATTRIBUTE_BLINK_OFF = 25; // Blink; off + protected static final int ATTRIBUTE_NEGATIVE_OFF = 27; // Image; Positive + protected static final int ATTRIBUTE_CONCEAL_OFF = 28; // Reveal conceal off + + /** + * process SGR other than 0 (reset), 30-39 (foreground), + * 40-49 (background), 90-97 (foreground high intensity) or + * 100-107 (background high intensity) + * @param attribute attribute + * @throws IOException IOException + * @see #processAttributeReset() + * @see #processSetForegroundColor(int) + * @see #processSetForegroundColor(int, boolean) + * @see #processSetForegroundColorExt(int) + * @see #processSetForegroundColorExt(int, int, int) + * @see #processDefaultTextColor() + * @see #processDefaultBackgroundColor() + */ + protected void processSetAttribute(int attribute) throws IOException {} + + protected static final int BLACK = 0; + protected static final int RED = 1; + protected static final int GREEN = 2; + protected static final int YELLOW = 3; + protected static final int BLUE = 4; + protected static final int MAGENTA = 5; + protected static final int CYAN = 6; + protected static final int WHITE = 7; + + /** + * process SGR 30-37 corresponding to Set text color (foreground). + * @param color the text color + * @throws IOException IOException + */ + protected void processSetForegroundColor(int color) throws IOException { + processSetForegroundColor(color, false); + } + + /** + * process SGR 30-37 or SGR 90-97 corresponding to + * Set text color (foreground) either in normal mode or high intensity. + * @param color the text color + * @param bright is high intensity? + * @throws IOException IOException + */ + protected void processSetForegroundColor(int color, boolean bright) throws IOException {} + + /** + * process SGR 38 corresponding to extended set text color (foreground) + * with a palette of 255 colors. + * @param paletteIndex the text color in the palette + * @throws IOException IOException + */ + protected void processSetForegroundColorExt(int paletteIndex) throws IOException {} + + /** + * process SGR 38 corresponding to extended set text color (foreground) + * with a 24 bits RGB definition of the color. + * @param r red + * @param g green + * @param b blue + * @throws IOException IOException + */ + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException {} + + /** + * process SGR 40-47 corresponding to Set background color. + * @param color the background color + * @throws IOException IOException + */ + protected void processSetBackgroundColor(int color) throws IOException { + processSetBackgroundColor(color, false); + } + + /** + * process SGR 40-47 or SGR 100-107 corresponding to + * Set background color either in normal mode or high intensity. + * @param color the background color + * @param bright is high intensity? + * @throws IOException IOException + */ + protected void processSetBackgroundColor(int color, boolean bright) throws IOException {} + + /** + * process SGR 48 corresponding to extended set background color + * with a palette of 255 colors. + * @param paletteIndex the background color in the palette + * @throws IOException IOException + */ + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException {} + + /** + * process SGR 48 corresponding to extended set background color + * with a 24 bits RGB definition of the color. + * @param r red + * @param g green + * @param b blue + * @throws IOException IOException + */ + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException {} + + /** + * process SGR 39 corresponding to Default text color (foreground) + * @throws IOException IOException + */ + protected void processDefaultTextColor() throws IOException {} + + /** + * process SGR 49 corresponding to Default background color + * @throws IOException IOException + */ + protected void processDefaultBackgroundColor() throws IOException {} + + /** + * process SGR 0 corresponding to Reset / Normal + * @throws IOException IOException + */ + protected void processAttributeReset() throws IOException {} + + /** + * process CSI n ; m H corresponding to CUP – Cursor Position or + * CSI n ; m f corresponding to HVP – Horizontal and Vertical Position + * @param row row + * @param col col + * @throws IOException IOException + */ + protected void processCursorTo(int row, int col) throws IOException {} + + /** + * process CSI n G corresponding to CHA – Cursor Horizontal Absolute + * @param x the column + * @throws IOException IOException + */ + protected void processCursorToColumn(int x) throws IOException {} + + /** + * process CSI n F corresponding to CPL – Cursor Previous Line + * @param count line count + * @throws IOException IOException + */ + protected void processCursorUpLine(int count) throws IOException {} + + /** + * process CSI n E corresponding to CNL – Cursor Next Line + * @param count line count + * @throws IOException IOException + */ + protected void processCursorDownLine(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + os.write('\n'); + } + } + + /** + * process CSI n D corresponding to CUB – Cursor Back + * @param count count + * @throws IOException IOException + */ + protected void processCursorLeft(int count) throws IOException {} + + /** + * process CSI n C corresponding to CUF – Cursor Forward + * @param count count + * @throws IOException IOException + */ + protected void processCursorRight(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + os.write(' '); + } + } + + /** + * process CSI n B corresponding to CUD – Cursor Down + * @param count count + * @throws IOException IOException + */ + protected void processCursorDown(int count) throws IOException {} + + /** + * process CSI n A corresponding to CUU – Cursor Up + * @param count count + * @throws IOException IOException + */ + protected void processCursorUp(int count) throws IOException {} + + /** + * Process Unknown Extension + * @param options options + * @param command command + */ + protected void processUnknownExtension(ArrayList options, int command) {} + + /** + * process OSC 0;text BEL corresponding to Change Window and Icon label + * @param label window title name + */ + protected void processChangeIconNameAndWindowTitle(String label) { + processChangeIconName(label); + processChangeWindowTitle(label); + } + + /** + * process OSC 1;text BEL corresponding to Change Icon label + * @param label icon label + */ + protected void processChangeIconName(String label) {} + + /** + * process OSC 2;text BEL corresponding to Change Window title + * @param label window title text + */ + protected void processChangeWindowTitle(String label) {} + + /** + * Process unknown OSC command. + * @param command command + * @param param param + */ + protected void processUnknownOperatingSystemCommand(int command, String param) {} + + protected void processCharsetSelect(int set, char seq) {} +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/Colors.java b/jansi-core/src/main/java/org/fusesource/jansi/io/Colors.java new file mode 100644 index 000000000..e777436e9 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/Colors.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +/** + * Helper class for dealing with color rounding. + * This is a simplified version of the JLine's one at + * https://github.com/jline/jline3/blob/a24636dc5de83baa6b65049e8215fb372433b3b1/terminal/src/main/java/org/jline/utils/Colors.java + */ +public class Colors { + + /** + * Default 256 colors palette + */ + public static final int[] DEFAULT_COLORS_256 = org.jline.utils.Colors.DEFAULT_COLORS_256; + + public static int roundColor(int col, int max) { + return org.jline.utils.Colors.roundColor(col, max); + } + + public static int roundRgbColor(int r, int g, int b, int max) { + return org.jline.utils.Colors.roundRgbColor(r, g, b, max); + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/ColorsAnsiProcessor.java b/jansi-core/src/main/java/org/fusesource/jansi/io/ColorsAnsiProcessor.java new file mode 100644 index 000000000..c817a2461 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/ColorsAnsiProcessor.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; + +import org.fusesource.jansi.AnsiColors; + +/** + * Ansi processor to process color conversion if needed. + */ +public class ColorsAnsiProcessor extends AnsiProcessor { + + private final AnsiColors colors; + + public ColorsAnsiProcessor(OutputStream os, AnsiColors colors) { + super(os); + this.colors = colors; + } + + @Override + protected boolean processEscapeCommand(ArrayList options, int command) throws IOException { + if (command == 'm' && (colors == AnsiColors.Colors256 || colors == AnsiColors.Colors16)) { + // Validate all options are ints... + boolean has38or48 = false; + for (Object next : options) { + if (next != null && next.getClass() != Integer.class) { + throw new IllegalArgumentException(); + } + Integer value = (Integer) next; + has38or48 |= value == 38 || value == 48; + } + // SGR commands do not contain an extended color, so just transfer the buffer + if (!has38or48) { + return false; + } + StringBuilder sb = new StringBuilder(32); + sb.append('\033').append('['); + boolean first = true; + Iterator optionsIterator = options.iterator(); + while (optionsIterator.hasNext()) { + Object next = optionsIterator.next(); + if (next != null) { + int value = (Integer) next; + if (value == 38 || value == 48) { + // extended color like `esc[38;5;m` or `esc[38;2;;;m` + int arg2or5 = getNextOptionInt(optionsIterator); + if (arg2or5 == 2) { + // 24 bit color style like `esc[38;2;;;m` + int r = getNextOptionInt(optionsIterator); + int g = getNextOptionInt(optionsIterator); + int b = getNextOptionInt(optionsIterator); + if (colors == AnsiColors.Colors256) { + int col = Colors.roundRgbColor(r, g, b, 256); + if (!first) { + sb.append(';'); + } + first = false; + sb.append(value); + sb.append(';'); + sb.append(5); + sb.append(';'); + sb.append(col); + } else { + int col = Colors.roundRgbColor(r, g, b, 16); + if (!first) { + sb.append(';'); + } + first = false; + sb.append( + value == 38 + ? col >= 8 ? 90 + col - 8 : 30 + col + : col >= 8 ? 100 + col - 8 : 40 + col); + } + } else if (arg2or5 == 5) { + // 256 color style like `esc[38;5;m` + int paletteIndex = getNextOptionInt(optionsIterator); + if (colors == AnsiColors.Colors256) { + if (!first) { + sb.append(';'); + } + first = false; + sb.append(value); + sb.append(';'); + sb.append(5); + sb.append(';'); + sb.append(paletteIndex); + } else { + int col = Colors.roundColor(paletteIndex, 16); + if (!first) { + sb.append(';'); + } + first = false; + sb.append( + value == 38 + ? col >= 8 ? 90 + col - 8 : 30 + col + : col >= 8 ? 100 + col - 8 : 40 + col); + } + } else { + throw new IllegalArgumentException(); + } + } else { + if (!first) { + sb.append(';'); + } + first = false; + sb.append(value); + } + } + } + sb.append('m'); + os.write(sb.toString().getBytes()); + return true; + + } else { + return false; + } + } + + @Override + protected boolean processOperatingSystemCommand(ArrayList options) { + return false; + } + + @Override + protected boolean processCharsetSelect(ArrayList options) { + return false; + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java b/jansi-core/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java new file mode 100644 index 000000000..be4f3bea6 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple buffering output stream with no synchronization. + */ +public class FastBufferedOutputStream extends FilterOutputStream { + + protected final byte[] buf = new byte[8192]; + protected int count; + + public FastBufferedOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + if (count >= buf.length) { + flushBuffer(); + } + buf[count++] = (byte) b; + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + if (len >= buf.length) { + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buf.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buf, count, len); + count += len; + } + + private void flushBuffer() throws IOException { + if (count > 0) { + out.write(buf, 0, count); + count = 0; + } + } + + @Override + public void flush() throws IOException { + flushBuffer(); + out.flush(); + } +} diff --git a/jansi-core/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java b/jansi-core/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java new file mode 100644 index 000000000..8938d9cf1 --- /dev/null +++ b/jansi-core/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.fusesource.jansi.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public final class WindowsAnsiProcessor extends AnsiProcessor { + + public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { + super(ps); + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + super(ps); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + super(ps); + } +} diff --git a/jansi-core/src/main/resources/META-INF/native-image/jansi/native-image.properties b/jansi-core/src/main/resources/META-INF/native-image/jansi/native-image.properties new file mode 100644 index 000000000..6cc8f4e42 --- /dev/null +++ b/jansi-core/src/main/resources/META-INF/native-image/jansi/native-image.properties @@ -0,0 +1 @@ +Args=--features=org.fusesource.jansi.internal.NativeImageFeature \ No newline at end of file diff --git a/jansi-core/src/main/resources/META-INF/native-image/jansi/resource-config.json b/jansi-core/src/main/resources/META-INF/native-image/jansi/resource-config.json new file mode 100644 index 000000000..794d89967 --- /dev/null +++ b/jansi-core/src/main/resources/META-INF/native-image/jansi/resource-config.json @@ -0,0 +1,6 @@ +{ + "resources": [ + {"pattern": "org/fusesource/jansi/jansi.properties"}, + {"pattern": "org/fusesource/jansi/jansi.txt"} + ] +} \ No newline at end of file diff --git a/jansi-core/src/main/resources/org/fusesource/jansi/jansi.properties b/jansi-core/src/main/resources/org/fusesource/jansi/jansi.properties new file mode 100644 index 000000000..defbd4820 --- /dev/null +++ b/jansi-core/src/main/resources/org/fusesource/jansi/jansi.properties @@ -0,0 +1 @@ +version=${project.version} diff --git a/jansi-core/src/main/resources/org/fusesource/jansi/jansi.txt b/jansi-core/src/main/resources/org/fusesource/jansi/jansi.txt new file mode 100644 index 000000000..da485b9c3 --- /dev/null +++ b/jansi-core/src/main/resources/org/fusesource/jansi/jansi.txt @@ -0,0 +1,8 @@ +[?7h +┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ +│██├┘█████└┬┘█████└┬┘██████│▐▌│ +┌──┐ │██│██▄▄▄██│██┌─┐██│██▄▄▄▄ │▄▄│ +│▒▒└─┘▒█│▒█┌─┐▒█│▒█│ │▒█│ ▀▀▀▀▒█│▒█│ +└┐▓▓▓▓▓┌┤▓▓│ │▓▓│▓▓│ │▓▓│▀▓▓▓▓▓▀│▓▓│ +└─────┘└──┘ └──┴──┘ └──┴───────┴──┘ + diff --git a/jansi/pom.xml b/jansi/pom.xml new file mode 100644 index 000000000..732929585 --- /dev/null +++ b/jansi/pom.xml @@ -0,0 +1,230 @@ + + + + + 4.0.0 + + + org.jline + jline-parent + 3.23.1-SNAPSHOT + + + jansi + Jansi bundle + + + org.jansi + --enable-preview --release 21 + + + + + org.jline + jansi-core + true + + + org.jline + jline-terminal + true + + + org.jline + jline-terminal-jni + true + + + org.jline + jline-terminal-ffm + test + + + + + + + maven-dependency-plugin + + + + unpack + + process-sources + + + + + org.jline + jline-native + sources + jar + false + ${project.build.directory}/generated-sources + + + org.jline + jline-terminal + sources + jar + false + ${project.build.directory}/generated-sources + + + org.jline + jline-terminal-jni + sources + jar + false + ${project.build.directory}/generated-sources + + + org.jline + jansi-core + sources + jar + false + ${project.build.directory}/generated-sources + + + + + org.jline + jline-native + jar + false + ${project.build.directory}/generated-resources + **/*.class + + + org.jline + jline-terminal + jar + false + ${project.build.directory}/generated-resources + **/*.class + + + org.jline + jline-terminal-jni + jar + false + ${project.build.directory}/generated-resources + **/*.class + + + org.jline + jline-terminal-ffm + jar + false + ${project.build.directory}/generated-resources + **/*.class + + + org.jline + jansi-core + jar + false + ${project.build.directory}/generated-resources + **/*.class + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + + add-source + + generate-sources + + + ${project.build.directory}/generated-sources + + + + + add-resource + + add-resource + + generate-resources + + + + ${project.build.directory}/generated-resources + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + true + + + + default-compile + + + **/ffm/*.java + + + -Xlint:all,-options + -Werror + + + + + jdk21 + + compile + + + + **/ffm/*.java + + 21 + + -Xlint:all,-options + -Werror + --enable-preview + + + + + + + org.apache.felix + maven-bundle-plugin + + + org.fusesource.jansi.AnsiMain + ${automatic.module.name} + *;-noimport:=true + + + + + + + diff --git a/native/src/main/java/org/jline/nativ/JLineNativeLoader.java b/native/src/main/java/org/jline/nativ/JLineNativeLoader.java index d6432ce3d..a15f1104d 100644 --- a/native/src/main/java/org/jline/nativ/JLineNativeLoader.java +++ b/native/src/main/java/org/jline/nativ/JLineNativeLoader.java @@ -71,7 +71,7 @@ public static synchronized boolean initialize() { try { loadJLineNativeLibrary(); } catch (Exception e) { - throw new RuntimeException("Unable to load jline native library", e); + throw new RuntimeException("Unable to load jline native library: " + e.getMessage(), e); } return loaded; } diff --git a/pom.xml b/pom.xml index f8c6af67a..2faf3753e 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,7 @@ style demo graal + jansi-core @@ -205,6 +206,18 @@ ${project.version} + + org.jline + jansi-core + ${project.version} + + + + org.jline + jansi + ${project.version} + + org.fusesource.jansi jansi @@ -726,6 +739,7 @@ jline + jansi diff --git a/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java b/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java index 484809b93..6ac524f36 100644 --- a/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java +++ b/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java @@ -33,7 +33,6 @@ import org.jline.terminal.spi.SystemStream; import org.jline.terminal.spi.TerminalProvider; import org.jline.utils.Log; -import org.jline.utils.OSUtils; /** * Builder class to create terminals. @@ -367,12 +366,9 @@ private Terminal doBuild() throws IOException { Charset encoding = computeEncoding(); String type = computeType(); - Boolean dumb = this.dumb; - if (dumb == null) { - dumb = getBoolean(PROP_DUMB, null); - } IllegalStateException exception = new IllegalStateException("Unable to create a terminal"); List providers = getProviders(exception); + Terminal terminal = null; if ((system != null && system) || (system == null && in == null && out == null)) { if (system != null @@ -388,43 +384,6 @@ private Terminal doBuild() throws IOException { .collect(Collectors.toMap( stream -> stream, stream -> providers.stream().anyMatch(p -> p.isSystemStream(stream)))); SystemStream systemStream = select(system, systemOutput); - - if (system.get(SystemStream.Input) && systemStream != null) { - if (attributes != null || size != null) { - Log.warn("Attributes and size fields are ignored when creating a system terminal"); - } - boolean ansiPassThrough = OSUtils.IS_CONEMU; - // Cygwin defaults to XTERM, but actually supports 256 colors, - // so if the value comes from the environment, change it to xterm-256color - if ((OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) - && "xterm".equals(type) - && this.type == null - && System.getProperty(PROP_TYPE) == null) { - type = "xterm-256color"; - } - for (TerminalProvider provider : providers) { - if (terminal == null) { - try { - terminal = provider.sysTerminal( - name, - type, - ansiPassThrough, - encoding, - nativeSignals, - signalHandler, - paused, - systemStream); - } catch (Throwable t) { - Log.debug("Error creating " + provider.name() + " based terminal: ", t.getMessage(), t); - exception.addSuppressed(t); - } - } - } - if (terminal == null && OSUtils.IS_WINDOWS && !jna && !jansi && !jni && (dumb == null || !dumb)) { - throw new IllegalStateException("Unable to create a system terminal. On windows, either " - + "JNA or JANSI library is required. Make sure to add one of those in the classpath."); - } - } if (terminal instanceof AbstractTerminal) { AbstractTerminal t = (AbstractTerminal) terminal; if (SYSTEM_TERMINAL.compareAndSet(null, t)) { @@ -437,6 +396,11 @@ private Terminal doBuild() throws IOException { terminal = null; } } + + Boolean dumb = this.dumb; + if (dumb == null) { + dumb = getBoolean(PROP_DUMB, null); + } if (terminal == null && (dumb == null || dumb)) { if (dumb == null) { if (Log.isDebugEnabled()) { diff --git a/terminal/src/main/java/org/jline/terminal/impl/Diag.java b/terminal/src/main/java/org/jline/terminal/impl/Diag.java index ecef6c13f..8b8852d34 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/Diag.java +++ b/terminal/src/main/java/org/jline/terminal/impl/Diag.java @@ -27,7 +27,7 @@ public static void main(String[] args) { diag(System.out); } - static void diag(PrintStream out) { + public static void diag(PrintStream out) { out.println("System properties"); out.println("================="); out.println("os.name = " + System.getProperty("os.name")); @@ -72,13 +72,13 @@ static void diag(PrintStream out) { } out.println(); - out.println("JansiSupport"); + out.println("Jansi2Support"); out.println("================="); try { TerminalProvider provider = TerminalProvider.load("jansi"); testProvider(out, provider); } catch (Throwable t) { - out.println("Jansi support not available: " + t); + out.println("Jansi 2 support not available: " + t); } out.println(); diff --git a/terminal/src/main/java/org/jline/terminal/impl/ExternalTerminal.java b/terminal/src/main/java/org/jline/terminal/impl/ExternalTerminal.java index 39fa27307..ff0771bc6 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/ExternalTerminal.java +++ b/terminal/src/main/java/org/jline/terminal/impl/ExternalTerminal.java @@ -18,6 +18,7 @@ import org.jline.terminal.Attributes; import org.jline.terminal.Cursor; import org.jline.terminal.Size; +import org.jline.terminal.spi.TerminalProvider; /** * Console implementation with embedded line disciplined. @@ -32,6 +33,7 @@ */ public class ExternalTerminal extends LineDisciplineTerminal { + private final TerminalProvider provider; protected final AtomicBoolean closed = new AtomicBoolean(); protected final InputStream masterInput; protected final Object lock = new Object(); @@ -41,10 +43,11 @@ public class ExternalTerminal extends LineDisciplineTerminal { public ExternalTerminal( String name, String type, InputStream masterInput, OutputStream masterOutput, Charset encoding) throws IOException { - this(name, type, masterInput, masterOutput, encoding, SignalHandler.SIG_DFL); + this(null, name, type, masterInput, masterOutput, encoding, SignalHandler.SIG_DFL); } public ExternalTerminal( + TerminalProvider provider, String name, String type, InputStream masterInput, @@ -52,10 +55,11 @@ public ExternalTerminal( Charset encoding, SignalHandler signalHandler) throws IOException { - this(name, type, masterInput, masterOutput, encoding, signalHandler, false); + this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, false); } public ExternalTerminal( + TerminalProvider provider, String name, String type, InputStream masterInput, @@ -64,11 +68,12 @@ public ExternalTerminal( SignalHandler signalHandler, boolean paused) throws IOException { - this(name, type, masterInput, masterOutput, encoding, signalHandler, paused, null, null); + this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, paused, null, null); } @SuppressWarnings("this-escape") public ExternalTerminal( + TerminalProvider provider, String name, String type, InputStream masterInput, @@ -80,6 +85,7 @@ public ExternalTerminal( Size size) throws IOException { super(name, type, masterOutput, encoding, signalHandler); + this.provider = provider; this.masterInput = masterInput; if (attributes != null) { setAttributes(attributes); @@ -179,4 +185,9 @@ public void pump() { public Cursor getCursorPosition(IntConsumer discarded) { return CursorSupport.getCursorPosition(this, discarded); } + + @Override + public TerminalProvider getProvider() { + return provider; + } } diff --git a/terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java b/terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java index 580a761b3..33e2e4c19 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java +++ b/terminal/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java @@ -112,7 +112,7 @@ public Terminal newTerminal( Attributes attributes, Size size) throws IOException { - return new ExternalTerminal(name, type, in, out, encoding, signalHandler, paused, attributes, size); + return new ExternalTerminal(this, name, type, in, out, encoding, signalHandler, paused, attributes, size); } @Override diff --git a/terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java b/terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java index 5e03677ed..9904661a2 100644 --- a/terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java +++ b/terminal/src/main/java/org/jline/terminal/spi/TerminalProvider.java @@ -68,7 +68,7 @@ static TerminalProvider load(String name) throws IOException { Class clazz = cl.loadClass(className); return (TerminalProvider) clazz.getConstructor().newInstance(); } catch (Exception e) { - throw new IOException("Unable to load terminal provider " + name, e); + throw new IOException("Unable to load terminal provider " + name + ": " + e.getMessage(), e); } } else { throw new IOException("Unable to find terminal provider " + name);