From d340856e78925269ecc76cd16c29850fa9ca01e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Boutemy?= Date: Tue, 19 Sep 2017 00:33:49 +0200 Subject: [PATCH] add AnsiPrintStream and FilterPrintStream to avoid encoding issues --- .../org/fusesource/jansi/AnsiConsole.java | 125 ++- .../org/fusesource/jansi/AnsiPrintStream.java | 757 ++++++++++++++++++ .../fusesource/jansi/FilterPrintStream.java | 244 ++++++ .../jansi/WindowsAnsiPrintStream.java | 369 +++++++++ .../jansi/FilterPrintStreamTest.java | 30 + 5 files changed, 1503 insertions(+), 22 deletions(-) create mode 100644 jansi/src/main/java/org/fusesource/jansi/AnsiPrintStream.java create mode 100644 jansi/src/main/java/org/fusesource/jansi/FilterPrintStream.java create mode 100644 jansi/src/main/java/org/fusesource/jansi/WindowsAnsiPrintStream.java create mode 100644 jansi/src/test/java/org/fusesource/jansi/FilterPrintStreamTest.java diff --git a/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java b/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java index 0b7ae030..5f720d0a 100644 --- a/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -15,8 +15,6 @@ */ package org.fusesource.jansi; -import org.fusesource.jansi.internal.Kernel32; - import static org.fusesource.jansi.internal.CLibrary.STDERR_FILENO; import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; import static org.fusesource.jansi.internal.CLibrary.isatty; @@ -25,8 +23,6 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; import java.util.Locale; /** @@ -34,6 +30,7 @@ * * @author Hiram Chirino * @since 1.0 + * @see #wrapPrintStream(PrintStream, int) */ public class AnsiConsole { @@ -58,24 +55,10 @@ public class AnsiConsole { static final JansiOutputType JANSI_STDOUT_TYPE; static final JansiOutputType JANSI_STDERR_TYPE; static { - String charset = Charset.defaultCharset().name(); - if (IS_WINDOWS && !IS_CYGWIN && !IS_MINGW) { - int codepage = Kernel32.GetConsoleOutputCP(); - //http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html - if (Charset.isSupported("ms" + codepage)) { - charset = "ms" + codepage; - } else if (Charset.isSupported("cp" + codepage)) { - charset = "cp" + codepage; - } - } - try { - out = new PrintStream(wrapOutputStream(system_out), false, charset); - JANSI_STDOUT_TYPE = jansiOutputType; - err = new PrintStream(wrapErrorOutputStream(system_err), false, charset); - JANSI_STDERR_TYPE = jansiOutputType; - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + out = wrapSystemOut(system_out); + JANSI_STDOUT_TYPE = jansiOutputType; + err = wrapSystemErr(system_err); + JANSI_STDERR_TYPE = jansiOutputType; } private static int installed; @@ -83,6 +66,7 @@ public class AnsiConsole { private AnsiConsole() { } + @Deprecated public static OutputStream wrapOutputStream(final OutputStream stream) { try { return wrapOutputStream(stream, STDOUT_FILENO); @@ -91,6 +75,15 @@ public static OutputStream wrapOutputStream(final OutputStream stream) { } } + public static PrintStream wrapSystemOut(final PrintStream ps) { + try { + return wrapPrintStream(ps, STDOUT_FILENO); + } catch (Throwable ignore) { + return wrapPrintStream(ps, 1); + } + } + + @Deprecated public static OutputStream wrapErrorOutputStream(final OutputStream stream) { try { return wrapOutputStream(stream, STDERR_FILENO); @@ -99,6 +92,15 @@ public static OutputStream wrapErrorOutputStream(final OutputStream stream) { } } + public static PrintStream wrapSystemErr(final PrintStream ps) { + try { + return wrapPrintStream(ps, STDERR_FILENO); + } catch (Throwable ignore) { + return wrapPrintStream(ps, 2); + } + } + + @Deprecated public static OutputStream wrapOutputStream(final OutputStream stream, int fileno) { // If the jansi.passthrough property is set, then don't interpret @@ -161,6 +163,83 @@ public void close() throws IOException { }; } + /** + * Wrap PrintStream applying rules in following order: + * + * @param ps original PrintStream to wrap + * @param fileno file descriptor + * @return wrapped PrintStream depending on OS and system properties + */ + public static PrintStream wrapPrintStream(final PrintStream ps, int fileno) { + + // If the jansi.passthrough property is set, then don't interpret + // any of the ansi sequences. + if (Boolean.getBoolean("jansi.passthrough")) { + jansiOutputType = JansiOutputType.PASSTHROUGH; + return ps; + } + + // If the jansi.strip property is set, then we just strip the + // the ansi escapes. + if (Boolean.getBoolean("jansi.strip")) { + jansiOutputType = JansiOutputType.STRIP_ANSI; + return new AnsiPrintStream(ps); + } + + if (IS_WINDOWS && !IS_CYGWIN && !IS_MINGW) { + + // On windows we know the console does not interpret ANSI codes.. + try { + jansiOutputType = JansiOutputType.WINDOWS; + return new WindowsAnsiPrintStream(ps); + } catch (Throwable ignore) { + // this happens when JNA is not in the path.. or + // this happens when the stdout is being redirected to a file. + } + + // Use the AnsiPrintStream to strip out the ANSI escape sequences. + jansiOutputType = JansiOutputType.STRIP_ANSI; + return new AnsiPrintStream(ps); + } + + // We must be on some Unix variant, including Cygwin or MSYS(2) on Windows... + try { + // 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) + boolean forceColored = Boolean.getBoolean("jansi.force"); + // If we can detect that stdout is not a tty.. then setup + // to strip the ANSI sequences.. + if (!forceColored && isatty(fileno) == 0) { + jansiOutputType = JansiOutputType.STRIP_ANSI; + return new AnsiPrintStream(ps); + } + } catch (Throwable ignore) { + // These errors happen if the JNI lib is not available for your platform. + // But since we are on ANSI friendly platform, assume the user is on the console. + } + + // By default we assume your Unix tty can handle ANSI codes. + // Just wrap it up so that when we get closed, we reset the + // attributes. + jansiOutputType = JansiOutputType.RESET_ANSI_AT_CLOSE; + return new FilterPrintStream(ps) { + @Override + public void close() { + ps.print(AnsiPrintStream.RESET_CODE); + ps.flush(); + super.close(); + } + }; + } + /** * If the standard out natively supports ANSI escape codes, then this just * returns System.out, otherwise it will provide an ANSI aware PrintStream @@ -168,6 +247,7 @@ public void close() throws IOException { * sequences. * * @return a PrintStream which is ANSI aware. + * @see #wrapPrintStream(PrintStream, int) */ public static PrintStream out() { return out; @@ -180,6 +260,7 @@ public static PrintStream out() { * sequences. * * @return a PrintStream which is ANSI aware. + * @see #wrapPrintStream(PrintStream, int) */ public static PrintStream err() { return err; diff --git a/jansi/src/main/java/org/fusesource/jansi/AnsiPrintStream.java b/jansi/src/main/java/org/fusesource/jansi/AnsiPrintStream.java new file mode 100644 index 00000000..0d1b7f45 --- /dev/null +++ b/jansi/src/main/java/org/fusesource/jansi/AnsiPrintStream.java @@ -0,0 +1,757 @@ +/* + * Copyright (C) 2009-2017 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * A ANSI print stream extracts ANSI escape codes written to + * a print stream and calls corresponding process* methods. + * + * For more information about ANSI escape codes, see: + * http://en.wikipedia.org/wiki/ANSI_escape_code + * + * This class just filters out the escape codes so that they are not + * sent out to the underlying OutputStream: process* methods + * are empty. Subclasses should actually perform the ANSI escape behaviors + * by implementing active code in process* methods. + * + * @author Hiram Chirino + * @author Joris Kuipers + * @since 1.7 + */ +public class AnsiPrintStream extends FilterPrintStream { + + public static final String RESET_CODE = "\033[0m"; + + public AnsiPrintStream(PrintStream ps) { + super(ps); + } + + private final static 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 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; + + int state = LOOKING_FOR_FIRST_ESC_CHAR; + + 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 = '\\'; + + @Override + protected boolean filter(int data) { + switch (state) { + case LOOKING_FOR_FIRST_ESC_CHAR: + if (data == FIRST_ESC_CHAR) { + buffer[pos++] = (byte) data; + state = LOOKING_FOR_SECOND_ESC_CHAR; + return false; + } + return true; + + 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 { + 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 { + reset(processEscapeCommand(options, 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, Charset.defaultCharset()); + Integer value = new Integer(strValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + reset(processEscapeCommand(options, data)); + } + } + break; + + case LOOKING_FOR_STR_ARG_END: + buffer[pos++] = (byte) data; + if ('"' != data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, Charset.defaultCharset()); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + reset(processEscapeCommand(options, 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, Charset.defaultCharset()); + Integer value = new Integer(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, Charset.defaultCharset()); + options.add(value); + reset(processOperatingSystemCommand(options)); + } 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, Charset.defaultCharset()); + options.add(value); + reset(processOperatingSystemCommand(options)); + } else { + state = LOOKING_FOR_OSC_PARAM; + } + break; + } + + // Is it just too long? + if (pos >= buffer.length) { + reset(false); + } + return false; + } + + /** + * Resets all state to continue with regular parsing + * @param skipBuffer if current buffer should be skipped or written to out + */ + private void reset(boolean skipBuffer) { + if (!skipBuffer) { + ps.write(buffer, 0, pos); + } + pos = 0; + startOfValue = 0; + options.clear(); + state = LOOKING_FOR_FIRST_ESC_CHAR; + } + + /** + * Helper for processEscapeCommand() to iterate over integer options + * @param optionsIterator the underlying iterator + * @throws IOException if no more non-null values left + */ + private int getNextOptionInt(Iterator optionsIterator) throws IOException { + for (;;) { + if (!optionsIterator.hasNext()) + throw new IllegalArgumentException(); + Object arg = optionsIterator.next(); + if (arg != null) + return (Integer) arg; + } + } + + /** + * + * @param options + * @param command + * @return true if the escape command was processed. + */ + private boolean processEscapeCommand(ArrayList options, int command) { + 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) { + // 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: + processAttributeRest(); + break; + default: + processSetAttribute(value); + } + } + } + } + if (count == 0) { + processAttributeRest(); + } + return true; + case 's': + processSaveCursorPosition(); + return true; + case 'u': + processRestoreCursorPosition(); + return true; + + default: + if ('a' <= command && 'z' <= command) { + processUnknownExtension(options, command); + return true; + } + if ('A' <= command && 'Z' <= command) { + processUnknownExtension(options, command); + return true; + } + return false; + } + } catch (IllegalArgumentException ignore) { + } catch (IOException ioe) { + setError(); + } + return false; + } + + /** + * + * @param options + * @return true if the operating system command was processed. + */ + private 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 CSI u ANSI code, corresponding to RCP – Restore Cursor Position + * @throws IOException + */ + protected void processRestoreCursorPosition() throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to SCP – Save Cursor Position + * @throws IOException + */ + protected void processSaveCursorPosition() throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to IL – Insert Line + * @throws IOException + */ + protected void processInsertLine(int optionInt) throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to DL – Delete Line + * @throws IOException + */ + protected void processDeleteLine(int optionInt) throws IOException { + } + + /** + * Process CSI n T ANSI code, corresponding to SD – Scroll Down + * @throws IOException + */ + protected void processScrollDown(int optionInt) throws IOException { + } + + /** + * Process CSI n U ANSI code, corresponding to SU – Scroll Up + * @throws 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 + * @throws 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 + * @throws 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 + @Deprecated + protected static final int ATTRIBUTE_NEGATIVE_Off = 27; // Image; Positive + 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 + * @throws IOException + * @see #processAttributeRest() + * @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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + } + + /** + * process SGR 39 corresponding to Default text color (foreground) + * @throws IOException + */ + protected void processDefaultTextColor() throws IOException { + } + + /** + * process SGR 49 corresponding to Default background color + * @throws IOException + */ + protected void processDefaultBackgroundColor() throws IOException { + } + + /** + * process SGR 0 corresponding to Reset / Normal + * @throws IOException + */ + protected void processAttributeRest() 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 + * @param col + * @throws 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 + */ + protected void processCursorToColumn(int x) throws IOException { + } + + /** + * process CSI n F corresponding to CPL – Cursor Previous Line + * @param count line count + * @throws IOException + */ + protected void processCursorUpLine(int count) throws IOException { + } + + /** + * process CSI n E corresponding to CNL – Cursor Next Line + * @param count line count + * @throws IOException + */ + protected void processCursorDownLine(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + print('\n'); + } + } + + /** + * process CSI n D corresponding to CUB – Cursor Back + * @param count + * @throws IOException + */ + protected void processCursorLeft(int count) throws IOException { + } + + /** + * process CSI n C corresponding to CUF – Cursor Forward + * @param count + * @throws IOException + */ + protected void processCursorRight(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + print(' '); + } + } + + /** + * process CSI n B corresponding to CUD – Cursor Down + * @param count + * @throws IOException + */ + protected void processCursorDown(int count) throws IOException { + } + + /** + * process CSI n A corresponding to CUU – Cursor Up + * @param count + * @throws IOException + */ + protected void processCursorUp(int count) throws IOException { + } + + protected void processUnknownExtension(ArrayList options, int command) { + } + + /** + * process OSC 0;text BEL corresponding to Change Window and Icon label + * @param label + */ + protected void processChangeIconNameAndWindowTitle(String label) { + processChangeIconName(label); + processChangeWindowTitle(label); + } + + /** + * process OSC 1;text BEL corresponding to Change Icon label + * @param label + */ + protected void processChangeIconName(String label) { + } + + /** + * process OSC 2;text BEL corresponding to Change Window title + * @param label + */ + protected void processChangeWindowTitle(String label) { + } + + /** + * Process unknown OSC command. + * @param command + * @param param + */ + protected void processUnknownOperatingSystemCommand(int command, String param) { + } + + 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; + } + + @Override + public void close() { + ps.print(RESET_CODE); + flush(); + super.close(); + } + +} diff --git a/jansi/src/main/java/org/fusesource/jansi/FilterPrintStream.java b/jansi/src/main/java/org/fusesource/jansi/FilterPrintStream.java new file mode 100644 index 00000000..2084e9af --- /dev/null +++ b/jansi/src/main/java/org/fusesource/jansi/FilterPrintStream.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2009-2017 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * A PrintStream filtering to another PrintStream, without making any assumption about encoding. + * + * @author Hervé Boutemy + * @since 1.17 + */ +public class FilterPrintStream extends PrintStream +{ + private static final String NEWLINE = System.getProperty("line.separator"); + protected final PrintStream ps; + + public FilterPrintStream(PrintStream ps) + { + super( new OutputStream() { + + @Override + public void write(int b) throws IOException { + throw new RuntimeException("Direct OutputStream use forbidden: must go through delegate PrintStream"); + } + + }); + this.ps = ps; + } + + /** + * Filter the content + * @param data + * @return true if the data is not filtered then has to be printed to delegate PrintStream + */ + protected boolean filter(int data) + { + return true; + } + + @Override + public void write(int data) + { + if (filter(data)) + { + ps.write(data); + } + } + + @Override + public void write(byte[] buf, int off, int len) + { + for (int i = 0; i < len; i++) + { + write(buf[off + i]); + } + } + + @Override + public boolean checkError() + { + return super.checkError() || ps.checkError(); + } + + @Override + public void close() + { + super.close(); + ps.close(); + } + + @Override + public void flush() + { + super.flush(); + ps.flush(); + } + + private void write(char buf[]) { + for (char c : buf) + { + if (filter(c)) + { + ps.print(c); + } + } + } + + private void write(String s) { + char[] buf = new char[s.length()]; + s.getChars(0, s.length(), buf, 0); + write(buf); + } + + private void newLine() { + write(NEWLINE); + } + + /* Methods that do not terminate lines */ + + @Override + public void print(boolean b) { + write(b ? "true" : "false"); + } + + @Override + public void print(char c) { + write(String.valueOf(c)); + } + + @Override + public void print(int i) { + write(String.valueOf(i)); + } + + @Override + public void print(long l) { + write(String.valueOf(l)); + } + + @Override + public void print(float f) { + write(String.valueOf(f)); + } + + @Override + public void print(double d) { + write(String.valueOf(d)); + } + + @Override + public void print(char s[]) { + write(s); + } + + @Override + public void print(String s) { + if (s == null) { + s = "null"; + } + write(s); + } + + @Override + public void print(Object obj) { + write(String.valueOf(obj)); + } + + + /* Methods that do terminate lines */ + + @Override + public void println() { + newLine(); + } + + @Override + public void println(boolean x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(char x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(int x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(long x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(float x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(double x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(char x[]) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(String x) { + synchronized (this) { + print(x); + newLine(); + } + } + + @Override + public void println(Object x) { + String s = String.valueOf(x); + synchronized (this) { + print(s); + newLine(); + } + } +} diff --git a/jansi/src/main/java/org/fusesource/jansi/WindowsAnsiPrintStream.java b/jansi/src/main/java/org/fusesource/jansi/WindowsAnsiPrintStream.java new file mode 100644 index 00000000..84c0dff2 --- /dev/null +++ b/jansi/src/main/java/org/fusesource/jansi/WindowsAnsiPrintStream.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2009-2017 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; +import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; +import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; +import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; +import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; +import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; +import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; +import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; +import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED; +import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputAttribute; +import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; +import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; +import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; +import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; + +import java.io.IOException; +import java.io.PrintStream; + +import org.fusesource.jansi.internal.WindowsSupport; +import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; +import org.fusesource.jansi.internal.Kernel32.COORD; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes. + * + * @since 1.7 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public final class WindowsAnsiPrintStream extends AnsiPrintStream { + + private static final long console = GetStdHandle(STD_OUTPUT_HANDLE); + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiPrintStream(PrintStream ps) throws IOException { + super(ps); + getConsoleInfo(); + originalColors = info.attributes; + } + + private void getConsoleInfo() throws IOException { + ps.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes = invertAttributeColors(info.attributes); + } + } + + private void applyAttribute() throws IOException { + ps.flush(); + short attributes = info.attributes; + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x = 0; + topLeft.y = info.window.top; + int screenLength = info.window.height() * info.size.x; + FillConsoleOutputAttribute(console, originalColors, screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x = 0; + topLeft2.y = info.window.top; + int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + + info.cursorPosition.x; + FillConsoleOutputAttribute(console, originalColors, lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + + (info.size.x - info.cursorPosition.x); + FillConsoleOutputAttribute(console, originalColors, lengthToEnd, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition.copy(); + leftColCurrRow.x = 0; + FillConsoleOutputAttribute(console, originalColors, info.size.x, leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition.copy(); + leftColCurrRow2.x = 0; + FillConsoleOutputAttribute(console, originalColors, info.cursorPosition.x, leftColCurrRow2, written); + FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = info.size.x - info.cursorPosition.x; + FillConsoleOutputAttribute(console, originalColors, lengthToLastCol, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); + info.attributes = (short) ((info.attributes & ~FOREGROUND_INTENSITY) | (bright ? FOREGROUND_INTENSITY : 0)); + applyAttribute(); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); + info.attributes = (short) ((info.attributes & ~BACKGROUND_INTENSITY) | (bright ? BACKGROUND_INTENSITY : 0)); + applyAttribute(); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processAttributeRest() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition.x; + savedY = info.cursorPosition.y; + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + ps.flush(); + info.cursorPosition.x = savedX; + info.cursorPosition.y = savedY; + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short)(info.cursorPosition.y + optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short)(info.cursorPosition.y - optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String label) { + SetConsoleTitle(label); + } +} diff --git a/jansi/src/test/java/org/fusesource/jansi/FilterPrintStreamTest.java b/jansi/src/test/java/org/fusesource/jansi/FilterPrintStreamTest.java new file mode 100644 index 00000000..25b2283b --- /dev/null +++ b/jansi/src/test/java/org/fusesource/jansi/FilterPrintStreamTest.java @@ -0,0 +1,30 @@ +package org.fusesource.jansi; + +import java.io.OutputStream; +import java.io.PrintStream; + +import org.junit.Test; + +public class FilterPrintStreamTest +{ + @Test + public void testPrintMethods() + throws Exception + { + PrintStream ps = new FilterPrintStream(System.out); + ps.println("String"); + ps.println('€'); + ps.flush(); + } + + @Test + public void testWrite() + throws Exception + { + OutputStream os = new FilterPrintStream(System.out); + os.write('A'); + os.write('B'); + os.write("€".getBytes() ); + os.flush(); + } +} \ No newline at end of file