From f190a31c39297d6c4cd87522d8575bb9108feb0c Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 28 May 2016 11:30:46 +0200 Subject: [PATCH] WIP - initial consolidation of input/output functionality. --- InputConsole.iml | 229 ---- README.md | 2 +- src/META-INF/MANIFEST.MF | 3 + src/dorkbox/console/Console.java | 47 + src/dorkbox/console/Input.java | 15 + src/dorkbox/console/InputConsole.java | 478 +++++++++ .../TerminalType.java | 13 +- src/dorkbox/console/input/PosixTerminal.java | 184 ++++ .../input}/Terminal.java | 42 +- .../input}/UnsupportedTerminal.java | 45 +- .../console/input/WindowsTerminal.java | 190 ++++ src/dorkbox/console/output/Ansi.java | 979 ++++++++++++++++++ src/dorkbox/console/output/AnsiCode.java | 42 + src/dorkbox/console/output/AnsiConsole.java | 154 +++ .../console/output/AnsiOutputStream.java | 561 ++++++++++ .../console/output/AnsiRenderWriter.java | 84 ++ src/dorkbox/console/output/AnsiRenderer.java | 180 ++++ src/dorkbox/console/output/AnsiString.java | 104 ++ src/dorkbox/console/output/Attribute.java | 74 ++ src/dorkbox/console/output/Color.java | 67 ++ src/dorkbox/console/output/Erase.java | 46 + .../console/output/HtmlAnsiOutputStream.java | 158 +++ .../output/WindowsAnsiOutputStream.java | 375 +++++++ .../console/util/posix/CLibraryPosix.java | 63 ++ src/dorkbox/console/util/posix/Termios.java | 184 ++++ .../windows/CONSOLE_SCREEN_BUFFER_INFO.java | 45 + src/dorkbox/console/util/windows/COORD.java | 51 + .../console/util/windows/CharUnion.java | 31 + .../util}/windows/ConsoleMode.java | 26 +- src/dorkbox/console/util/windows/HANDLE.java | 74 ++ .../console/util/windows/INPUT_RECORD.java | 44 + .../util/windows/KEY_EVENT_RECORD.java | 23 + .../console/util/windows/Kernel32.java | 173 ++++ .../util/windows/MOUSE_EVENT_RECORD.java | 18 + .../console/util/windows/SMALL_RECT.java | 49 + src/dorkbox/inputConsole/Encoding.java | 50 - src/dorkbox/inputConsole/InputConsole.java | 580 ----------- .../inputConsole/posix/InputStreamReader.java | 306 ------ .../posix/PosixTerminalControl.java | 85 -- .../inputConsole/posix/TermiosStruct.java | 75 -- .../inputConsole/posix/UnixTerminal.java | 193 ---- .../inputConsole/windows/WindowsTerminal.java | 124 --- .../dorkbox/console/AnsiConsoleExample.java | 50 + .../dorkbox/console/AnsiConsoleExample2.java | 55 + .../dorkbox/console/AnsiRenderWriterTest.java | 60 ++ .../com/dorkbox/console/AnsiRendererTest.java | 116 +++ .../console/AnsiRendererTestExample.java | 172 +++ test/com/dorkbox/console/AnsiStringTest.java | 48 + test/com/dorkbox/console/AnsiTest.java | 61 ++ .../console/HtmlAnsiOutputStreamTest.java | 89 ++ test/com/dorkbox/console/jansi.ans | 8 + 51 files changed, 5218 insertions(+), 1707 deletions(-) delete mode 100644 InputConsole.iml create mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/dorkbox/console/Console.java create mode 100644 src/dorkbox/console/Input.java create mode 100644 src/dorkbox/console/InputConsole.java rename src/dorkbox/{inputConsole => console}/TerminalType.java (53%) create mode 100644 src/dorkbox/console/input/PosixTerminal.java rename src/dorkbox/{inputConsole => console/input}/Terminal.java (64%) rename src/dorkbox/{inputConsole/unsupported => console/input}/UnsupportedTerminal.java (79%) create mode 100644 src/dorkbox/console/input/WindowsTerminal.java create mode 100644 src/dorkbox/console/output/Ansi.java create mode 100644 src/dorkbox/console/output/AnsiCode.java create mode 100644 src/dorkbox/console/output/AnsiConsole.java create mode 100644 src/dorkbox/console/output/AnsiOutputStream.java create mode 100644 src/dorkbox/console/output/AnsiRenderWriter.java create mode 100644 src/dorkbox/console/output/AnsiRenderer.java create mode 100644 src/dorkbox/console/output/AnsiString.java create mode 100644 src/dorkbox/console/output/Attribute.java create mode 100644 src/dorkbox/console/output/Color.java create mode 100644 src/dorkbox/console/output/Erase.java create mode 100644 src/dorkbox/console/output/HtmlAnsiOutputStream.java create mode 100644 src/dorkbox/console/output/WindowsAnsiOutputStream.java create mode 100644 src/dorkbox/console/util/posix/CLibraryPosix.java create mode 100644 src/dorkbox/console/util/posix/Termios.java create mode 100644 src/dorkbox/console/util/windows/CONSOLE_SCREEN_BUFFER_INFO.java create mode 100644 src/dorkbox/console/util/windows/COORD.java create mode 100644 src/dorkbox/console/util/windows/CharUnion.java rename src/dorkbox/{inputConsole => console/util}/windows/ConsoleMode.java (79%) create mode 100644 src/dorkbox/console/util/windows/HANDLE.java create mode 100644 src/dorkbox/console/util/windows/INPUT_RECORD.java create mode 100644 src/dorkbox/console/util/windows/KEY_EVENT_RECORD.java create mode 100644 src/dorkbox/console/util/windows/Kernel32.java create mode 100644 src/dorkbox/console/util/windows/MOUSE_EVENT_RECORD.java create mode 100644 src/dorkbox/console/util/windows/SMALL_RECT.java delete mode 100644 src/dorkbox/inputConsole/Encoding.java delete mode 100644 src/dorkbox/inputConsole/InputConsole.java delete mode 100644 src/dorkbox/inputConsole/posix/InputStreamReader.java delete mode 100644 src/dorkbox/inputConsole/posix/PosixTerminalControl.java delete mode 100644 src/dorkbox/inputConsole/posix/TermiosStruct.java delete mode 100644 src/dorkbox/inputConsole/posix/UnixTerminal.java delete mode 100644 src/dorkbox/inputConsole/windows/WindowsTerminal.java create mode 100644 test/com/dorkbox/console/AnsiConsoleExample.java create mode 100644 test/com/dorkbox/console/AnsiConsoleExample2.java create mode 100644 test/com/dorkbox/console/AnsiRenderWriterTest.java create mode 100644 test/com/dorkbox/console/AnsiRendererTest.java create mode 100644 test/com/dorkbox/console/AnsiRendererTestExample.java create mode 100644 test/com/dorkbox/console/AnsiStringTest.java create mode 100644 test/com/dorkbox/console/AnsiTest.java create mode 100644 test/com/dorkbox/console/HtmlAnsiOutputStreamTest.java create mode 100644 test/com/dorkbox/console/jansi.ans diff --git a/InputConsole.iml b/InputConsole.iml deleted file mode 100644 index b483d80..0000000 --- a/InputConsole.iml +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 05dc2b0..7a50187 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This small library is very similar to what JLine provides, however it does 4 thi - Backspace functionality for line input is preserved. - Ctrl-C (SIGINT) is also preserved in windows 2. Uses native calls via JNA (instead of shell execution) for linux & mac terminal configuration -3. Supports unsupported teminals (for example, while in an IDE ), so in.read() will still return (a line is split into chars, then fed to consumer). The enter key must still be pressed. +3. Supports unsupported terminals (for example, while in an IDE ), `in.read()` will still return (a line is split into chars, then fed to consumer). The enter key must still be pressed. 4. Multi-threaded, intelligent buffering of command input for simultaneous input readers on different threads diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..1af4762 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.dorkbox.console.AnsiRendererTestExample + diff --git a/src/dorkbox/console/Console.java b/src/dorkbox/console/Console.java new file mode 100644 index 0000000..45af5bb --- /dev/null +++ b/src/dorkbox/console/Console.java @@ -0,0 +1,47 @@ +package dorkbox.console; + +/** + * + */ +public +class Console { + public static volatile boolean ENABLE_ECHO = true; + + public static boolean PASSWORD_ECHO = true; + public static char PASSWORD_ECHO_CHAR = '*'; + + // how many threads can read from this input at the same time + public static int NUMBER_OF_READERS = 32; + + + public final static boolean ENABLE_BACKSPACE = true; +// enableBackspace = Boolean.parseBoolean(System.getProperty(Console.ENABLE_BACKSPACE, "true")); + + + public static String ENABLE_BACKSPACEs = "input.enableBackspace"; + + + // OS types supported by the input console. Default is AUTO + public static final String AUTO = "auto"; + public static final String UNIX = "unix"; + public static final String WIN = "win"; + public static final String WINDOWS = "windows"; + + public static final String NONE = "none"; // this is the same as unsupported + public static final String OFF = "off"; // this is the same as unsupported + public static final String FALSE = "false"; // this is the same as unsupported + public static final String INPUT_CONSOLE_OSTYPE = "AUTO"; + + + /** + * Gets the version number. + */ + public static + String getVersion() { + return "2.9"; + } + + public static Input in = new Input(); + public static String out; + public static String err; +} diff --git a/src/dorkbox/console/Input.java b/src/dorkbox/console/Input.java new file mode 100644 index 0000000..f44706f --- /dev/null +++ b/src/dorkbox/console/Input.java @@ -0,0 +1,15 @@ +package dorkbox.console; + +/** + * + */ +public +class Input { + public + Input() { + } + + public int read() { + return InputConsole.read(); + } +} diff --git a/src/dorkbox/console/InputConsole.java b/src/dorkbox/console/InputConsole.java new file mode 100644 index 0000000..66a7ecb --- /dev/null +++ b/src/dorkbox/console/InputConsole.java @@ -0,0 +1,478 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.console; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; + +import dorkbox.console.input.PosixTerminal; +import dorkbox.console.input.Terminal; +import dorkbox.console.input.UnsupportedTerminal; +import dorkbox.console.input.WindowsTerminal; +import dorkbox.console.output.Ansi; +import dorkbox.objectPool.ObjectPool; +import dorkbox.util.FastThreadLocal; +import dorkbox.util.OS; +import dorkbox.util.bytes.ByteBuffer2; +import dorkbox.util.bytes.ByteBuffer2Poolable; + +public +class InputConsole { + + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InputConsole.class); + private static final char[] emptyLine = new char[0]; + + + private final static ObjectPool pool; + private final static Terminal terminal; + + private static final Object inputLock = new Object(); + private static final Object inputLockSingle = new Object(); + private static final Object inputLockLine = new Object(); + + private static final FastThreadLocal readBuff = new FastThreadLocal(); + private static final List readBuffers = new CopyOnWriteArrayList(); + private static final FastThreadLocal threadBufferCounter = new FastThreadLocal(); + + private static final FastThreadLocal readLineBuff = new FastThreadLocal(); + private static final List readLineBuffers = new CopyOnWriteArrayList(); + + + + static { + pool = ObjectPool.Blocking(new ByteBuffer2Poolable(), Console.NUMBER_OF_READERS); + + String type = Console.INPUT_CONSOLE_OSTYPE.toUpperCase(Locale.ENGLISH); + + Throwable didFallbackE = null; + Class t; + try { + if (type.equals(Console.UNIX)) { + t = PosixTerminal.class; + } + else if (type.equals(Console.WIN) || type.equals(Console.WINDOWS)) { + t = WindowsTerminal.class; + } + else if (type.equals(Console.NONE) || type.equals(Console.OFF) || type.equals(Console.FALSE)) { + t = UnsupportedTerminal.class; + } + else { + // if these cannot be created, because we are in an IDE, an error will be thrown + if (OS.isWindows()) { + t = WindowsTerminal.class; + } + else { + t = PosixTerminal.class; + } + } + } catch (Exception e) { + didFallbackE = e; + t = UnsupportedTerminal.class; + } + + Terminal term = null; + try { + term = t.newInstance(); + } catch (Throwable e) { + didFallbackE = e; + t = UnsupportedTerminal.class; + + try { + term = t.newInstance(); + } catch (Exception e1) { + // UnsupportedTerminal can't do this + } + } + terminal = term; + + + if (logger.isTraceEnabled()) { + logger.trace("Creating terminal based on type: " + type); + } + + if (logger.isDebugEnabled()) { + logger.debug("Created Terminal: {} ({}w x {}h)", InputConsole.terminal.getClass().getSimpleName(), + InputConsole.terminal.getWidth(), InputConsole.terminal.getHeight()); + } + + if (didFallbackE != null && !didFallbackE.getMessage().equals(Terminal.CONSOLE_ERROR_INIT)) { + logger.error("Failed to construct terminal, falling back to unsupported.", didFallbackE); + } else if (term instanceof UnsupportedTerminal) { + logger.debug("Terminal is in UNSUPPORTED (best guess). Unable to support single key input. Only line input available."); + } + + // echo and backspace + term.setEchoEnabled(Console.ENABLE_ECHO); + term.setInterruptEnabled(Console.ENABLE_BACKSPACE); + + Thread consoleThread = new Thread(new Runnable() { + @Override + public + void run() { + InputConsole.run(); + } + }); + consoleThread.setDaemon(true); + consoleThread.setName("Console Input Reader"); + + consoleThread.start(); + + // has to be NOT DAEMON thread, since it must run before the app closes. + + // don't forget we have to shut down the ansi console as well + // alternatively, shut everything down when the JVM closes. + Thread shutdownThread = new Thread() { + @Override + public + void run() { + // called when the JVM is shutting down. + release0(); + + try { + terminal.restore(); + // this will 'hang' our shutdown, and honestly, who cares? We're shutting down anyways. + // inputConsole.reader.close(); // hangs on shutdown + } catch (IOException ignored) { + ignored.printStackTrace(); + } + } + }; + shutdownThread.setName("Console Input Shutdown"); + Runtime.getRuntime().addShutdownHook(shutdownThread); + } + + private static InputStream wrappedInputStream = new InputStream() { + @Override + public + int read() throws IOException { + return InputConsole.read(); + } + + @Override + public + void close() throws IOException { + InputConsole.release0(); + } + }; + + public static + InputStream getInputStream() { + return wrappedInputStream; + } + + private + InputConsole() { + } + + /** + * return -1 if no data or bunged-up + */ + public static + int read() { + Integer bufferCounter = threadBufferCounter.get(); + ByteBuffer2 buffer = readBuff.get(); + + if (buffer == null) { + bufferCounter = 0; + threadBufferCounter.set(bufferCounter); + + try { + buffer = pool.takeInterruptibly(); + buffer.clear(); + } catch (InterruptedException e) { + logger.error("Interrupted while receiving buffer from pool."); + buffer = pool.newInstance(); + } + + readBuff.set(buffer); + readBuffers.add(buffer); + } + + if (bufferCounter == buffer.position()) { + synchronized (inputLockSingle) { + buffer.setPosition(0); + threadBufferCounter.set(0); + + try { + inputLockSingle.wait(); + } catch (InterruptedException e) { + return -1; + } + } + } + + bufferCounter = threadBufferCounter.get(); + char c = buffer.readChar(bufferCounter); + bufferCounter += 2; + + threadBufferCounter.set(bufferCounter); + return c; + } + + /** + * return empty char[] if no data + */ + public static + char[] readLinePassword() { + // don't bother in an IDE. it won't work. + boolean echoEnabled = Console.ENABLE_ECHO; + Console.ENABLE_ECHO = false; + char[] readLine0 = readLineChars(); + Console.ENABLE_ECHO = echoEnabled; + + return readLine0; + } + + /** + * return null if no data + */ + public static + String readLine() { + char[] line = InputConsole.readLineChars(); + if (line == null) { + return null; + } + return new String(line); + } + + /** + * return empty char[] if no data + */ + public static + char[] readLineChars() { + synchronized (inputLock) { + // empty here, because we don't want to register a readLine WHILE we are still processing + // the current line info. + + // the threadBufferForRead getting added is the part that is important + if (readLineBuff.get() == null) { + ByteBuffer2 buffer; + try { + buffer = pool.takeInterruptibly(); + } catch (InterruptedException e) { + logger.error("Interrupted while receiving buffer from pool."); + buffer = pool.newInstance(); + } + + readLineBuff.set(buffer); + readLineBuffers.add(buffer); + } + else { + readLineBuff.get().clear(); + } + } + + synchronized (inputLockLine) { + try { + inputLockLine.wait(); + } catch (InterruptedException e) { + return emptyLine; + } + } + + ByteBuffer2 buffer = readLineBuff.get(); + int len = buffer.position(); + if (len == 0) { + return emptyLine; + } + + buffer.rewind(); + char[] readChars = buffer.readChars(len / 2); // java always stores chars in 2 bytes + + // dump the chars in the buffer (safer for passwords, etc) + buffer.clearSecure(); + + readLineBuffers.remove(buffer); + pool.put(buffer); + readLineBuff.set(null); + + return readChars; + } + + /** + * releases any thread still waiting. + */ + private static + void release0() { + synchronized (inputLockSingle) { + inputLockSingle.notifyAll(); + } + + synchronized (inputLockLine) { + inputLockLine.notifyAll(); + } + } + + private static + void run() { + Logger logger2 = logger; + + final boolean ansiEnabled = Ansi.isEnabled(); + Ansi ansi = Ansi.ansi(); +// PrintStream out = AnsiConsole.out; + PrintStream out = System.out; + + int typedChar; + char asChar; + final char overWriteChar = ' '; + boolean enableBackspace = Console.ENABLE_BACKSPACE; + + + // don't type ; in a bash shell, it quits everything + // \n is replaced by \r in unix terminal? + while ((typedChar = terminal.read()) != -1) { + synchronized (inputLock) { + // don't let anyone add a new reader while we are still processing the current actions + asChar = (char) typedChar; + + if (logger2.isTraceEnabled()) { + logger2.trace("READ: {} ({})", asChar, typedChar); + } + + // notify everyone waiting for a character. + synchronized (inputLockSingle) { + // have to do readChar first (readLine has to deal with \b and \n + for (ByteBuffer2 buffer : readBuffers) { + buffer.writeChar(asChar); + } + + inputLockSingle.notifyAll(); + } + + // now to handle readLine stuff + + // if we type a backspace key, swallow it + previous in READLINE. READCHAR will have it passed anyways. + if (enableBackspace && asChar == '\b') { + int position = 0; + char[] overwrite = null; + + // clear ourself + one extra. + for (ByteBuffer2 buffer : readLineBuffers) { + // size of the buffer BEFORE our backspace was typed + int length = buffer.position(); + int amtToOverwrite = 4; // 2*2 backspace is always 2 chars (^?) * 2 because it's bytes + + if (length > 1) { + char charAt = buffer.readChar(length - 2); + amtToOverwrite += getPrintableCharacters(charAt); + + // delete last item in our buffer + length -= 2; + buffer.setPosition(length); + + // now figure out where the cursor is really at. + // this is more memory friendly than buf.toString.length + for (int i = 0; i < length; i += 2) { + charAt = buffer.readChar(i); + position += getPrintableCharacters(charAt); + } + + position++; + } + + overwrite = new char[amtToOverwrite]; + for (int i = 0; i < amtToOverwrite; i++) { + overwrite[i] = overWriteChar; + } + } + + if (ansiEnabled && overwrite != null) { + // move back however many, over write, then go back again + out.print(ansi.cursorToColumn(position)); + out.print(overwrite); + out.print(ansi.cursorToColumn(position)); + out.flush(); + } + } + else if (asChar == '\n') { + // ignoring \r, because \n is ALWAYS the last character in a new line sequence. (even for windows) + synchronized (inputLockLine) { + inputLockLine.notifyAll(); + } + } + else { + // only append if we are not a new line. + // our windows console PREVENTS us from returning '\r' (it truncates '\r\n', and returns just '\n') + for (ByteBuffer2 buffer : readLineBuffers) { + buffer.writeChar(asChar); + } + } + } + } + } + + private static final int PLUS_TWO_MAYBE = 128 + 32; + private static final int PLUS_ONE = 128 + 127; + + /** + * Return the number of characters that will be printed when the specified character is echoed to the screen + *

+ * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. + */ + private static + int getPrintableCharacters(final int ch) { + // StringBuilder sbuff = new StringBuilder(); + + if (ch >= 32) { + if (ch < 127) { + // sbuff.append((char) ch); + return 1; + } + else if (ch == 127) { + // sbuff.append('^'); + // sbuff.append('?'); + return 2; + } + else { + // sbuff.append('M'); + // sbuff.append('-'); + int count = 2; + + if (ch >= PLUS_TWO_MAYBE) { + if (ch < PLUS_ONE) { + // sbuff.append((char) (ch - 128)); + count++; + } + else { + // sbuff.append('^'); + // sbuff.append('?'); + count += 2; + } + } + else { + // sbuff.append('^'); + // sbuff.append((char) (ch - 128 + 64)); + count += 2; + } + return count; + } + } + else { + // sbuff.append('^'); + // sbuff.append((char) (ch + 64)); + return 2; + } + + // return sbuff; + } +} + diff --git a/src/dorkbox/inputConsole/TerminalType.java b/src/dorkbox/console/TerminalType.java similarity index 53% rename from src/dorkbox/inputConsole/TerminalType.java rename to src/dorkbox/console/TerminalType.java index e828286..f5d2bdb 100644 --- a/src/dorkbox/inputConsole/TerminalType.java +++ b/src/dorkbox/console/TerminalType.java @@ -13,19 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.inputConsole; +package dorkbox.console; public class TerminalType { - public static final String TYPE = "input.terminal"; - public static final String READERS = "input.terminal.readers"; - public static final String ENABLE_BACKSPACE = "input.enableBackspace"; - public static final String AUTO = "auto"; - public static final String UNIX = "unix"; - public static final String WIN = "win"; - public static final String WINDOWS = "windows"; - public static final String NONE = "none"; - public static final String OFF = "off"; - public static final String FALSE = "false"; + } diff --git a/src/dorkbox/console/input/PosixTerminal.java b/src/dorkbox/console/input/PosixTerminal.java new file mode 100644 index 0000000..bbedbc3 --- /dev/null +++ b/src/dorkbox/console/input/PosixTerminal.java @@ -0,0 +1,184 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.console.input; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.sun.jna.ptr.IntByReference; + +import dorkbox.console.Console; +import dorkbox.console.util.posix.CLibraryPosix; +import dorkbox.console.util.posix.Termios; + +/** + * Terminal that is used for unix platforms. Terminal initialization is handled via JNA and ioctl/tcgetattr/tcsetattr/cfmakeraw. + *

+ * This implementation should work for an reasonable POSIX system. + */ +public +class PosixTerminal extends Terminal { + + private final Termios original = new Termios(); + private Termios termInfo = new Termios(); + private ByteBuffer windowSizeBuffer = ByteBuffer.allocate(8); + private volatile boolean echo_enabled = !Console.ENABLE_ECHO; + private final IntByReference inputRef = new IntByReference(); + + public + PosixTerminal() throws IOException { + // save off the defaults + if (CLibraryPosix.tcgetattr(0, this.original) != 0) { + throw new IOException(CONSOLE_ERROR_INIT); + } + + // COMPARABLE TO (from upstream) + //settings.set("-icanon min 1 -ixon"); + //settings.set("dsusp undef"); + + // CTRL-I (tab), CTRL-M (enter) do not work + + if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) { + throw new IOException(CONSOLE_ERROR_INIT); + } + +// this.termInfo.inputFlags |= Termios.Input.INLCR; // Map NL to CR on input +// this.termInfo.inputFlags |= Termios.Input.ICRNL; // map CR to NL (ala CRMOD) + + + this.termInfo.inputFlags &= ~Termios.Input.IXON; // DISABLE - output flow control mediated by ^S and ^Q + this.termInfo.inputFlags &= ~Termios.Input.IXOFF; // DISABLE - input flow control mediated by ^S and ^Q + this.termInfo.inputFlags &= ~Termios.Input.BRKINT; // DISABLE - map BREAK to SIGINTR + this.termInfo.inputFlags &= ~Termios.Input.INPCK; // DISABLE - enable checking of parity errors + this.termInfo.inputFlags &= ~Termios.Input.PARMRK; // DISABLE - mark parity and framing errors + this.termInfo.inputFlags &= ~Termios.Input.ISTRIP; // DISABLE - strip 8th bit off chars + this.termInfo.inputFlags |= Termios.Input.IGNBRK; // ignore BREAK condition + + this.termInfo.localFlags &= ~Termios.Local.ICANON; // DISABLE - pass chars straight through to terminal instantly + this.termInfo.localFlags |= Termios.Local.ECHOCTL; // echo control chars as ^(Char) + + this.termInfo.controlFlags &= ~Termios.Control.CSIZE; // REMOVE character size mask + this.termInfo.controlFlags &= ~Termios.Control.PARENB; // DISABLE - parity enable + + this.termInfo.controlFlags |= Termios.Control.CS8; // set character size mask 8 bits + this.termInfo.controlFlags |= Termios.Control.CREAD; // enable receiver + + // If MIN > 0 and TIME = 0, MIN sets the number of characters to receive before the read is satisfied. + // As TIME is zero, the timer is not used. + this.termInfo.controlChars[Termios.ControlChars.VMIN] = (byte) 2; // Minimum number of characters for non-canonical read (MIN). + this.termInfo.controlChars[Termios.ControlChars.VTIME] = (byte) 5; // Timeout in deci-seconds for non-canonical read (TIME). + this.termInfo.controlChars[Termios.ControlChars.VSUSP] = (byte) 0; // suspend disabled + this.termInfo.controlChars[Termios.ControlChars.VEOF] = (byte) 0; // eof disabled + this.termInfo.controlChars[Termios.ControlChars.VEOL] = (byte) 0; // eol disabled + + if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, this.termInfo) != 0) { + throw new IOException("Can not set terminal flags"); + } + } + + /** + * Restore the original terminal configuration, which can be used when shutting down the console reader. The ConsoleReader cannot be + * used after calling this method. + */ + @Override + public final + void restore() throws IOException { + if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, this.original) != 0) { + throw new IOException("Can not reset terminal to defaults"); + } + } + + /** + * Returns number of columns in the terminal. + */ + @SuppressWarnings("NumericCastThatLosesPrecision") + @Override + public final + int getWidth() { + if (CLibraryPosix.ioctl(0, CLibraryPosix.TIOCGWINSZ, this.windowSizeBuffer) != 0) { + return DEFAULT_WIDTH; + } + + return (short) (0x000000FF & this.windowSizeBuffer.get(2) + (0x000000FF & this.windowSizeBuffer.get(3)) * 256); + } + + /** + * Returns number of rows in the terminal. + */ + @SuppressWarnings("NumericCastThatLosesPrecision") + @Override + public final + int getHeight() { + if (CLibraryPosix.ioctl(0, CLibraryPosix.TIOCGWINSZ, this.windowSizeBuffer) != 0) { + return DEFAULT_HEIGHT; + } + + return (short) (0x000000FF & this.windowSizeBuffer.get(0) + (0x000000FF & this.windowSizeBuffer.get(1)) * 256); + } + + @Override + public final + int read() { + // TODO: should have a better way of doing this! (should use a method to set echo, instead of a field?) + // have to determine when it changes + if (echo_enabled != Console.ENABLE_ECHO) { + echo_enabled = Console.ENABLE_ECHO; + setEchoEnabled(echo_enabled); + setInterruptEnabled(false); + } + + CLibraryPosix.read(0, inputRef, 1); + return inputRef.getValue(); + } + + public + void setEchoEnabled(final boolean enabled) { + // have to re-get them, since flags change everything + if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) { + this.logger.error("Failed to get terminal info"); + } + + if (enabled) { + this.termInfo.localFlags |= Termios.Local.ECHO; // ENABLE Echo input characters. + } + else { + this.termInfo.localFlags &= ~Termios.Local.ECHO; // DISABLE Echo input characters. + } + + if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, this.termInfo) != 0) { + this.logger.error("Can not set terminal flags"); + } + } + + public + void setInterruptEnabled(final boolean enabled) { + // have to re-get them, since flags change everything + if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) { + this.logger.error("Failed to get terminal info"); + } + + if (enabled) { + this.termInfo.localFlags |= Termios.Local.ISIG; // ENABLE ctrl-C + } + else { + this.termInfo.localFlags &= ~Termios.Local.ISIG; // DISABLE ctrl-C + } + + if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, this.termInfo) != 0) { + this.logger.error("Can not set terminal flags"); + } + } +} diff --git a/src/dorkbox/inputConsole/Terminal.java b/src/dorkbox/console/input/Terminal.java similarity index 64% rename from src/dorkbox/inputConsole/Terminal.java rename to src/dorkbox/console/input/Terminal.java index 28bd5dd..3762370 100644 --- a/src/dorkbox/inputConsole/Terminal.java +++ b/src/dorkbox/console/input/Terminal.java @@ -13,40 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.inputConsole; +package dorkbox.console.input; import java.io.IOException; -public abstract class Terminal { - - protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - +public abstract +class Terminal { + public static final String CONSOLE_ERROR_INIT = "Unable to get input console mode."; protected static final int DEFAULT_WIDTH = 80; protected static final int DEFAULT_HEIGHT = 24; + protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - private volatile boolean echoEnabled; - - protected Terminal() {} - - public abstract void init() throws IOException; - - public abstract void restore() throws IOException; - - public void setEchoEnabled(boolean enabled) { - this.echoEnabled = enabled; + protected + Terminal() { } - public boolean isEchoEnabled() { - return this.echoEnabled; - } + public abstract + void restore() throws IOException; - public abstract int getWidth(); + public abstract + int getWidth(); - public abstract int getHeight(); + public abstract + int getHeight(); + + public abstract + void setEchoEnabled(final boolean enabled); + + public abstract + void setInterruptEnabled(final boolean enabled); /** * @return a character from whatever underlying input method the terminal has available. */ - public abstract int read(); + public abstract + int read(); } diff --git a/src/dorkbox/inputConsole/unsupported/UnsupportedTerminal.java b/src/dorkbox/console/input/UnsupportedTerminal.java similarity index 79% rename from src/dorkbox/inputConsole/unsupported/UnsupportedTerminal.java rename to src/dorkbox/console/input/UnsupportedTerminal.java index 369d82c..fb45c83 100644 --- a/src/dorkbox/inputConsole/unsupported/UnsupportedTerminal.java +++ b/src/dorkbox/console/input/UnsupportedTerminal.java @@ -13,43 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.inputConsole.unsupported; +package dorkbox.console.input; import java.io.IOException; import java.io.InputStream; -import dorkbox.inputConsole.Terminal; import dorkbox.util.bytes.ByteBuffer2; -public class UnsupportedTerminal extends Terminal { +public +class UnsupportedTerminal extends Terminal { private final ByteBuffer2 buffer = new ByteBuffer2(8, -1); - + private final InputStream in = System.in; private int readerCount = -1; - private final InputStream in; - public UnsupportedTerminal() { - this.in = System.in; + public + UnsupportedTerminal() { } @Override - public final void init() throws IOException {} + public final + void restore() { + } @Override - public final void restore() {} - - @Override - public final int getWidth() { + public final + int getWidth() { return 0; } @Override - public final int getHeight() { + public final + int getHeight() { return 0; } @Override - public final int read() { + public + void setEchoEnabled(final boolean enabled) { + } + + @Override + public + void setInterruptEnabled(final boolean enabled) { + } + + @Override + public final + int read() { // if we are reading data (because we are in IDE mode), we want to return ALL the chars of the line! // so, 'readerCount' is REALLY the index at which we return letters (until the whole string is returned) @@ -67,7 +78,8 @@ public class UnsupportedTerminal extends Terminal { this.readerCount = this.buffer.position(); this.buffer.rewind(); break; - } else { + } + else { this.buffer.writeChar(asChar); } } @@ -79,7 +91,8 @@ public class UnsupportedTerminal extends Terminal { if (this.readerCount == this.buffer.position()) { this.readerCount = -1; return '\n'; - } else { + } + else { return this.buffer.readChar(); } } diff --git a/src/dorkbox/console/input/WindowsTerminal.java b/src/dorkbox/console/input/WindowsTerminal.java new file mode 100644 index 0000000..d60cf6e --- /dev/null +++ b/src/dorkbox/console/input/WindowsTerminal.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * http://www.opensource.org/licenses/bsd-license.php + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + */ +package dorkbox.console.input; + +import static dorkbox.console.util.windows.Kernel32.GetConsoleScreenBufferInfo; +import static dorkbox.console.util.windows.Kernel32.STD_INPUT_HANDLE; +import static dorkbox.console.util.windows.Kernel32.STD_OUTPUT_HANDLE; + +import java.io.IOException; + +import com.sun.jna.ptr.IntByReference; + +import dorkbox.console.util.windows.CONSOLE_SCREEN_BUFFER_INFO; +import dorkbox.console.util.windows.ConsoleMode; +import dorkbox.console.util.windows.HANDLE; +import dorkbox.console.util.windows.INPUT_RECORD; +import dorkbox.console.util.windows.KEY_EVENT_RECORD; +import dorkbox.console.util.windows.Kernel32; + +/** + * Terminal implementation for Microsoft Windows. + */ +public +class WindowsTerminal extends Terminal { + + private final HANDLE console; + private final HANDLE outputConsole; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final INPUT_RECORD.ByReference inputRecords = new INPUT_RECORD.ByReference(); + private final IntByReference reference = new IntByReference(); + + private volatile int originalMode; + + public + WindowsTerminal() throws IOException { + console = Kernel32.GetStdHandle(STD_INPUT_HANDLE); + if (console == HANDLE.INVALID_HANDLE_VALUE) { + throw new IOException("Unable to get input console handle."); + } + + outputConsole = Kernel32.GetStdHandle(STD_OUTPUT_HANDLE); + if (outputConsole == HANDLE.INVALID_HANDLE_VALUE) { + throw new IOException("Unable to get output console handle."); + } + + IntByReference mode = new IntByReference(); + if (Kernel32.GetConsoleMode(console, mode) == 0) { + throw new IOException(CONSOLE_ERROR_INIT); + } + + this.originalMode = mode.getValue(); + + int newMode = this.originalMode | + ConsoleMode.ENABLE_LINE_INPUT.code | + ConsoleMode.ENABLE_ECHO_INPUT.code | + ConsoleMode.ENABLE_PROCESSED_INPUT.code | + ConsoleMode.ENABLE_WINDOW_INPUT.code; + + // Disable input echo + newMode = newMode & ~ConsoleMode.ENABLE_ECHO_INPUT.code; + + // Must set these four modes at the same time to make it work fine. + Kernel32.SetConsoleMode(console, newMode); + } + + /** + * Restore the original terminal configuration, which can be used when shutting down the console reader. + * The ConsoleReader cannot be used after calling this method. + */ + @Override + public final + void restore() throws IOException { + // restore the old console mode + Kernel32.SetConsoleMode(console, this.originalMode); + + Kernel32.CloseHandle(console); + Kernel32.CloseHandle(outputConsole); + } + + @Override + public final + int getWidth() { + GetConsoleScreenBufferInfo(outputConsole, info); + int w = info.window.width() + 1; + return w < 1 ? DEFAULT_WIDTH : w; + } + + @Override + public final + int getHeight() { + GetConsoleScreenBufferInfo(outputConsole, info); + int h = info.window.height() + 1; + return h < 1 ? DEFAULT_HEIGHT : h; + } + + @Override + public + void setEchoEnabled(final boolean enabled) { + IntByReference mode = new IntByReference(); + Kernel32.GetConsoleMode(console, mode); + + int newMode; + if (enabled) { + // Enable Ctrl+C + newMode = mode.getValue() | ConsoleMode.ENABLE_ECHO_INPUT.code; + } else { + // Disable Ctrl+C + newMode = mode.getValue() & ~ConsoleMode.ENABLE_ECHO_INPUT.code; + } + + Kernel32.SetConsoleMode(console, newMode); + } + + @Override + public + void setInterruptEnabled(final boolean enabled) { + IntByReference mode = new IntByReference(); + Kernel32.GetConsoleMode(console, mode); + + int newMode; + if (enabled) { + // Enable Ctrl+C + newMode = mode.getValue() | ConsoleMode.ENABLE_PROCESSED_INPUT.code; + } else { + // Disable Ctrl+C + newMode = mode.getValue() & ~ConsoleMode.ENABLE_PROCESSED_INPUT.code; + } + + Kernel32.SetConsoleMode(console, newMode); + } + + @Override + public final + int read() { + int input = readInput(); + +// if (Console.ENABLE_ECHO) { +// char asChar = (char) input; +// if (asChar == '\n') { +// System.out.println(); +// } +// else { +// System.out.print(asChar); +// } +// +// // have to flush, otherwise we'll never see the chars on screen +// System.out.flush(); +// } + + return input; + } + + private + int readInput() { + // keep reading input events until we find one that we are interested in (ie: keyboard input) + while (true) { + // blocks until there is (at least) 1 event on the buffer + Kernel32.ReadConsoleInputW(console, inputRecords, 1, reference); + + for (int i = 0; i < reference.getValue(); ++i) { + if (inputRecords.EventType == INPUT_RECORD.KEY_EVENT) { + KEY_EVENT_RECORD keyEvent = inputRecords.Event.KeyEvent; + + //logger.trace(keyEvent.bKeyDown ? "KEY_DOWN" : "KEY_UP", "key code:", keyEvent.wVirtualKeyCode, "char:", (long)keyEvent.uChar.unicodeChar); + if (keyEvent.keyDown) { + final char uChar = keyEvent.uChar.unicodeChar; + if (uChar > 0) { + if (uChar == '\r') { + // we purposefully swallow input after \r, and substitute it with \n + return '\n'; + } + + return uChar; + } + } + } + } + } + } +} diff --git a/src/dorkbox/console/output/Ansi.java b/src/dorkbox/console/output/Ansi.java new file mode 100644 index 0000000..b0d49df --- /dev/null +++ b/src/dorkbox/console/output/Ansi.java @@ -0,0 +1,979 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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. + * + * + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.output; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +/** + * Provides a fluent API for generating ANSI escape sequences. + * + * See: https://en.wikipedia.org/wiki/ANSI_escape_code + * + * @author Dorkbox, LLC + * @author Hiram Chirino + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class Ansi { + + private static final String NEW_LINE = System.getProperty("line.separator"); + + private static final char FIRST_ESC_CHAR = 27; + private static final char SECOND_ESC_CHAR = '['; + + private static int installed; + + /** + * Override System.err and System.out with an ANSI capable {@link java.io.PrintStream}. + */ + public static synchronized + void systemInstall() { + installed++; + if (installed == 1) { + System.setOut(AnsiConsole.out); + System.setErr(AnsiConsole.err); + } + } + + /** + * un-does a previous {@link #systemInstall()}. + * + * If {@link #systemInstall()} was called multiple times, then {@link #systemUninstall()} must be called the same number of + * times before it is uninstalled. + */ + public static synchronized + void systemUninstall() { + installed--; + if (installed == 0) { + if (AnsiConsole.out != AnsiConsole.system_out) { + AnsiConsole.out.close(); + } + + if (AnsiConsole.err != AnsiConsole.system_err) { + AnsiConsole.err.close(); + } + + System.setOut(AnsiConsole.system_out); + System.setErr(AnsiConsole.system_err); + } + } + + public static final String DISABLE = Ansi.class.getName() + ".disable"; + + private static Callable detector = new Callable() { + @Override + public Boolean call() throws Exception { + return !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(); + } + + private static class NoAnsi extends Ansi + { + public + NoAnsi() { + super(); + } + + public + NoAnsi(final StringBuilder builder) { + super(builder); + } + + public + NoAnsi(final int size) { + super(size); + } + + @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 fgBrightDefault() { return this; } + + @Override + public Ansi bgBrightDefault() { return this; } + + @Override + public Ansi fgDefault() { return this; } + + @Override + public Ansi bgDefault() { return this; } + + + @Override + public Ansi a(Attribute attribute) { + return this; + } + + @Override + public Ansi cursor(int x, int y) { + 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 + public Ansi restoreCursorPosition() { + return this; + } + + @Override + public Ansi reset() { + return this; + } + } + + /** + * Creates a new Ansi object and resets the output to the default. + */ + public static Ansi ansi() { + if (isEnabled()) { + return new Ansi(); + } + else { + return new NoAnsi(); + } + } + + /** + * Creates a new Ansi object from the specified StringBuilder. This does NOT reset the output back to default. + */ + public static + Ansi ansi(StringBuilder builder) { + if (isEnabled()) { + return new Ansi(builder); + } + else { + return new NoAnsi(builder); + } + } + + /** + * Creates a new Ansi object of the specified length and reset the output back to default. + */ + public static + Ansi ansi(int size) { + + if (isEnabled()) { + return new Ansi(size); + } + else { + return new NoAnsi(size); + } + } + + + + + private final StringBuilder builder; + private final ArrayList attributeOptions = new ArrayList(5); + + /** + * Creates a new Ansi object and resets the output to the default. + */ + public + Ansi() { + this(new StringBuilder()); + reset(); // always reset a NEW Ansi object (w/ no parent) + } + + /** + * Creates a new Ansi object from the parent. This does NOT reset the output back to default. + */ + public + Ansi(Ansi parent) { + this(new StringBuilder(parent.builder)); + attributeOptions.addAll(parent.attributeOptions); + } + + /** + * Creates a new Ansi object of the specified length and reset the output back to default. + */ + public + Ansi(int size) { + this(new StringBuilder(size)); + reset(); // always reset a NEW Ansi object (w/ no parent) + } + + /** + * Creates a new Ansi object from the specified StringBuilder. This does NOT reset the output back to default. + */ + public + Ansi(StringBuilder builder) { + this.builder = builder; + // don't know if there is a parent or not, so we don't reset() + } + + public + Ansi fg(Color color) { + attributeOptions.add(color.fg()); + return this; + } + + public + Ansi fgDefault() { + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_DEFAULT_FG); + 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 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 fgWhite() { + return this.fg(Color.WHITE); + } + + public + Ansi fgBright(Color color) { + attributeOptions.add(color.fgBright()); + return this; + } + + public + Ansi fgBrightDefault() { + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_DEFAULT_FG); + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_BOLD); + 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 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 fgBrightWhite() { + return this.fgBright(Color.WHITE); + } + + + public + Ansi bg(Color color) { + attributeOptions.add(color.bg()); + return this; + } + + public + Ansi bgDefault() { + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_DEFAULT_BG); + return this; + } + + public + Ansi bgBlack() { + return this.bg(Color.BLACK); + } + + public + Ansi bgBlue() { + return this.bg(Color.BLUE); + } + + public + Ansi bgCyan() { + return this.bg(Color.CYAN); + } + + 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 bgWhite() { + return this.bg(Color.WHITE); + } + + public + Ansi bgBright(Color color) { + attributeOptions.add(color.bgBright()); + return this; + } + + public + Ansi bgBrightDefault() { + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_DEFAULT_BG); + attributeOptions.add(AnsiOutputStream.ATTRIBUTE_BOLD); + return this; + } + + + public + Ansi bgBrightBlack() { + return this.bgBright(Color.BLACK); + } + + public + Ansi bgBrightBlue() { + return this.bgBright(Color.BLUE); + } + + public + Ansi bgBrightCyan() { + return this.bgBright(Color.CYAN); + } + + 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 bgBrightWhite() { + return this.bgBright(Color.WHITE); + } + + public + Ansi a(Attribute attribute) { + attributeOptions.add(attribute.value()); + return this; + } + + /** + * @param x is 1 indexed (the very first value is 1, not 0) + * @param y is 1 indexed (the very first value is 1, not 0) + */ + public + Ansi cursor(final int x, final int y) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_POS, x, y); + } + + /** + * @param x is 1 indexed (the very first value is 1, not 0) + */ + public + Ansi cursorToColumn(final int x) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_TO_COL, x); + } + + public + Ansi cursorUp(final int y) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_UP, y); + } + + public + Ansi cursorDown(final int y) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_DOWN, y); + } + + public + Ansi cursorRight(final int x) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_RIGHT, x); + } + + public + Ansi cursorLeft(final int x) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_LEFT, x); + } + + public + Ansi cursorDownLine() { + return appendEscapeSequence(AnsiOutputStream.CURSOR_DOWN_LINE); + } + + public + Ansi cursorDownLine(final int n) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_DOWN_LINE, n); + } + + public + Ansi cursorUpLine() { + return appendEscapeSequence(AnsiOutputStream.CURSOR_UP_LINE); + } + + public + Ansi cursorUpLine(final int n) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_UP_LINE, n); + } + + public + Ansi eraseScreen() { + return appendEscapeSequence(AnsiOutputStream.CURSOR_ERASE_SCREEN, Erase.ALL.value()); + } + + public + Ansi eraseScreen(final Erase kind) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_ERASE_SCREEN, kind.value()); + } + + public + Ansi eraseLine() { + return appendEscapeSequence(AnsiOutputStream.CURSOR_ERASE_LINE); + } + + public + Ansi eraseLine(final Erase kind) { + return appendEscapeSequence(AnsiOutputStream.CURSOR_ERASE_LINE, kind.value()); + } + + public + Ansi scrollUp(final int rows) { + return appendEscapeSequence(AnsiOutputStream.PAGE_SCROLL_UP, rows); + } + + public + Ansi scrollDown(final int rows) { + return appendEscapeSequence(AnsiOutputStream.PAGE_SCROLL_DOWN, rows); + } + + public + Ansi saveCursorPosition() { + return appendEscapeSequence(AnsiOutputStream.SAVE_CURSOR_POS); + } + + public + Ansi restoreCursorPosition() { + return appendEscapeSequence(AnsiOutputStream.RESTORE_CURSOR_POS); + } + + public + Ansi reset() { + return a(Attribute.RESET); + } + + public + Ansi bold() { + return a(Attribute.BOLD); + } + + public + Ansi boldOff() { + return a(Attribute.BOLD_OFF); + } + + public + Ansi faint() { + return a(Attribute.FAINT); + } + + public + Ansi faintOff() { + return a(Attribute.FAINT_OFF); + } + + public + Ansi italic() { + return a(Attribute.ITALIC); + } + + public + Ansi italicOff() { + return a(Attribute.ITALIC_OFF); + } + + public + Ansi underline() { + return a(Attribute.UNDERLINE); + } + + public + Ansi underlineDouble() { + return a(Attribute.UNDERLINE_DOUBLE); + } + + public + Ansi underlineOff() { + return a(Attribute.UNDERLINE_OFF); + } + + public + Ansi blinkSlow() { + return a(Attribute.BLINK_SLOW); + } + + public + Ansi blinkFast() { + return a(Attribute.BLINK_FAST); + } + + public + Ansi blinkOff() { + return a(Attribute.BLINK_OFF); + } + + public + Ansi negative() { + return a(Attribute.NEGATIVE); + } + + public + Ansi negativeOff() { + return a(Attribute.NEGATIVE_OFF); + } + + public + Ansi conceal() { + return a(Attribute.CONCEAL); + } + + public + Ansi concealOff() { + return a(Attribute.CONCEAL_OFF); + } + + public + Ansi strikethrough() { + return a(Attribute.STRIKETHROUGH); + } + + public + Ansi strikethroughOff() { + return a(Attribute.STRIKETHROUGH_OFF); + } + + public + Ansi a(final String value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final boolean value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final char value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final char[] value, final int offset, final int len) { + flushAttributes(); + builder.append(value, offset, len); + return this; + } + + public + Ansi a(final char[] value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final CharSequence value, final int start, final int end) { + flushAttributes(); + builder.append(value, start, end); + return this; + } + + public + Ansi a(final CharSequence value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final double value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final float value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final int value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final long value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final Object value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final StringBuilder value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi a(final StringBuffer value) { + flushAttributes(); + builder.append(value); + return this; + } + + public + Ansi newline() { + flushAttributes(); + builder.append(NEW_LINE); + return this; + } + + public + Ansi format(final String pattern, final Object... args) { + flushAttributes(); + builder.append(String.format(pattern, args)); + return this; + } + + /** + * Uses the {@link AnsiRenderer} to generate the ANSI escape sequences for the supplied text. + */ + 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. + */ + public + Ansi render(final String text, final Object... args) { + a(String.format(AnsiRenderer.render(text), args)); + return this; + } + + @Override + public String toString() { + flushAttributes(); + return builder.toString(); + } + + /////////////////////////////////////////////////////////////////// + // Private Helper Methods + /////////////////////////////////////////////////////////////////// + + private + Ansi appendCommandSequence(final char command) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(command); + return this; + } + + private + Ansi appendEscapeSequence(final char command) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(command); + return this; + } + + private + Ansi appendEscapeSequence(final char command, final int option) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(option); + builder.append(command); + return this; + } + + private + Ansi appendEscapeSequence(final char command, final 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(AnsiOutputStream.TEXT_ATTRIBUTE); + } else { + _appendEscapeSequence(AnsiOutputStream.TEXT_ATTRIBUTE, attributeOptions.toArray()); + } + attributeOptions.clear(); + } + + private + Ansi _appendEscapeSequence(final char command, final 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; + } +} diff --git a/src/dorkbox/console/output/AnsiCode.java b/src/dorkbox/console/output/AnsiCode.java new file mode 100644 index 0000000..dee8996 --- /dev/null +++ b/src/dorkbox/console/output/AnsiCode.java @@ -0,0 +1,42 @@ +package dorkbox.console.output; + +/** + * + */ +class AnsiCode { + Enum anEnum; + String formalName; + boolean isColorForBackground; + + public + AnsiCode(final Enum anEnum, final String formalName, final boolean isColorForBackground) { + this.anEnum = anEnum; + this.formalName = formalName; + this.isColorForBackground = isColorForBackground; + } + + public + boolean isColor() { + return anEnum instanceof Color; + } + + public + boolean isBackgroundColor() { + return isColorForBackground; + } + + public + Color getColor() { + return (Color) anEnum; + } + + public + boolean isAttribute() { + return anEnum instanceof Attribute; + } + + public + Attribute getAttribute() { + return (Attribute) anEnum; + } +} diff --git a/src/dorkbox/console/output/AnsiConsole.java b/src/dorkbox/console/output/AnsiConsole.java new file mode 100644 index 0000000..bd33f83 --- /dev/null +++ b/src/dorkbox/console/output/AnsiConsole.java @@ -0,0 +1,154 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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 dorkbox.console.output; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import dorkbox.console.util.posix.CLibraryPosix; +import dorkbox.console.util.windows.Kernel32; + +/** + * Provides consistent access to an ANSI aware console PrintStream. + * + * See: https://en.wikipedia.org/wiki/ANSI_escape_code + * + * @author Dorkbox, LLC + * @author Hiram Chirino + */ +@SuppressWarnings("SpellCheckingInspection") +class AnsiConsole { + + static final int STDOUT_FILENO = 1; + static final int STDERR_FILENO = 2; + + public static final PrintStream system_out = System.out; + public static final PrintStream out = new PrintStream(wrapOutputStream(system_out, STDOUT_FILENO)); + + public static final PrintStream system_err = System.err; + public static final PrintStream err = new PrintStream(wrapOutputStream(system_err, STDERR_FILENO)); + + private static boolean isXterm() { + String term = System.getenv("TERM"); + return term != null && term.equals("xterm"); + } + + private static + OutputStream wrapOutputStream(final OutputStream stream, int fileno) { + + // If the jansi.passthrough property is set, then don't interpret + // any of the ansi sequences. + if (Boolean.getBoolean("jansi.passthrough")) { + return stream; + } + + // If the jansi.strip property is set, then we just strip the + // the ansi escapes. + if (Boolean.getBoolean("jansi.strip")) { + return new AnsiOutputStream(stream); + } + + String os = System.getProperty("os.name"); + if (os.startsWith("Windows") && !isXterm()) { + + // check if windows10+ (which natively supports ANSI) + if (Kernel32.isWindows10OrGreater()) { + // Just wrap it up so that when we get closed, we reset the attributes. + return new FilterOutputStream(stream) { + @Override + public + void close() throws IOException { + write(AnsiOutputStream.RESET_CODE); + flush(); + super.close(); + } + }; + } + + // On windows we know the console does not interpret ANSI codes.. + try { + return new WindowsAnsiOutputStream(stream, fileno); + } catch (Throwable ignore) { + ignore.printStackTrace(); + // this happens when JNA is not in the path.. or + // this happens when the stdout is being redirected to a file. + // this happens when the stdout is being redirected to different console. + } + + // Use the ANSIOutputStream to strip out the ANSI escape sequences. + return new AnsiOutputStream(stream); + } + + // We must be on some unix variant.. + 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.. + int rc = CLibraryPosix.isatty(fileno); + if (!isXterm() && !forceColored && rc == 0) { + return new AnsiOutputStream(stream); + } + + // These errors happen if the JNI lib is not available for your platform. + } catch (NoClassDefFoundError ignore) { + } catch (UnsatisfiedLinkError ignore) { + } + + // 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. + return new FilterOutputStream(stream) { + @Override + public + void close() throws IOException { + write(AnsiOutputStream.RESET_CODE); + 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 + * which strips out the ANSI escape sequences or which implement the escape + * sequences. + * + * @return a PrintStream which is ANSI aware. + */ + public static + PrintStream out() { + return 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 + PrintStream err() { + return err; + } +} diff --git a/src/dorkbox/console/output/AnsiOutputStream.java b/src/dorkbox/console/output/AnsiOutputStream.java new file mode 100644 index 0000000..f83c59b --- /dev/null +++ b/src/dorkbox/console/output/AnsiOutputStream.java @@ -0,0 +1,561 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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 dorkbox.console.output; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * A ANSI output stream extracts ANSI escape codes written to + * an output stream. + * + * 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. Subclasses should + * actually perform the ANSI escape behaviors. + * + * @author Hiram Chirino + * @author Joris Kuipers + * @since 1.0 + */ +@SuppressWarnings("NumericCastThatLosesPrecision") +public +class AnsiOutputStream extends FilterOutputStream { + private static final Charset CHARSET = Charset.forName("UTF-8"); + + static final int BLACK = 0; + static final int RED = 1; + static final int GREEN = 2; + static final int YELLOW = 3; + static final int BLUE = 4; + static final int MAGENTA = 5; + static final int CYAN = 6; + static final int WHITE = 7; + + static final char CURSOR_UP = 'A'; + static final char CURSOR_DOWN = 'B'; + static final char CURSOR_RIGHT = 'C'; + static final char CURSOR_LEFT = 'D'; + static final char CURSOR_DOWN_LINE = 'E'; + static final char CURSOR_UP_LINE = 'F'; + static final char CURSOR_TO_COL = 'G'; + static final char CURSOR_POS = 'H'; + static final char CURSOR_POS_ALT = 'f'; + + static final char CURSOR_ERASE_SCREEN = 'J'; + static final char CURSOR_ERASE_LINE = 'K'; + static final char PAGE_SCROLL_UP = 'S'; + static final char PAGE_SCROLL_DOWN = 'T'; + static final char SAVE_CURSOR_POS = 's'; + static final char RESTORE_CURSOR_POS = 'u'; + static final char TEXT_ATTRIBUTE = 'm'; + + static final int ATTRIBUTE_RESET = 0; // Reset / Normal - all attributes off + static final int ATTRIBUTE_BOLD = 1; // Intensity: Bold + static final int ATTRIBUTE_FAINT = 2; // Intensity; Faint (not widely supported) + static final int ATTRIBUTE_ITALIC = 3; // Italic; (on not widely supported. Sometimes treated as inverse) + static final int ATTRIBUTE_UNDERLINE = 4; // Underline; Single + static final int ATTRIBUTE_BLINK_SLOW = 5; // Blink; Slow less than 150 per minute + static final int ATTRIBUTE_BLINK_FAST = 6; // Blink; Rapid 150 per minute or more + static final int ATTRIBUTE_NEGATIVE_ON = 7; // Negative inverse or reverse; swap foreground and background + static final int ATTRIBUTE_CONCEAL_ON = 8; // Conceal on + static final int ATTRIBUTE_STRIKETHROUGH_ON = 9; // Crossed-out + + static final int ATTRIBUTE_UNDERLINE_DOUBLE = 21; // Underline; Double not widely supported + static final int ATTRIBUTE_NORMAL = 22; // Intensity; Normal not bold and not faint + static final int ATTRIBUTE_ITALIC_OFF = 23; // Not italic + static final int ATTRIBUTE_UNDERLINE_OFF = 24; // Underline; None + static final int ATTRIBUTE_BLINK_OFF = 25; // Blink; off + static final int ATTRIBUTE_NEGATIVE_OFF = 27; // Image; Positive + static final int ATTRIBUTE_CONCEAL_OFF = 28; // Reveal conceal off + static final int ATTRIBUTE_STRIKETHROUGH_OFF = 29; // Not crossed out + + static final int ATTRIBUTE_DEFAULT_FG = 39; // Default text color (foreground) + static final int ATTRIBUTE_DEFAULT_BG = 49; // Default background color + + // for Erase Screen/Line + static final int ERASE_TO_END = 0; + static final int ERASE_TO_BEGINNING = 1; + static final int ERASE_ALL = 2; + + static final byte[] RESET_CODE = new Ansi().reset() + .toString() + .getBytes(CHARSET); + + AnsiOutputStream(OutputStream os) { + super(os); + } + + private final static int MAX_ESCAPE_SEQUENCE_LENGTH = 100; + private 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; + + private 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 = '\\'; + + // TODO: implement to get perf boost: public void write(byte[] b, int off, int len) + + 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 { + 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; + + 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); + Integer value = Integer.valueOf(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); + 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); + 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, CHARSET); + 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); + options.add(value); + reset(processOperatingSystemCommand(options)); + } + else { + state = LOOKING_FOR_OSC_PARAM; + } + break; + + } + + // Is it just too long? + if (pos >= buffer.length) { + reset(false); + } + } + + /** + * 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; + } + + + + + /** + * @return true if the escape command was processed. + */ + private + boolean processEscapeCommand(ArrayList options, int command) throws IOException { + try { + switch (command) { + case CURSOR_UP: + processCursorUp(optionInt(options, 0, 1)); + return true; + case CURSOR_DOWN: + processCursorDown(optionInt(options, 0, 1)); + return true; + case CURSOR_RIGHT: + processCursorRight(optionInt(options, 0, 1)); + return true; + case CURSOR_LEFT: + processCursorLeft(optionInt(options, 0, 1)); + return true; + case CURSOR_DOWN_LINE: + processCursorDownLine(optionInt(options, 0, 1)); + return true; + case CURSOR_UP_LINE: + processCursorUpLine(optionInt(options, 0, 1)); + return true; + case CURSOR_TO_COL: + processCursorToColumn(optionInt(options, 0)); + return true; + case CURSOR_POS: + case CURSOR_POS_ALT: + processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1)); + return true; + case CURSOR_ERASE_SCREEN: + processEraseScreen(optionInt(options, 0, 0)); + return true; + case CURSOR_ERASE_LINE: + processEraseLine(optionInt(options, 0, 0)); + return true; + case PAGE_SCROLL_UP: + processScrollUp(optionInt(options, 0, 1)); + return true; + case PAGE_SCROLL_DOWN: + processScrollDown(optionInt(options, 0, 1)); + return true; + case TEXT_ATTRIBUTE: + int count = 0; + for (Object next : options) { + if (next != null) { + count++; + + // will throw a ClassCast exception IF NOT an int. + int value = (Integer) next; + + if (30 <= value && value <= 37) { + // foreground + processSetForegroundColor(value - 30); + } + else if (40 <= value && value <= 47) { + // background + processSetBackgroundColor(value - 40); + } + else { + switch (value) { + case ATTRIBUTE_DEFAULT_FG: + processDefaultTextColor(); + break; + case ATTRIBUTE_DEFAULT_BG: + processDefaultBackgroundColor(); + break; + case ATTRIBUTE_RESET: + processAttributeReset(); + break; + default: + processSetAttribute(value); + } + } + } + } + + if (count == 0) { + processAttributeReset(); + } + return true; + case SAVE_CURSOR_POS: + processSaveCursorPosition(); + return true; + case RESTORE_CURSOR_POS: + 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. + */ + private + boolean processOperatingSystemCommand(final ArrayList options) throws IOException { + final int command = optionInt(options, 0); + final 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) { + default: + // not exactly unknown, but not supported through dedicated process methods + processUnknownOperatingSystemCommand(command, label); + return true; + } + } catch (IllegalArgumentException ignore) { + } + + return false; + } + + protected + void processRestoreCursorPosition() throws IOException { + } + + protected + void processSaveCursorPosition() throws IOException { + } + + protected + void processScrollDown(int optionInt) throws IOException { + } + + protected + void processScrollUp(int optionInt) throws IOException { + } + + protected + void processEraseScreen(int eraseOption) throws IOException { + } + + protected + void processEraseLine(int eraseOption) throws IOException { + } + + protected + void processSetAttribute(int attribute) throws IOException { + } + + protected + void processSetForegroundColor(int color) throws IOException { + } + + protected + void processSetBackgroundColor(int color) throws IOException { + } + + protected + void processDefaultTextColor() throws IOException { + } + + protected + void processDefaultBackgroundColor() throws IOException { + } + + protected + void processAttributeReset() throws IOException { + } + + protected + void processCursorTo(int row, int col) throws IOException { + } + + protected + void processCursorToColumn(int x) throws IOException { + } + + protected + void processCursorUpLine(int count) throws IOException { + } + + protected + void processCursorDownLine(int count) throws IOException { + } + + protected + void processCursorLeft(int count) throws IOException { + } + + protected + void processCursorRight(int count) throws IOException { + } + + protected + void processCursorDown(int count) throws IOException { + } + + protected + void processCursorUp(int count) throws IOException { + } + + protected + void processUnknownExtension(ArrayList options, int command) { + } + + protected + void processUnknownOperatingSystemCommand(int command, String param) { + } + + private + int optionInt(final ArrayList options, final 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(final ArrayList options, final int index, final 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() throws IOException { + write(RESET_CODE); + flush(); + super.close(); + } +} diff --git a/src/dorkbox/console/output/AnsiRenderWriter.java b/src/dorkbox/console/output/AnsiRenderWriter.java new file mode 100644 index 0000000..a3f05fa --- /dev/null +++ b/src/dorkbox/console/output/AnsiRenderWriter.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 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 dorkbox.console.output; + +import static dorkbox.console.output.AnsiRenderer.render; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.Locale; + +/** + * Print writer which supports automatic ANSI color rendering via {@link AnsiRenderer}. + * + * @author Jason Dillon + * @author Hiram Chirino + * @since 1.1 + */ +public +class AnsiRenderWriter extends PrintWriter { + + public + AnsiRenderWriter(final OutputStream out) { + super(out); + } + + public + AnsiRenderWriter(final OutputStream out, final boolean autoFlush) { + super(out, autoFlush); + } + + public + AnsiRenderWriter(final Writer out) { + super(out); + } + + public + AnsiRenderWriter(final Writer out, final boolean autoFlush) { + super(out, autoFlush); + } + + @Override + public + void write(final String s) { + if (s != null && s.contains(AnsiRenderer.BEGIN_TOKEN)) { + super.write(render(s)); + } + else { + super.write(s); + } + } + + // + // Need to prevent partial output from being written while formatting or we will get rendering exceptions + // + + @Override + public + PrintWriter format(final String format, final Object... args) { + print(String.format(format, args)); + return this; + } + + @Override + public + PrintWriter format(final Locale l, final String format, final Object... args) { + print(String.format(l, format, args)); + return this; + } +} diff --git a/src/dorkbox/console/output/AnsiRenderer.java b/src/dorkbox/console/output/AnsiRenderer.java new file mode 100644 index 0000000..634c228 --- /dev/null +++ b/src/dorkbox/console/output/AnsiRenderer.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2009 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 dorkbox.console.output; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * 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!|@
+ * 
+ * + * @author dorkbox, llc + * @author Jason Dillon + * @author Hiram Chirino + */ +@SuppressWarnings("WeakerAccess") +public +class AnsiRenderer { + public static final String BEGIN_TOKEN = "@|"; + public static final String CODE_LIST_SEPARATOR = ","; + public static final String CODE_TEXT_SEPARATOR = " "; + public static final String END_TOKEN = "|@"; + + private static final int BEGIN_TOKEN_LEN = 2; + private static final int END_TOKEN_LEN = 2; + + private static Map codeMap = new HashMap(32); + + static + void reg(Enum anEnum, String codeName) { + reg(anEnum, codeName, false); + } + + static + void reg(Enum anEnum, String codeName, boolean isBackgroundColor) { + codeMap.put(codeName, new AnsiCode(anEnum, codeName, isBackgroundColor)); + } + + /** + * Renders {@link AnsiCode} names on the given Ansi. + * + * @param ansi The Ansi to render upon + * @param codeNames The code names to render + */ + public static + Ansi render(Ansi ansi, final String... codeNames) { + for (String codeName : codeNames) { + render(ansi, codeName); + } + return ansi; + } + + /** + * Renders a {@link AnsiCode} name on the given Ansi. + * + * @param ansi The Ansi to render upon + * @param codeName The code name to render + */ + public static + Ansi render(Ansi ansi, String codeName) { + AnsiCode ansiCode = codeMap.get(codeName.toUpperCase(Locale.ENGLISH)); + assert ansiCode != null : "Invalid ANSI code name: '" + codeName + "'"; + + if (ansiCode.isColor()) { + if (ansiCode.isBackgroundColor()) { + ansi = ansi.bg(ansiCode.getColor()); + } + else { + ansi = ansi.fg(ansiCode.getColor()); + } + } + else if (ansiCode.isAttribute()) { + ansi = ansi.a(ansiCode.getAttribute()); + } else { + assert false : "Undetermined ANSI code name: '" + codeName + "'"; + } + + return ansi; + } + + /** + * Renders text using the {@link AnsiCode} names. + * + * @param text The text to render + * @param codeNames The code names to render + */ + public static + String render(final String text, final String... codeNames) { + Ansi ansi = render(Ansi.ansi(), codeNames); + return ansi.a(text) + .reset() + .toString(); + } + + public static + String render(final String input) throws IllegalArgumentException { + StringBuilder buff = new StringBuilder(); + + int i = 0; + int j, k; + + while (true) { + j = input.indexOf(BEGIN_TOKEN, i); + if (j == -1) { + if (i == 0) { + return input; + } + else { + buff.append(input.substring(i, input.length())); + return buff.toString(); + } + } + else { + buff.append(input.substring(i, j)); + k = input.indexOf(END_TOKEN, j); + + if (k == -1) { + return input; + } + else { + j += BEGIN_TOKEN_LEN; + String spec = input.substring(j, k); + + String[] items = spec.split(CODE_TEXT_SEPARATOR, 2); + if (items.length == 1) { + return input; + } + String replacement = render(items[1], items[0].split(CODE_LIST_SEPARATOR)); + + buff.append(replacement); + + i = k + END_TOKEN_LEN; + } + } + } + } + + /** + * Renders {@link AnsiCode} names as an ANSI escape string. + * + * @param codeNames The code names to render + * + * @return an ANSI escape string. + */ + public static + String renderCodeNames(final String codeNames) { + return render(new Ansi(), codeNames.split("\\s")).toString(); + } +} diff --git a/src/dorkbox/console/output/AnsiString.java b/src/dorkbox/console/output/AnsiString.java new file mode 100644 index 0000000..9a6248e --- /dev/null +++ b/src/dorkbox/console/output/AnsiString.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2009 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 dorkbox.console.output; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * An ANSI string which reports the size of rendered text correctly (ignoring any ANSI escapes). + * + * @author Jason Dillon + * @since 1.1 + */ +public +class AnsiString implements CharSequence { + private final CharSequence encoded; + + private final CharSequence plain; + + public + AnsiString(final CharSequence str) { + assert str != null; + this.encoded = str; + this.plain = chew(str); + } + + private + CharSequence chew(final CharSequence str) { + assert str != null; + + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + AnsiOutputStream out = new AnsiOutputStream(buff); + + try { + out.write(str.toString() + .getBytes()); + out.flush(); + out.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return new String(buff.toByteArray()); + } + + public + CharSequence getEncoded() { + return encoded; + } + + public + CharSequence getPlain() { + return plain; + } + + // FIXME: charAt() and subSequence() will make things barf, need to call toString() first to get expected results + + public + int length() { + return getPlain().length(); + } + + public + char charAt(final int index) { + return getEncoded().charAt(index); + } + + public + CharSequence subSequence(final int start, final int end) { + return getEncoded().subSequence(start, end); + } + + @Override + public + int hashCode() { + return getEncoded().hashCode(); + } + + @Override + public + boolean equals(final Object obj) { + return getEncoded().equals(obj); + } + + @Override + public + String toString() { + return getEncoded().toString(); + } +} diff --git a/src/dorkbox/console/output/Attribute.java b/src/dorkbox/console/output/Attribute.java new file mode 100644 index 0000000..30f134a --- /dev/null +++ b/src/dorkbox/console/output/Attribute.java @@ -0,0 +1,74 @@ +package dorkbox.console.output; + +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_BLINK_FAST; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_BLINK_OFF; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_BLINK_SLOW; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_BOLD; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_CONCEAL_OFF; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_CONCEAL_ON; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_FAINT; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_ITALIC; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_ITALIC_OFF; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_NEGATIVE_OFF; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_NEGATIVE_ON; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_NORMAL; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_RESET; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_STRIKETHROUGH_OFF; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_STRIKETHROUGH_ON; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_UNDERLINE; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_UNDERLINE_DOUBLE; +import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_UNDERLINE_OFF; + +public +enum Attribute { + RESET(ATTRIBUTE_RESET, "RESET"), + + BOLD(ATTRIBUTE_BOLD, "BOLD"), + BOLD_OFF(ATTRIBUTE_NORMAL, "BOLD_OFF"), + + FAINT(ATTRIBUTE_FAINT, "FAINT"), + FAINT_OFF(ATTRIBUTE_NORMAL, "FAINT_OFF"), + + ITALIC(ATTRIBUTE_ITALIC, "ITALIC"), + ITALIC_OFF(ATTRIBUTE_ITALIC_OFF, "ITALIC_OFF"), + + UNDERLINE(ATTRIBUTE_UNDERLINE, "UNDERLINE"), + UNDERLINE_DOUBLE(ATTRIBUTE_UNDERLINE_DOUBLE, "UNDERLINE_DOUBLE"), + UNDERLINE_OFF(ATTRIBUTE_UNDERLINE_OFF, "UNDERLINE_OFF"), + + BLINK_SLOW(ATTRIBUTE_BLINK_SLOW, "BLINK_SLOW"), + BLINK_FAST(ATTRIBUTE_BLINK_FAST, "BLINK_FAST"), + BLINK_OFF(ATTRIBUTE_BLINK_OFF, "BLINK_OFF"), + + NEGATIVE(ATTRIBUTE_NEGATIVE_ON, "NEGATIVE"), + NEGATIVE_OFF(ATTRIBUTE_NEGATIVE_OFF, "NEGATIVE_OFF"), + + CONCEAL(ATTRIBUTE_CONCEAL_ON, "CONCEAL"), + CONCEAL_OFF(ATTRIBUTE_CONCEAL_OFF, "CONCEAL_OFF"), + + STRIKETHROUGH(ATTRIBUTE_STRIKETHROUGH_ON, "STRIKETHROUGH"), + STRIKETHROUGH_OFF(ATTRIBUTE_STRIKETHROUGH_OFF, "STRIKETHROUGH_OFF"), + ; + + private final int value; + private final String name; + + Attribute(final int index, final String name) { + this.value = index; + this.name = name; + + // register code names with the ANSI renderer + AnsiRenderer.reg(this, name); + } + + @Override + public + String toString() { + return name; + } + + public + int value() { + return value; + } +} diff --git a/src/dorkbox/console/output/Color.java b/src/dorkbox/console/output/Color.java new file mode 100644 index 0000000..bbe958c --- /dev/null +++ b/src/dorkbox/console/output/Color.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.output; + +public +enum Color { + BLACK (AnsiOutputStream.BLACK, "BLACK"), + RED (AnsiOutputStream.RED, "RED"), + GREEN (AnsiOutputStream.GREEN, "GREEN"), + YELLOW (AnsiOutputStream.YELLOW, "YELLOW"), + BLUE (AnsiOutputStream.BLUE, "BLUE"), + MAGENTA(AnsiOutputStream.MAGENTA, "MAGENTA"), + CYAN (AnsiOutputStream.CYAN, "CYAN"), + WHITE (AnsiOutputStream.WHITE, "WHITE"); + + private final int value; + private final String name; + + Color(int index, String name) { + this.value = index; + this.name = name; + + // register code names with the ANSI renderer + AnsiRenderer.reg(this, name, false); + AnsiRenderer.reg(this, "FG_" + name, false); + AnsiRenderer.reg(this, "BG_" + name, true); + } + + @Override + public + String toString() { + return name; + } + + public + int fg() { + return value + 30; + } + + public + int bg() { + return value + 40; + } + + public + int fgBright() { + return value + 90; + } + + public + int bgBright() { + return value + 100; + } +} diff --git a/src/dorkbox/console/output/Erase.java b/src/dorkbox/console/output/Erase.java new file mode 100644 index 0000000..944eae2 --- /dev/null +++ b/src/dorkbox/console/output/Erase.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.output; + +import static dorkbox.console.output.AnsiOutputStream.ERASE_ALL; +import static dorkbox.console.output.AnsiOutputStream.ERASE_TO_BEGINNING; +import static dorkbox.console.output.AnsiOutputStream.ERASE_TO_END; + +public +enum Erase { + FORWARD(ERASE_TO_END, "FORWARD"), + BACKWARD(ERASE_TO_BEGINNING, "BACKWARD"), + ALL(ERASE_ALL, "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; + } +} diff --git a/src/dorkbox/console/output/HtmlAnsiOutputStream.java b/src/dorkbox/console/output/HtmlAnsiOutputStream.java new file mode 100644 index 0000000..9d6baf3 --- /dev/null +++ b/src/dorkbox/console/output/HtmlAnsiOutputStream.java @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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 dorkbox.console.output; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Daniel Doubrovkine + */ +public +class HtmlAnsiOutputStream extends AnsiOutputStream { + + private static final String ANSI_COLOR_MAP[]; + + static { + ANSI_COLOR_MAP = new String[8]; + ANSI_COLOR_MAP[BLACK] = "black"; + ANSI_COLOR_MAP[RED] = "red"; + ANSI_COLOR_MAP[GREEN] = "green"; + ANSI_COLOR_MAP[YELLOW] = "yellow"; + ANSI_COLOR_MAP[BLUE] = "blue"; + ANSI_COLOR_MAP[MAGENTA] = "magenta"; + ANSI_COLOR_MAP[CYAN] = "cyan"; + ANSI_COLOR_MAP[WHITE] = "white"; + } + + private static final byte[] BYTES_QUOT = """.getBytes(); + private static final byte[] BYTES_AMP = "&".getBytes(); + private static final byte[] BYTES_LT = "<".getBytes(); + private static final byte[] BYTES_GT = ">".getBytes(); + + private boolean concealOn = false; + private List closingAttributes = new ArrayList(); + + public + HtmlAnsiOutputStream(OutputStream os) { + super(os); + } + + private + void write(String s) throws IOException { + super.out.write(s.getBytes()); + } + + private + void writeAttribute(String s) throws IOException { + write("<" + s + ">"); + closingAttributes.add(0, s.split(" ", 2)[0]); + } + + private + void closeAttributes() throws IOException { + for (String attr : closingAttributes) { + write(""); + } + closingAttributes.clear(); + } + + public + void write(int data) throws IOException { + switch (data) { + case 34: // " + out.write(BYTES_QUOT); + break; + case 38: // & + out.write(BYTES_AMP); + break; + case 60: // < + out.write(BYTES_LT); + break; + case 62: // > + out.write(BYTES_GT); + break; + default: + super.write(data); + } + } + + @Override + protected + void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_CONCEAL_ON: + write("\u001B[8m"); + concealOn = true; + break; + case ATTRIBUTE_BOLD: + writeAttribute("b"); + break; + case ATTRIBUTE_NORMAL: + closeAttributes(); + break; + case ATTRIBUTE_UNDERLINE: + writeAttribute("u"); + break; + case ATTRIBUTE_UNDERLINE_OFF: + closeAttributes(); + break; + case ATTRIBUTE_NEGATIVE_ON: + break; + case ATTRIBUTE_NEGATIVE_OFF: + break; + } + } + + @Override + protected + void processSetForegroundColor(final int color) throws IOException { + writeAttribute("span style=\"color: " + ANSI_COLOR_MAP[color] + ";\""); + } + + @Override + protected + void processSetBackgroundColor(final int color) throws IOException { + writeAttribute("span style=\"background-color: " + ANSI_COLOR_MAP[color] + ";\""); + } + + @Override + protected + void processAttributeReset() throws IOException { + if (concealOn) { + write("\u001B[0m"); + concealOn = false; + } + closeAttributes(); + } + + @Override + public + void close() throws IOException { + closeAttributes(); + super.close(); + } + + public + void writeLine(final byte[] buf, final int offset, final int len) throws IOException { + write(buf, offset, len); + closeAttributes(); + } +} diff --git a/src/dorkbox/console/output/WindowsAnsiOutputStream.java b/src/dorkbox/console/output/WindowsAnsiOutputStream.java new file mode 100644 index 0000000..3bef1a8 --- /dev/null +++ b/src/dorkbox/console/output/WindowsAnsiOutputStream.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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. + * + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.output; + +import static dorkbox.console.util.windows.Kernel32.ASSERT; + +import java.io.IOException; +import java.io.OutputStream; + +import dorkbox.console.util.windows.CONSOLE_SCREEN_BUFFER_INFO; +import dorkbox.console.util.windows.COORD; +import dorkbox.console.util.windows.HANDLE; +import dorkbox.console.util.windows.Kernel32; + +/** + * A Windows ANSI escape processor, uses JNA direct-mapping to access native platform API's to change the console attributes. + * + * See: https://en.wikipedia.org/wiki/ANSI_escape_code + * + * @author dorkbox, llc + * @author Hiram Chirino + * @author Joris Kuipers + */ +@SuppressWarnings("NumericCastThatLosesPrecision") +final class WindowsAnsiOutputStream extends AnsiOutputStream { + private static final short ANSI_FOREGROUND_COLOR_MAP[]; + private static final short ANSI_BACKGROUND_COLOR_MAP[]; + + static { + ANSI_FOREGROUND_COLOR_MAP = new short[8]; + ANSI_FOREGROUND_COLOR_MAP[BLACK] = Kernel32.FOREGROUND_BLACK; + ANSI_FOREGROUND_COLOR_MAP[RED] = Kernel32.FOREGROUND_RED; + ANSI_FOREGROUND_COLOR_MAP[GREEN] = Kernel32.FOREGROUND_GREEN; + ANSI_FOREGROUND_COLOR_MAP[YELLOW] = Kernel32.FOREGROUND_YELLOW; + ANSI_FOREGROUND_COLOR_MAP[BLUE] = Kernel32.FOREGROUND_BLUE; + ANSI_FOREGROUND_COLOR_MAP[MAGENTA] = Kernel32.FOREGROUND_MAGENTA; + ANSI_FOREGROUND_COLOR_MAP[CYAN] = Kernel32.FOREGROUND_CYAN; + ANSI_FOREGROUND_COLOR_MAP[WHITE] = Kernel32.FOREGROUND_GREY; + + ANSI_BACKGROUND_COLOR_MAP = new short[8]; + ANSI_BACKGROUND_COLOR_MAP[BLACK] = Kernel32.BACKGROUND_BLACK; + ANSI_BACKGROUND_COLOR_MAP[RED] = Kernel32.BACKGROUND_RED; + ANSI_BACKGROUND_COLOR_MAP[GREEN] = Kernel32.BACKGROUND_GREEN; + ANSI_BACKGROUND_COLOR_MAP[YELLOW] = Kernel32.BACKGROUND_YELLOW; + ANSI_BACKGROUND_COLOR_MAP[BLUE] = Kernel32.BACKGROUND_BLUE; + ANSI_BACKGROUND_COLOR_MAP[MAGENTA] = Kernel32.BACKGROUND_MAGENTA; + ANSI_BACKGROUND_COLOR_MAP[CYAN] = Kernel32.BACKGROUND_CYAN; + ANSI_BACKGROUND_COLOR_MAP[WHITE] = Kernel32.BACKGROUND_GREY; + } + + private final HANDLE console; + private final CONSOLE_SCREEN_BUFFER_INFO originalInfo = new CONSOLE_SCREEN_BUFFER_INFO(); + private volatile CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + + private volatile boolean negative; + private volatile short savedX = (short) -1; + private volatile short savedY = (short) -1; + + WindowsAnsiOutputStream(final OutputStream os, int fileHandle) throws IOException { + super(os); + + if (fileHandle == AnsiConsole.STDOUT_FILENO) { + fileHandle = Kernel32.STD_OUTPUT_HANDLE; + } else if (fileHandle == AnsiConsole.STDERR_FILENO) { + fileHandle = Kernel32.STD_ERROR_HANDLE; + } else { + throw new IllegalArgumentException("Invalid file handle " + fileHandle); + } + + console = Kernel32.GetStdHandle(fileHandle); + if (console == HANDLE.INVALID_HANDLE_VALUE) { + throw new IOException("Unable to get input console handle."); + } + + out.flush(); + ASSERT(Kernel32.GetConsoleScreenBufferInfo(console, originalInfo), "Could not get the screen info"); + } + + private + void getConsoleInfo() throws IOException { + out.flush(); + ASSERT(Kernel32.GetConsoleScreenBufferInfo(console, info), "Could not get the screen info:"); + } + + private + void applyAttributes() throws IOException { + out.flush(); + + short attributes = info.attributes; + if (negative) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 8; + int bg = 0X00F0 * attributes; + bg >>= 8; + attributes = (short) (attributes & 0xFF00 | fg | bg); + } + + ASSERT(Kernel32.SetConsoleTextAttribute(console, attributes), "Could not set text attributes"); + } + + private + void applyCursorPosition() throws IOException { + ASSERT(Kernel32.SetConsoleCursorPosition(console, info.cursorPosition.asValue()), "Could not set cursor position"); + } + + @Override + protected + void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + out.flush(); + info.cursorPosition.x = savedX; + info.cursorPosition.y = savedY; + applyCursorPosition(); + } + } + + @Override + protected + void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition.x; + savedY = info.cursorPosition.y; + } + + @Override + protected + void processEraseScreen(final int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_ALL: + COORD topLeft = new COORD(); + topLeft.x = (short) 0; + topLeft.y = info.window.top; + int screenLength = info.window.height() * info.size.x; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, screenLength, topLeft.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft.asValue(), written), "Could not fill console"); + break; + case ERASE_TO_BEGINNING: + COORD topLeft2 = new COORD(); + topLeft2.x = (short) 0; + topLeft2.y = info.window.top; + int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, lengthToCursor, topLeft2.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2.asValue(), written), "Could not fill console"); + break; + case ERASE_TO_END: + int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + info.size.x - info.cursorPosition.x; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, lengthToEnd, info.cursorPosition.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.asValue(), written), "Could not fill console"); + } + } + + @Override + protected + void processEraseLine(final int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_ALL: + COORD currentRow = info.cursorPosition.asValue(); + currentRow.x = (short) 0; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, info.size.x, currentRow.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', info.size.x, currentRow.asValue(), written), "Could not fill console"); + break; + case ERASE_TO_BEGINNING: + COORD leftColCurrRow2 = info.cursorPosition.asValue(); + leftColCurrRow2.x = (short) 0; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, info.cursorPosition.x, leftColCurrRow2.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2.asValue(), written), "Could not fill console"); + break; + case ERASE_TO_END: + int lengthToLastCol = info.size.x - info.cursorPosition.x; + + ASSERT(Kernel32.FillConsoleOutputAttribute(console, originalInfo.attributes, lengthToLastCol, info.cursorPosition.asValue(), written), "Could not fill console"); + ASSERT(Kernel32.FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.asValue(), written), "Could not fill console"); + } + } + + @Override + protected + void processSetAttribute(final int attribute) throws IOException { + if (90 <= attribute && attribute <= 97) { + // foreground bright + info.attributes = (short) (info.attributes & ~0x000F | ANSI_FOREGROUND_COLOR_MAP[attribute - 90]); + info.attributes = (short) (info.attributes | Kernel32.FOREGROUND_INTENSITY); + applyAttributes(); + return; + } else if (100 <= attribute && attribute <= 107) { + // background bright + info.attributes = (short) (info.attributes & ~0x00F0 | ANSI_BACKGROUND_COLOR_MAP[attribute - 100]); + info.attributes = (short) (info.attributes | Kernel32.BACKGROUND_INTENSITY); + applyAttributes(); + return; + } + + switch (attribute) { + case ATTRIBUTE_BOLD: + info.attributes = (short) (info.attributes | Kernel32.FOREGROUND_INTENSITY); + applyAttributes(); + break; + case ATTRIBUTE_NORMAL: + info.attributes = (short) (info.attributes & ~Kernel32.FOREGROUND_INTENSITY); + applyAttributes(); + 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 | Kernel32.BACKGROUND_INTENSITY); + applyAttributes(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes = (short) (info.attributes & ~Kernel32.BACKGROUND_INTENSITY); + applyAttributes(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttributes(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttributes(); + break; + } + } + + @Override + protected + void processSetForegroundColor(final int color) throws IOException { + info.attributes = (short) (info.attributes & ~0x000F | ANSI_FOREGROUND_COLOR_MAP[color]); + applyAttributes(); + } + + @Override + protected + void processSetBackgroundColor(final int color) throws IOException { + info.attributes = (short) (info.attributes & ~0x00F0 | ANSI_BACKGROUND_COLOR_MAP[color]); + applyAttributes(); + } + + @Override + protected + void processDefaultTextColor() throws IOException { + info.attributes = (short) (info.attributes & ~0x000F | originalInfo.attributes & 0x000F); + applyAttributes(); + } + + @Override + protected + void processDefaultBackgroundColor() throws IOException { + info.attributes = (short) (info.attributes & ~0x00F0 | originalInfo.attributes & 0x00F0); + applyAttributes(); + } + + @Override + protected + void processAttributeReset() throws IOException { + //info.attributes = originalInfo.attributes; + info.attributes = (short)((info.attributes & ~0x00FF ) | originalInfo.attributes); + this.negative = false; + applyAttributes(); + } + + @Override + protected + void processScrollDown(final int optionInt) throws IOException { + } + + @Override + protected + void processScrollUp(final int optionInt) throws IOException { + } + + protected + void processCursorUpLine(final int count) throws IOException { + } + + protected + void processCursorDownLine(final int count) throws IOException { + } + + @Override + protected + void processCursorTo(final int row, final 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(final int x) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); + applyCursorPosition(); + } + + @Override + protected + void processCursorLeft(final int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); + applyCursorPosition(); + } + + @Override + protected + void processCursorRight(final int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); + applyCursorPosition(); + } + + @Override + protected + void processCursorDown(final int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.min(info.size.y, info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected + void processCursorUp(final int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + public void close() throws IOException { + super.close(); + + if (console != null) { + Kernel32.CloseHandle(console); + } + } +} diff --git a/src/dorkbox/console/util/posix/CLibraryPosix.java b/src/dorkbox/console/util/posix/CLibraryPosix.java new file mode 100644 index 0000000..c06677e --- /dev/null +++ b/src/dorkbox/console/util/posix/CLibraryPosix.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.console.util.posix; + +import java.nio.ByteBuffer; + +import com.sun.jna.Native; +import com.sun.jna.ptr.IntByReference; + +@SuppressWarnings("ALL") +public +class CLibraryPosix { + static { + Native.register("c"); + } + + // MAGIC! + public static final int TIOCGWINSZ = System.getProperty("os.name").equalsIgnoreCase("linux") ? 0x5413 : 1074295912; + + + public static native + int isatty(int fd); + + public static native + int read(int fd, IntByReference c, int count); + + /** + * Original signature : int ioctl(int, int, char*)
+ */ + public static native + int ioctl(int d, int request, ByteBuffer data); + + /** + * Put the state of FD into *TERMIOS_P.
+ *

+ * Original signature : int tcgetattr(int, char*)
+ */ + public static native + int tcgetattr(int fd, Termios termios_p); + + /** + * Set the state of FD to *TERMIOS_P.
+ *

+ * Values for OPTIONAL_ACTIONS (TCSA*) are in .
+ *

+ * Original signature : int tcsetattr(int, int, char*)
+ */ + public static native + int tcsetattr(int fd, int optional_actions, Termios termios_p); +} diff --git a/src/dorkbox/console/util/posix/Termios.java b/src/dorkbox/console/util/posix/Termios.java new file mode 100644 index 0000000..d49adb6 --- /dev/null +++ b/src/dorkbox/console/util/posix/Termios.java @@ -0,0 +1,184 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.console.util.posix; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +@SuppressWarnings("ALL") +public +class Termios extends Structure { + // NOTE: MUST BE BITS!! from: /usr/include/x86_64-linux-gnu/bits/termios.h + // the one in octal WILL NOT WORK!! (you have been warned) + + // Definitions at: http://linux.die.net/man/3/termios + + // Input flags - software input processing + public static class Input { + public static final int IGNBRK = 0000001; // ignore BREAK condition + public static final int BRKINT = 0000002; // map BREAK to SIGINTR + public static final int IGNPAR = 0000004; // ignore (discard) parity errors + public static final int PARMRK = 0000010; // mark parity and framing errors + public static final int INPCK = 0000020; // enable checking of parity errors + public static final int ISTRIP = 0000040; // strip 8th bit off chars + public static final int INLCR = 0000100; // map NL into CR + public static final int IGNCR = 0000200; // ignore CR + public static final int ICRNL = 0000400; // map CR to NL (ala CRMOD) + // public static final int IUCLC = 0001000; // (not in POSIX) Map uppercase characters to lowercase on input. + public static final int IXON = 0002000; // enable output flow control + public static final int IXANY = 0004000; // any char will restart after stop + public static final int IXOFF = 0010000; // enable input flow control + public static final int IMAXBEL = 0020000; // ring bell on input queue full + // public static final int IUTF8 = 0040000; // (since Linux 2.6.4) (not in POSIX) Input is UTF8; this allows character-erase to be correctly performed in cooked mode. + } + + public static class Output { + // Output flags - software output processing + public static final int OPOST = 0000001; // enable following output processing (not set = raw output) + // public static final int OLCUC = 0000002; // (not in POSIX) Map lowercase characters to uppercase on output. + public static final int ONLCR = 0000004; // map NL to CR-NL (ala CRMOD) + public static final int OCRNL = 0000010; // map CR to NL on output + public static final int ONOCR = 0000020; // no CR output at column 0 + public static final int ONLRET = 0000040; // NL performs CR function + public static final int OFILL = 0000100; // Send fill characters for a delay, rather than using a timed delay. + public static final int OFDEL = 0000200; // Fill character is ASCII DEL (0177). If unset, fill character is ASCII NUL ('\0'). (Not implemented on Linux.) + } + + + public static class Control { + // Control flags - hardware control of terminal + public static final int CSIZE = 0000060; // character size mask + public static final int CS5 = 0000000; // 5 bits (pseudo) + public static final int CS6 = 0000020; // 6 bits + public static final int CS7 = 0000040; // 7 bits + public static final int CS8 = 0000060; // 8 bits + public static final int CSTOPB = 0000100; // send 2 stop bits + public static final int CREAD = 0000200; // enable receiver + public static final int PARENB = 0000400; // parity enable + public static final int PARODD = 0001000; // odd parity, else even + public static final int HUPCL = 0002000; // hang up on last close + public static final int CLOCAL = 0004000; // ignore modem status lines + } + + + public static class Local { + // "Local" flags - dumping ground for other state + // Warning: some flags in this structure begin with the letter "I" and look like they belong in the input flag. + public static final int ISIG = 0000001; // enable signals INTR, QUIT, [D]SUSP + public static final int ICANON = 0000002; // canonicalize input lines + //public static final int XCASE = 0000004; // (not in POSIX; not supported under Linux) + public static final int ECHO = 0000010; // enable echoing + public static final int ECHOE = 0000020; // visually erase chars + public static final int ECHOK = 0000040; // echo NL after line kill + public static final int ECHONL = 0000100; // echo NL even if ECHO is off + public static final int NOFLSH = 0000200; // don't flush after interrupt + public static final int TOSTOP = 0000400; // stop background jobs from output + public static final int ECHOCTL = 0001000; // echo control chars as ^(Char) + public static final int ECHOPRT = 0002000; // visual erase mode for hardcopy + public static final int ECHOKE = 0004000; // visual erase for line kill + public static final int FLUSHO = 0001000; // output being flushed (state) + public static final int PENDIN = 0004000; // XXX retype pending input (state) + public static final int IEXTEN = 0100000; // enable DISCARD and LNEXT + public static final int EXTPROC = 0200000; // external processing + } + + + public static class ControlChars { + // Special Control Characters + // + // the value is the index into c_cc[] character array. + public static final int VINTR = 0; // ISIG + public static final int VQUIT = 1; // ISIG + public static final int VERASE = 2; // ICANON + public static final int VKILL = 3; // ICANON + public static final int VEOF = 4; // ICANON + public static final int VTIME = 5; // !ICANON + public static final int VMIN = 6; // !ICANON + public static final int VSWTC = 7; + public static final int VSTART = 8; // IXON, IXOFF + public static final int VSTOP = 9; // IXON, IXOFF + public static final int VSUSP = 10;// ISIG + public static final int VEOL = 11;// ICANON + public static final int VREPRINT = 12;// ICANON together with IEXTEN + public static final int VDISCARD = 13; + public static final int VWERASE = 14;// ICANON together with IEXTEN + public static final int VLNEXT = 15;// IEXTEN + public static final int VEOL2 = 16;// ICANON together with IEXTEN + } + + + + + public static final int TCSANOW = 0; + + /** + * input mode flags + */ + public int inputFlags; + + /** + * output mode flags + */ + public int outputFlags; + + /** + * control mode flags + */ + public int controlFlags; + + /** + * local mode flags + */ + public int localFlags; + + /** + * line discipline + */ + public char lineDiscipline; + + /** + * control characters + */ + public byte[] controlChars = new byte[32]; + + /** + * input speed + */ + public int inputSpeed; + + /** + * output speed + */ + public int outputSpeed; + + public + Termios() {} + + @Override + protected + List getFieldOrder() { + return Arrays.asList("inputFlags", + "outputFlags", + "controlFlags", + "localFlags", + "lineDiscipline", + "controlChars", + "inputSpeed", + "outputSpeed"); + } +} diff --git a/src/dorkbox/console/util/windows/CONSOLE_SCREEN_BUFFER_INFO.java b/src/dorkbox/console/util/windows/CONSOLE_SCREEN_BUFFER_INFO.java new file mode 100644 index 0000000..714e8b8 --- /dev/null +++ b/src/dorkbox/console/util/windows/CONSOLE_SCREEN_BUFFER_INFO.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +/** + * https://msdn.microsoft.com/en-us/library/ms682093%28VS.85%29.aspx + */ +public +class CONSOLE_SCREEN_BUFFER_INFO extends Structure { + + public COORD size = new COORD(); + public COORD cursorPosition = new COORD(); + public short attributes = (short) 0; + public SMALL_RECT window = new SMALL_RECT(); + public COORD maximumWindowSize = new COORD(); + + @Override + protected + List getFieldOrder() { + return Arrays.asList("size", "cursorPosition", "attributes", "window", "maximumWindowSize"); + } + + @Override + public String toString() { + return "Size: " + size + " CursorPos: " + cursorPosition + " Attribs: " + attributes + " Window: " + window + " MaxWindowSize: " + maximumWindowSize; + } +} diff --git a/src/dorkbox/console/util/windows/COORD.java b/src/dorkbox/console/util/windows/COORD.java new file mode 100644 index 0000000..84da97f --- /dev/null +++ b/src/dorkbox/console/util/windows/COORD.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +/** + * https://msdn.microsoft.com/en-us/library/ms682119(v=vs.85).aspx + */ +public +class COORD extends Structure { + static public class ByValue extends COORD implements Structure.ByValue { } + + public short x; + public short y; + + public + COORD.ByValue asValue() { + COORD.ByValue copy = new COORD.ByValue(); + copy.x = this.x; + copy.y = this.y; + return copy; + } + + @Override + protected + List getFieldOrder() { + return Arrays.asList("x", "y"); + } + + @Override + public String toString() { + return x + ":" + y; + } +} diff --git a/src/dorkbox/console/util/windows/CharUnion.java b/src/dorkbox/console/util/windows/CharUnion.java new file mode 100644 index 0000000..5cb20d7 --- /dev/null +++ b/src/dorkbox/console/util/windows/CharUnion.java @@ -0,0 +1,31 @@ +package dorkbox.console.util.windows; + +import com.sun.jna.Union; + +public class CharUnion extends Union { + public char unicodeChar; + public byte asciiChar; + + public CharUnion() { + } + + public CharUnion(char c) { + setType(char.class); + unicodeChar = c; + } + + public CharUnion(byte c) { + setType(byte.class); + asciiChar = c; + } + + public void set(char c) { + setType(char.class); + unicodeChar = c; + } + + public void set(byte c) { + setType(byte.class); + asciiChar = c; + } +} diff --git a/src/dorkbox/inputConsole/windows/ConsoleMode.java b/src/dorkbox/console/util/windows/ConsoleMode.java similarity index 79% rename from src/dorkbox/inputConsole/windows/ConsoleMode.java rename to src/dorkbox/console/util/windows/ConsoleMode.java index f88e8b9..092b518 100644 --- a/src/dorkbox/inputConsole/windows/ConsoleMode.java +++ b/src/dorkbox/console/util/windows/ConsoleMode.java @@ -9,7 +9,7 @@ * @author Marc Prud'hommeaux * @author Jason Dillon */ -package dorkbox.inputConsole.windows; +package dorkbox.console.util.windows; /** * Console mode @@ -17,6 +17,13 @@ package dorkbox.inputConsole.windows; * Constants copied wincon.h. */ public enum ConsoleMode { + /** + * CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or + * ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the + * ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and linefeed characters are handled by the system. + */ + ENABLE_PROCESSED_INPUT(1), + /** * The ReadFile or ReadConsole function returns only when a carriage return character is read. If this mode is disable, the functions * return when one or more characters are available. @@ -30,25 +37,12 @@ public enum ConsoleMode { ENABLE_ECHO_INPUT(4), /** - * CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or - * ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the - * ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and linefeed characters are handled by the system. - */ - ENABLE_PROCESSED_INPUT(1), - - /** - * User interactions that change the size of the console screen buffer are reported in the console's input buffee. Information about + * User interactions that change the size of the console screen buffer are reported in the console's input buffer. Information about * these events can be read from the input buffer by applications using theReadConsoleInput function, but not by those using ReadFile * orReadConsole. */ ENABLE_WINDOW_INPUT(8), - - /** - * If the mouse pointer is within the borders of the console window and the window has the keyboard focus, mouse events generated by - * mouse movement and button presses are placed in the input buffer. These events are discarded by ReadFile or ReadConsole, even when - * this mode is enabled. - */ - ENABLE_MOUSE_INPUT(16), ; + ; public final int code; diff --git a/src/dorkbox/console/util/windows/HANDLE.java b/src/dorkbox/console/util/windows/HANDLE.java new file mode 100644 index 0000000..3662689 --- /dev/null +++ b/src/dorkbox/console/util/windows/HANDLE.java @@ -0,0 +1,74 @@ +/* + *Copyright (c) 2010 Daniel Doubrovkine, All Rights Reserved + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * + * This library is licensed under the LGPL, version 2.1 or later, and + * (from version 4.0 onward) the Apache Software License, version 2.0. + * Commercial license arrangements are negotiable. + */ +package dorkbox.console.util.windows; + +import com.sun.jna.FromNativeContext; +import com.sun.jna.Pointer; +import com.sun.jna.PointerType; + +/** + * Handle to an object. + */ +public +class HANDLE extends PointerType { + /** Constant value representing an invalid HANDLE. */ + public static final HANDLE INVALID_HANDLE_VALUE = new HANDLE(Pointer.createConstant(Pointer.SIZE == 8 ? -1 : 0xFFFFFFFFL)); + + private boolean immutable; + + public + HANDLE() { + } + + public + HANDLE(Pointer p) { + setPointer(p); + immutable = true; + } + + @Override + public + void setPointer(Pointer p) { + if (immutable) { + throw new UnsupportedOperationException("immutable reference"); + } + + super.setPointer(p); + } + + /** + * Override to the appropriate object for INVALID_HANDLE_VALUE. + */ + @Override + public + Object fromNative(Object nativeValue, FromNativeContext context) { + Object o = super.fromNative(nativeValue, context); + + if (INVALID_HANDLE_VALUE.equals(o)) { + return INVALID_HANDLE_VALUE; + } + return o; + } + + @Override + public + String toString() { + return String.valueOf(getPointer()); + } +} diff --git a/src/dorkbox/console/util/windows/INPUT_RECORD.java b/src/dorkbox/console/util/windows/INPUT_RECORD.java new file mode 100644 index 0000000..073c3f5 --- /dev/null +++ b/src/dorkbox/console/util/windows/INPUT_RECORD.java @@ -0,0 +1,44 @@ +package dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; +import com.sun.jna.Union; + +/** + * https://msdn.microsoft.com/en-us/library/ms683499(v=VS.85).aspx + */ +public class INPUT_RECORD extends Structure { + static public class ByReference extends INPUT_RECORD implements Structure.ByReference {} + + public static final short KEY_EVENT = 0x0001; + public static final short MOUSE_EVENT = 0x0002; + + public short EventType; + public EventUnion Event; + + public static class EventUnion extends Union { + public KEY_EVENT_RECORD KeyEvent; + public MOUSE_EVENT_RECORD MouseEvent; + } + + @Override + public void read() { + readField("EventType"); + switch (EventType) { + case KEY_EVENT: + Event.setType(KEY_EVENT_RECORD.class); + break; + case MOUSE_EVENT: + Event.setType(MOUSE_EVENT_RECORD.class); + break; + } + super.read(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("EventType", "Event"); + } +} diff --git a/src/dorkbox/console/util/windows/KEY_EVENT_RECORD.java b/src/dorkbox/console/util/windows/KEY_EVENT_RECORD.java new file mode 100644 index 0000000..43e575f --- /dev/null +++ b/src/dorkbox/console/util/windows/KEY_EVENT_RECORD.java @@ -0,0 +1,23 @@ +package dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +/** + * https://msdn.microsoft.com/en-us/library/ms684166(v=VS.85).aspx + */ +public class KEY_EVENT_RECORD extends Structure { + public boolean keyDown; + public short repeatCount; + public short virtualKeyCode; + public short virtualScanCode; + public CharUnion uChar; + public int controlKeyState; + + @Override + protected List getFieldOrder() { + return Arrays.asList("keyDown", "repeatCount", "virtualKeyCode", "virtualScanCode", "uChar", "controlKeyState"); + } +} diff --git a/src/dorkbox/console/util/windows/Kernel32.java b/src/dorkbox/console/util/windows/Kernel32.java new file mode 100644 index 0000000..30e6abe --- /dev/null +++ b/src/dorkbox/console/util/windows/Kernel32.java @@ -0,0 +1,173 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.util.windows; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; + +public class Kernel32 { + static { + Native.register("kernel32"); + } + + // see: http://msdn.microsoft.com/en-us/library/ms682013%28VS.85%29.aspx + public static final short FOREGROUND_BLACK = (short) 0x0000; + public static final short FOREGROUND_BLUE = (short) 0x0001; + public static final short FOREGROUND_GREEN = (short) 0x0002; + public static final short FOREGROUND_CYAN = (short) 0x0003; + public static final short FOREGROUND_RED = (short) 0x0004; + public static final short FOREGROUND_MAGENTA = (short) 0x0005; + public static final short FOREGROUND_YELLOW = (short) 0x0006; + public static final short FOREGROUND_GREY = (short) 0x0007; + public static final short FOREGROUND_INTENSITY = (short) 0x0008; // foreground color is intensified. + + public static final short BACKGROUND_BLACK = (short) 0x0000; + public static final short BACKGROUND_BLUE = (short) 0x0010; + public static final short BACKGROUND_GREEN = (short) 0x0020; + public static final short BACKGROUND_CYAN = (short) 0x0030; + public static final short BACKGROUND_RED = (short) 0x0040; + public static final short BACKGROUND_MAGENTA = (short) 0x0050; + public static final short BACKGROUND_YELLOW = (short) 0x0060; + public static final short BACKGROUND_GREY = (short) 0x0070; + public static final short BACKGROUND_INTENSITY = (short) 0x0080; // background color is intensified. + + + public static final short COMMON_LVB_LEADING_BYTE = (short) 0x0100; + public static final short COMMON_LVB_TRAILING_BYTE = (short) 0x0200; + public static final short COMMON_LVB_GRID_HORIZONTAL = (short) 0x0400; + public static final short COMMON_LVB_GRID_LVERTICAL = (short) 0x0800; + public static final short COMMON_LVB_GRID_RVERTICAL = (short) 0x1000; + public static final short COMMON_LVB_REVERSE_VIDEO = (short) 0x4000; + public static final short COMMON_LVB_UNDERSCORE = (short) 0x8000; + + + private static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x1000; + + public static final int STD_INPUT_HANDLE = -10; + public static final int STD_OUTPUT_HANDLE = -11; + public static final int STD_ERROR_HANDLE = -12; + + + /** + * https://msdn.microsoft.com/en-us/library/ms683231%28VS.85%29.aspx + */ + public static native + + HANDLE GetStdHandle(int stdHandle); + + /** + * https://msdn.microsoft.com/en-us/library/ms724211%28VS.85%29.aspx + */ + public static native + int CloseHandle(HANDLE handle); + + /** + * https://msdn.microsoft.com/en-us/library/ms686047%28VS.85%29.aspx + */ + public static native + int SetConsoleTextAttribute(HANDLE consoleOutput, short attributes); + + /** + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx + */ + public static native + int FormatMessageA(int flags, Pointer source, int messageId, int languageId, byte[] buffer, int size, long[] args); + + + /** + * https://msdn.microsoft.com/en-us/library/ms683171%28VS.85%29.aspx + */ + public static native + int GetConsoleScreenBufferInfo(HANDLE consoleOutput, CONSOLE_SCREEN_BUFFER_INFO consoleScreenBufferInfo); + + /** + * https://msdn.microsoft.com/en-us/library/ms686025%28VS.85%29.aspx + */ + public static native + int SetConsoleCursorPosition(HANDLE consoleOutput, COORD.ByValue cursorPosition); + + + /** + * https://msdn.microsoft.com/en-us/library/ms682662%28VS.85%29.aspx + */ + public static native + int FillConsoleOutputAttribute(HANDLE consoleOutput, short attribute, int length, COORD.ByValue writeCoord, int[] numberOfAttrsWritten); + + /** + * https://msdn.microsoft.com/en-us/library/ms682663%28VS.85%29.aspx + */ + public static native + int FillConsoleOutputCharacterW(HANDLE consoleOutput, char character, int length, COORD.ByValue writeCoord, int[] numberOfCharsWritten); + + + + /** + * https://msdn.microsoft.com/en-us/library/ms683167%28VS.85%29.aspx + */ + public static native + int GetConsoleMode(HANDLE handle, IntByReference mode); + + /** + * https://msdn.microsoft.com/en-us/library/ms686033%28VS.85%29.aspx + */ + public static native + int SetConsoleMode(HANDLE handle, int mode); + + /** + * https://msdn.microsoft.com/en-us/library/ms684961(v=VS.85).aspx + */ + public static native + int ReadConsoleInputW(HANDLE handle, INPUT_RECORD.ByReference inputRecords, int length, IntByReference eventsCount); + + public static void ASSERT(final int returnValue, final String message) { + // if returnValue == 0, throw assertion error + assert returnValue != 0 : message + " : " + getLastErrorMessage(); + } + + + private interface Win10 { + boolean IsWindows10OrGreater(); + } + + private static + String getLastErrorMessage() { + int errorCode = Native.getLastError(); + int bufferSize = 160; + byte data[] = new byte[bufferSize]; + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, Pointer.NULL, errorCode, 0, data, bufferSize, null); + return new String(data); + } + + /** + * Windows 10+ supports ANSI according to microsoft + */ + public static + boolean isWindows10OrGreater() { + try { + final Object kernel32 = Native.loadLibrary("kernel32", Win10.class); + if (kernel32 != null) { + boolean isWin10Plus = ((Win10)kernel32).IsWindows10OrGreater(); + Native.unregister(Win10.class); + return isWin10Plus; + } + + return false; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/dorkbox/console/util/windows/MOUSE_EVENT_RECORD.java b/src/dorkbox/console/util/windows/MOUSE_EVENT_RECORD.java new file mode 100644 index 0000000..0e66a7b --- /dev/null +++ b/src/dorkbox/console/util/windows/MOUSE_EVENT_RECORD.java @@ -0,0 +1,18 @@ +package dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +public class MOUSE_EVENT_RECORD extends Structure { + public COORD mousePosition; + public int buttonState; + public int controlKeyState; + public int eventFlags; + + @Override + protected List getFieldOrder() { + return Arrays.asList("mousePosition", "buttonState", "controlKeyState", "eventFlags"); + } +} diff --git a/src/dorkbox/console/util/windows/SMALL_RECT.java b/src/dorkbox/console/util/windows/SMALL_RECT.java new file mode 100644 index 0000000..5bef4ee --- /dev/null +++ b/src/dorkbox/console/util/windows/SMALL_RECT.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 dorkbox, llc + * + * 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 dorkbox.console.util.windows; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +/** + * https://msdn.microsoft.com/en-us/library/ms686311%28VS.85%29.aspx + */ +public class SMALL_RECT extends Structure { + + public short left; + public short top; + public short right; + public short bottom; + + public short width() { + return (short) (this.right-this.left); + } + public short height() { + return (short) (this.bottom-this.top); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("left", "top", "right", "bottom"); + } + + @Override + public String toString() { + return "LTRB: " + left + "," + top + "," + right + "," + bottom; + } +} diff --git a/src/dorkbox/inputConsole/Encoding.java b/src/dorkbox/inputConsole/Encoding.java deleted file mode 100644 index a8450fb..0000000 --- a/src/dorkbox/inputConsole/Encoding.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2002-2012, the original author or authors. - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * http://www.opensource.org/licenses/bsd-license.php - * - * @author Jason Dillon - * @author Guillaume Nodet - */ -package dorkbox.inputConsole; - -import java.nio.charset.Charset; - -public class Encoding { - - /** - * Get the default encoding. Will first look at the LC_CTYPE environment variable, then the input.encoding system property, then the - * default charset according to the JVM. - * - * @return The default encoding to use when none is specified. - */ - public static String get() { - // LC_CTYPE is usually in the form en_US.UTF-8 - String envEncoding = extractEncodingFromCtype(System.getenv("LC_CTYPE")); - if (envEncoding != null) { - return envEncoding; - } - return System.getProperty("input.encoding", Charset.defaultCharset().name()); - } - - /** - * Parses the LC_CTYPE value to extract the encoding according to the POSIX standard, which says that the LC_CTYPE environment variable - * may be of the format [language[_territory][.codeset][@modifier]] - * - * @param ctype The ctype to parse, may be null - * @return The encoding, if one was present, otherwise null - */ - private static String extractEncodingFromCtype(String ctype) { - if (ctype != null && ctype.indexOf('.') > 0) { - String encodingAndModifier = ctype.substring(ctype.indexOf('.') + 1); - if (encodingAndModifier.indexOf('@') > 0) { - return encodingAndModifier.substring(0, encodingAndModifier.indexOf('@')); - } - return encodingAndModifier; - } - return null; - } -} diff --git a/src/dorkbox/inputConsole/InputConsole.java b/src/dorkbox/inputConsole/InputConsole.java deleted file mode 100644 index 9e7989b..0000000 --- a/src/dorkbox/inputConsole/InputConsole.java +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * 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 dorkbox.inputConsole; - -import dorkbox.inputConsole.posix.UnixTerminal; -import dorkbox.inputConsole.unsupported.UnsupportedTerminal; -import dorkbox.inputConsole.windows.WindowsTerminal; -import dorkbox.objectPool.ObjectPool; -import dorkbox.util.FastThreadLocal; -import dorkbox.util.OS; -import dorkbox.util.bytes.ByteBuffer2; -import dorkbox.util.bytes.ByteBuffer2Poolable; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.AnsiConsole; -import org.slf4j.Logger; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.net.URL; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -public -class InputConsole { - - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InputConsole.class); - private static final InputConsole consoleProxyReader = new InputConsole(); - private static final char[] emptyLine = new char[0]; - - // this is run when init is called - static { - AnsiConsole.systemInstall(); - - Thread consoleThread = new Thread(new Runnable() { - @Override - public - void run() { - consoleProxyReader.run(); - } - }); - consoleThread.setDaemon(true); - consoleThread.setName("Console Input Reader"); - - consoleThread.start(); - - // has to be NOT DAEMON thread, since it must run before the app closes. - - // don't forget we have to shut down the ansi console as well - // alternatively, shut everything down when the JVM closes. - Thread shutdownThread = new Thread() { - @Override - public - void run() { - AnsiConsole.systemUninstall(); - - consoleProxyReader.shutdown0(); - } - }; - shutdownThread.setName("Console Input Shutdown"); - Runtime.getRuntime().addShutdownHook(shutdownThread); - } - - /** - * Permit our InputConsole to be initialized - */ - public static - void init() { - if (logger.isDebugEnabled()) { - logger.debug("Created Terminal: {} ({}w x {}h)", consoleProxyReader.terminal.getClass().getSimpleName(), - consoleProxyReader.terminal.getWidth(), consoleProxyReader.terminal.getHeight()); - } - } - - /** - * Gets the version number. - */ - public static - String getVersion() { - return "2.9"; - } - - /** - * return null if no data - */ - public static - String readLine() { - char[] line = consoleProxyReader.readLine0(); - return new String(line); - } - - private static InputStream wrappedInputStream = new InputStream() { - @Override - public - int read() throws IOException { - return consoleProxyReader.read0(); - } - - @Override - public - void close() throws IOException { - consoleProxyReader.release0(); - } - }; - - - /** - * return -1 if no data - */ - public static - int read() { - return consoleProxyReader.read0(); - } - - /** - * return null if no data - */ - public static - char[] readLinePassword() { - return consoleProxyReader.readLinePassword0(); - } - - public static - InputStream getInputStream() { - return wrappedInputStream; - } - - public static - void echo(boolean enableEcho) { - consoleProxyReader.echo0(enableEcho); - } - - public static - boolean echo() { - return consoleProxyReader.echo0(); - } - - private final Object inputLock = new Object(); - private final Object inputLockSingle = new Object(); - private final Object inputLockLine = new Object(); - - private final ObjectPool pool; - - private FastThreadLocal readBuff = new FastThreadLocal(); - private List readBuffers = new CopyOnWriteArrayList(); - private FastThreadLocal threadBufferCounter = new FastThreadLocal(); - - private FastThreadLocal readLineBuff = new FastThreadLocal(); - private List readLineBuffers = new CopyOnWriteArrayList(); - - private final Terminal terminal; - private final Boolean enableBackspace; - - private - InputConsole() { - Logger logger = InputConsole.logger; - - String readers = System.getProperty(TerminalType.READERS); - int readers2 = 32; - if (readers != null) { - try { - readers2 = Integer.parseInt(readers); - } catch (Exception e) { - e.printStackTrace(); - } - } - this.pool = ObjectPool.Blocking(new ByteBuffer2Poolable(), readers2); - - String type = System.getProperty(TerminalType.TYPE, TerminalType.AUTO).toLowerCase(); - if ("dumb".equals(System.getenv("TERM"))) { - type = TerminalType.NONE; - if (logger.isTraceEnabled()) { - logger.trace("System environment 'TERM'=dumb, creating type=" + type); - } - } - else { - if (logger.isTraceEnabled()) { - logger.trace("Creating terminal, type=" + type); - } - } - - Class t; - try { - if (type.equals(TerminalType.UNIX)) { - t = UnixTerminal.class; - } - else if (type.equals(TerminalType.WIN) || type.equals(TerminalType.WINDOWS)) { - t = WindowsTerminal.class; - } - else if (type.equals(TerminalType.NONE) || type.equals(TerminalType.OFF) || type.equals(TerminalType.FALSE)) { - t = UnsupportedTerminal.class; - } - else { - if (isIDEAutoDetect()) { - logger.debug("Terminal is in UNSUPPORTED (best guess). Unable to support single key input. Only line input available."); - t = UnsupportedTerminal.class; - } - else { - if (OS.isWindows()) { - t = WindowsTerminal.class; - } - else { - t = UnixTerminal.class; - } - } - } - } catch (Exception e) { - logger.error("Failed to construct terminal, falling back to unsupported.", e); - t = UnsupportedTerminal.class; - } - - Terminal terminal = null; - try { - terminal = t.newInstance(); - terminal.init(); - } catch (Throwable e) { - logger.error("Terminal initialization failed for {}, falling back to unsupported.", t.getSimpleName(), e); - t = UnsupportedTerminal.class; - - try { - terminal = t.newInstance(); - terminal.init(); - } catch (Exception e1) { - // UnsupportedTerminal can't do this - } - } - - if (terminal != null) { - terminal.setEchoEnabled(true); - } - - this.terminal = terminal; - this.enableBackspace = Boolean.parseBoolean(System.getProperty(TerminalType.ENABLE_BACKSPACE, "true")); - } - - // called when the JVM is shutting down. - private - void shutdown0() { - synchronized (this.inputLockSingle) { - this.inputLockSingle.notifyAll(); - } - - synchronized (this.inputLockLine) { - this.inputLockLine.notifyAll(); - } - - try { - InputConsole inputConsole = InputConsole.this; - - inputConsole.terminal.restore(); - // this will 'hang' our shutdown, and honestly, who cares? We're shutting down anyways. - // inputConsole.reader.close(); // hangs on shutdown - } catch (IOException ignored) { - ignored.printStackTrace(); - } - } - - private - void echo0(boolean enableEcho) { - this.terminal.setEchoEnabled(enableEcho); - } - - private - boolean echo0() { - return this.terminal.isEchoEnabled(); - } - - /** - * return -1 if no data or bunged-up - */ - private - int read0() { - Integer bufferCounter = this.threadBufferCounter.get(); - ByteBuffer2 buffer = this.readBuff.get(); - - if (buffer == null) { - bufferCounter = 0; - this.threadBufferCounter.set(bufferCounter); - - try { - buffer = this.pool.takeInterruptibly(); - buffer.clear(); - } catch (InterruptedException e) { - logger.error("Interrupted while receiving buffer from pool."); - buffer = pool.newInstance(); - } - - this.readBuff.set(buffer); - this.readBuffers.add(buffer); - } - - if (bufferCounter == buffer.position()) { - synchronized (this.inputLockSingle) { - buffer.setPosition(0); - this.threadBufferCounter.set(0); - - try { - this.inputLockSingle.wait(); - } catch (InterruptedException e) { - return -1; - } - } - } - - bufferCounter = this.threadBufferCounter.get(); - char c = buffer.readChar(bufferCounter); - bufferCounter += 2; - - this.threadBufferCounter.set(bufferCounter); - return c; - } - - /** - * return empty char[] if no data - */ - private - char[] readLinePassword0() { - // don't bother in an IDE. it won't work. - boolean echoEnabled = this.terminal.isEchoEnabled(); - this.terminal.setEchoEnabled(false); - char[] readLine0 = readLine0(); - this.terminal.setEchoEnabled(echoEnabled); - - return readLine0; - } - - /** - * return empty char[] if no data - */ - private - char[] readLine0() { - synchronized (this.inputLock) { - // empty here, because we don't want to register a readLine WHILE we are still processing - // the current line info. - - // the threadBufferForRead getting added is the part that is important - if (this.readLineBuff.get() == null) { - ByteBuffer2 buffer; - try { - buffer = this.pool.takeInterruptibly(); - } catch (InterruptedException e) { - logger.error("Interrupted while receiving buffer from pool."); - buffer = pool.newInstance(); - } - - this.readLineBuff.set(buffer); - this.readLineBuffers.add(buffer); - } - else { - this.readLineBuff.get().clear(); - } - } - - synchronized (this.inputLockLine) { - try { - this.inputLockLine.wait(); - } catch (InterruptedException e) { - return emptyLine; - } - } - - ByteBuffer2 buffer = this.readLineBuff.get(); - int len = buffer.position(); - if (len == 0) { - return emptyLine; - } - - buffer.rewind(); - char[] readChars = buffer.readChars(len / 2); // java always stores chars in 2 bytes - - // dump the chars in the buffer (safer for passwords, etc) - buffer.clearSecure(); - - this.readLineBuffers.remove(buffer); - this.pool.put(buffer); - this.readLineBuff.set(null); - - return readChars; - } - - /** - * releases any thread still waiting. - */ - private - void release0() { - synchronized (this.inputLockSingle) { - this.inputLockSingle.notifyAll(); - } - - synchronized (this.inputLockLine) { - this.inputLockLine.notifyAll(); - } - } - - private - void run() { - Logger logger2 = logger; - - final boolean ansiEnabled = Ansi.isEnabled(); - Ansi ansi = Ansi.ansi(); - PrintStream out = AnsiConsole.out; - - int typedChar; - char asChar; - - // don't type ; in a bash shell, it quits everything - // \n is replaced by \r in unix terminal? - while ((typedChar = this.terminal.read()) != -1) { - synchronized (this.inputLock) { - // don't let anyone add a new reader while we are still processing the current actions - asChar = (char) typedChar; - - if (logger2.isTraceEnabled()) { - logger2.trace("READ: {} ({})", asChar, typedChar); - } - - // notify everyone waiting for a character. - synchronized (this.inputLockSingle) { - // have to do readChar first (readLine has to deal with \b and \n - for (ByteBuffer2 buffer : this.readBuffers) { - buffer.writeChar(asChar); - } - - this.inputLockSingle.notifyAll(); - } - - // now to handle readLine stuff - - // if we type a backspace key, swallow it + previous in READLINE. READCHAR will have it passed. - if (this.enableBackspace && asChar == '\b') { - int position = 0; - - // clear ourself + one extra. - if (ansiEnabled) { - for (ByteBuffer2 buffer : this.readLineBuffers) { - // size of the buffer BEFORE our backspace was typed - int length = buffer.position(); - int amtToOverwrite = 2 * 2; // backspace is always 2 chars (^?) * 2 because it's bytes - - if (length > 1) { - char charAt = buffer.readChar(length - 2); - amtToOverwrite += getPrintableCharacters(charAt); - - // delete last item in our buffer - length -= 2; - buffer.setPosition(length); - - // now figure out where the cursor is really at. - // this is more memory friendly than buf.toString.length - for (int i = 0; i < length; i += 2) { - charAt = buffer.readChar(i); - position += getPrintableCharacters(charAt); - } - - position++; - } - - char[] overwrite = new char[amtToOverwrite]; - char c = ' '; - for (int i = 0; i < amtToOverwrite; i++) { - overwrite[i] = c; - } - - // move back however many, over write, then go back again - out.print(ansi.cursorToColumn(position)); - out.print(overwrite); - out.print(ansi.cursorToColumn(position)); - out.flush(); - } - } - } - else if (asChar == '\n') { - // ignoring \r, because \n is ALWAYS the last character in a new line sequence. (even for windows) - synchronized (this.inputLockLine) { - this.inputLockLine.notifyAll(); - } - } - else { - // only append if we are not a new line. - // our windows console PREVENTS us from returning '\r' (it truncates '\r\n', and returns just '\n') - for (ByteBuffer2 buffer : this.readLineBuffers) { - buffer.writeChar(asChar); - } - } - } - } - } - - /** - * try to guess if we are running inside an IDE - */ - private - boolean isIDEAutoDetect() { - try { - // Get the location of this class - ProtectionDomain pDomain = getClass().getProtectionDomain(); - CodeSource cSource = pDomain.getCodeSource(); - URL loc = cSource.getLocation(); // file:/X:/workspace/xxxx/classes/ when it's in eclipse - - // if we are in eclipse, this won't be a jar -- it will be a class directory. - File locFile = new File(loc.getFile()); - return locFile.isDirectory(); - - } catch (Exception ignored) { - } - - // fall-back to unsupported - return true; - } - - - /** - * Return the number of characters that will be printed when the specified character is echoed to the screen - *

- * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. - */ - private static - int getPrintableCharacters(final int ch) { - // StringBuilder sbuff = new StringBuilder(); - - if (ch >= 32) { - if (ch < 127) { - // sbuff.append((char) ch); - return 1; - } - else if (ch == 127) { - // sbuff.append('^'); - // sbuff.append('?'); - return 2; - } - else { - // sbuff.append('M'); - // sbuff.append('-'); - int count = 2; - - if (ch >= 128 + 32) { - if (ch < 128 + 127) { - // sbuff.append((char) (ch - 128)); - count++; - } - else { - // sbuff.append('^'); - // sbuff.append('?'); - count += 2; - } - } - else { - // sbuff.append('^'); - // sbuff.append((char) (ch - 128 + 64)); - count += 2; - } - return count; - } - } - else { - // sbuff.append('^'); - // sbuff.append((char) (ch + 64)); - return 2; - } - - // return sbuff; - } -} - diff --git a/src/dorkbox/inputConsole/posix/InputStreamReader.java b/src/dorkbox/inputConsole/posix/InputStreamReader.java deleted file mode 100644 index 3980d85..0000000 --- a/src/dorkbox/inputConsole/posix/InputStreamReader.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 dorkbox.inputConsole.posix; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.MalformedInputException; -import java.nio.charset.UnmappableCharacterException; - - -/** - * NOTE for JLine: the default InputStreamReader that comes from the JRE usually read more bytes than needed from the input stream, which is - * not usable in a character per character model used in the console. We thus use the harmony code which only reads the minimal number of - * bytes, with a modification to ensure we can read larger characters (UTF-16 has up to 4 bytes, and UTF-32, rare as it is, may have up to - * 8). - * - * - * A class for turning a byte stream into a character stream. Data read from the source input stream is converted into characters by either - * a default or a provided character converter. The default encoding is taken from the "file.encoding" system property. - * {@code InputStreamReader} contains a buffer of bytes read from the source stream and converts these into characters as needed. The buffer - * size is 8K. - * - * @see OutputStreamWriter - */ -@SuppressWarnings("ALL") -class InputStreamReader extends Reader { - - private InputStream in; - - private static final int BUFFER_SIZE = 8192; - - private boolean endOfInput = false; - - String encoding; - - CharsetDecoder decoder; - - ByteBuffer bytes = ByteBuffer.allocate(BUFFER_SIZE); - - /** - * Constructs a new {@code InputStreamReader} on the {@link InputStream} {@code in}. This constructor sets the character converter to - * the encoding specified in the "file.encoding" property and falls back to ISO 8859_1 (ISO-Latin-1) if the property doesn't exist. - * - * @param in the input stream from which to read characters. - */ - public InputStreamReader(InputStream in) { - super(in); - this.in = in; - // FIXME: This should probably use Configuration.getFileEncoding() - this.encoding = System.getProperty("file.encoding", "ISO8859_1"); //$NON-NLS-1$//$NON-NLS-2$ - this.decoder = - Charset.forName(this.encoding).newDecoder().onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - this.bytes.limit(0); - } - - /** - * Constructs a new InputStreamReader on the InputStream {@code in}. The character converter that is used to decode bytes into - * characters is identified by name by {@code enc}. If the encoding cannot be found, an UnsupportedEncodingException error is thrown. - * - * @param in the InputStream from which to read characters. - * @param enc identifies the character converter to use. - * @throws NullPointerException if {@code enc} is {@code null}. - * @throws UnsupportedEncodingException if the encoding specified by {@code enc} cannot be found. - */ - public InputStreamReader(InputStream in, final String enc) - // throws UnsupportedEncodingException - { - super(in); - - if (enc == null) { - throw new NullPointerException(); - } - this.in = in; - - try { - this.decoder = - Charset.forName(enc).newDecoder().onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - // throw (UnsupportedEncodingException) - // new UnsupportedEncodingException(enc).initCause(e); - } - this.bytes.limit(0); - } - - /** - * Constructs a new InputStreamReader on the InputStream {@code in} and CharsetDecoder {@code dec}. - * - * @param in the source InputStream from which to read characters. - * @param dec the CharsetDecoder used by the character conversion. - */ - public InputStreamReader(InputStream in, CharsetDecoder dec) { - super(in); - dec.averageCharsPerByte(); - this.in = in; - this.decoder = dec; - this.bytes.limit(0); - } - - /** - * Constructs a new InputStreamReader on the InputStream {@code in} and Charset {@code charset}. - * - * @param in the source InputStream from which to read characters. - * @param charset the Charset that defines the character converter - */ - public InputStreamReader(InputStream in, Charset charset) { - super(in); - this.in = in; - this.decoder = charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE); - this.bytes.limit(0); - } - - /** - * Closes this reader. This implementation closes the source InputStream and releases all local storage. - * - * @throws IOException if an error occurs attempting to close this reader. - */ - @Override - public void close() throws IOException { - synchronized (this.lock) { - this.decoder = null; - if (this.in != null) { - this.in.close(); - this.in = null; - } - } - } - - /** - * Returns the name of the encoding used to convert bytes into characters. The value {@code null} is returned if this reader has been - * closed. - * - * @return the name of the character converter or {@code null} if this reader is closed. - */ - public String getEncoding() { - if (!isOpen()) { - return null; - } - return this.encoding; - } - - /** - * Reads a single character from this reader and returns it as an integer with the two higher-order bytes set to 0. Returns -1 if the - * end of the reader has been reached. The byte value is either obtained from converting bytes in this reader's buffer or by first - * filling the buffer from the source InputStream and then reading from the buffer. - * - * @return the character read or -1 if the end of the reader has been reached. - * @throws IOException if this reader is closed or some other I/O error occurs. - */ - @Override - public int read() throws IOException { - synchronized (this.lock) { - if (!isOpen()) { - throw new IOException("InputStreamReader is closed."); - } - - char buf[] = new char[4]; - return read(buf, 0, 4) != -1 ? Character.codePointAt(buf, 0) : -1; - } - } - - /** - * Reads at most {@code length} characters from this reader and stores them at position {@code offset} in the character array - * {@code buf}. Returns the number of characters actually read or -1 if the end of the reader has been reached. The bytes are either - * obtained from converting bytes in this reader's buffer or by first filling the buffer from the source InputStream and then reading - * from the buffer. - * - * @param buf the array to store the characters read. - * @param offset the initial position in {@code buf} to store the characters read from this reader. - * @param length the maximum number of characters to read. - * @return the number of characters read or -1 if the end of the reader has been reached. - * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code length < 0}, or if {@code offset + length} is greater than the - * length of {@code buf}. - * @throws IOException if this reader is closed or some other I/O error occurs. - */ - @Override - public int read(char[] buf, int offset, int length) throws IOException { - synchronized (this.lock) { - if (!isOpen()) { - throw new IOException("InputStreamReader is closed."); - } - if (offset < 0 || offset > buf.length - length || length < 0) { - throw new IndexOutOfBoundsException(); - } - if (length == 0) { - return 0; - } - - CharBuffer out = CharBuffer.wrap(buf, offset, length); - CoderResult result = CoderResult.UNDERFLOW; - - // bytes.remaining() indicates number of bytes in buffer - // when 1-st time entered, it'll be equal to zero - boolean needInput = !this.bytes.hasRemaining(); - - while (out.hasRemaining()) { - // fill the buffer if needed - if (needInput) { - try { - if (this.in.available() == 0 && out.position() > offset) { - // we could return the result without blocking read - break; - } - } catch (IOException e) { - // available didn't work so just try the read - } - - int to_read = this.bytes.capacity() - this.bytes.limit(); - int off = this.bytes.arrayOffset() + this.bytes.limit(); - int was_red = this.in.read(this.bytes.array(), off, to_read); - - if (was_red == -1) { - this.endOfInput = true; - break; - } else if (was_red == 0) { - break; - } - this.bytes.limit(this.bytes.limit() + was_red); - needInput = false; - } - - // decode bytes - result = this.decoder.decode(this.bytes, out, false); - - if (result.isUnderflow()) { - // compact the buffer if no space left - if (this.bytes.limit() == this.bytes.capacity()) { - this.bytes.compact(); - this.bytes.limit(this.bytes.position()); - this.bytes.position(0); - } - needInput = true; - } else { - break; - } - } - - if (result == CoderResult.UNDERFLOW && this.endOfInput) { - result = this.decoder.decode(this.bytes, out, true); - this.decoder.flush(out); - this.decoder.reset(); - } - if (result.isMalformed()) { - throw new MalformedInputException(result.length()); - } else if (result.isUnmappable()) { - throw new UnmappableCharacterException(result.length()); - } - - return out.position() - offset == 0 ? -1 : out.position() - offset; - } - } - - /* - * Answer a boolean indicating whether or not this InputStreamReader is open. - */ - private boolean isOpen() { - return this.in != null; - } - - /** - * Indicates whether this reader is ready to be read without blocking. If the result is {@code true}, the next {@code read()} will not - * block. If the result is {@code false} then this reader may or may not block when {@code read()} is called. This implementation - * returns {@code true} if there are bytes available in the buffer or the source stream has bytes available. - * - * @return {@code true} if the receiver will not block when {@code read()} is called, {@code false} if unknown or blocking will occur. - * @throws IOException if this reader is closed or some other I/O error occurs. - */ - @Override - public boolean ready() throws IOException { - synchronized (this.lock) { - if (this.in == null) { - throw new IOException("InputStreamReader is closed."); - } - try { - return this.bytes.hasRemaining() || this.in.available() > 0; - } catch (IOException e) { - return false; - } - } - } -} diff --git a/src/dorkbox/inputConsole/posix/PosixTerminalControl.java b/src/dorkbox/inputConsole/posix/PosixTerminalControl.java deleted file mode 100644 index 54b9bc0..0000000 --- a/src/dorkbox/inputConsole/posix/PosixTerminalControl.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * 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 dorkbox.inputConsole.posix; - -import com.sun.jna.Library; - -import java.nio.ByteBuffer; - -@SuppressWarnings("ALL") -interface PosixTerminalControl extends Library { - - public static final int TCSANOW = 0; - public static final int TBUFLEN = 124; - - // Definitions at: http://linux.die.net/man/3/termios - // also: http://code.metager.de/source/xref/DragonFly-BSD/sys/sys/termios.h - public static final int IGNBRK = 0x00000001; /* ignore BREAK condition */ - public static final int BRKINT = 0x00000002; /* map BREAK to SIGINTR */ - - - public static final int ISIG = 0000001; - public static final int ICANON = 0000002; - public static final int ECHO = 0000010; - public static final int IXON = 0002000; - - - public static final int VINTR = 0; - public static final int VQUIT = 1; - public static final int VERASE = 2; - public static final int VKILL = 3; - public static final int VEOF = 4; - public static final int VTIME = 5; - public static final int VMIN = 6; - public static final int VSWTC = 7; - public static final int VSTART = 8; - public static final int VSTOP = 9; - public static final int VSUSP = 10; - public static final int VEOL = 11; - public static final int VREPRINT = 12; - public static final int VDISCARD = 13; - public static final int VWERASE = 14; - public static final int VLNEXT = 15; - public static final int VEOL2 = 16; - - // MAGIC! - public static final int TIOCGWINSZ = System.getProperty("os.name").equalsIgnoreCase("linux") ? 0x5413 : 1074295912; - - public int open(String path, int flags); - - public int close(int fd); - - /** - * Original signature : int ioctl(int, int, char*)
- */ - public int ioctl(int d, int request, ByteBuffer data); - - /** - * Put the state of FD into *TERMIOS_P.
- * - * Original signature : int tcgetattr(int, char*)
- */ - public int tcgetattr(int fd, TermiosStruct termios_p); - - /** - * Set the state of FD to *TERMIOS_P.
- * - * Values for OPTIONAL_ACTIONS (TCSA*) are in .
- * - * Original signature : int tcsetattr(int, int, char*)
- */ - public int tcsetattr(int fd, int optional_actions, TermiosStruct termios_p); -} diff --git a/src/dorkbox/inputConsole/posix/TermiosStruct.java b/src/dorkbox/inputConsole/posix/TermiosStruct.java deleted file mode 100644 index 670ff4c..0000000 --- a/src/dorkbox/inputConsole/posix/TermiosStruct.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * 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 dorkbox.inputConsole.posix; - -import com.sun.jna.Structure; - -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings("ALL") -public -class TermiosStruct extends Structure { - /** - * input mode flags - */ - public int c_iflag; - /** - * output mode flags - */ - public int c_oflag; - /** - * control mode flags - */ - public int c_cflag; - /** - * local mode flags - */ - public int c_lflag; - /** - * line discipline - */ - public byte c_line; - - /** - * control characters - */ - public byte[] c_cc = new byte[32]; - - /** - * input speed - */ - public int c_ispeed; - /** - * output speed - */ - public int c_ospeed; - - public TermiosStruct() {} - - @Override - protected List getFieldOrder() { - return Arrays.asList( - "c_iflag", - "c_oflag", - "c_cflag", - "c_lflag", - "c_line", - "c_cc", - "c_ispeed", - "c_ospeed"); - } -} diff --git a/src/dorkbox/inputConsole/posix/UnixTerminal.java b/src/dorkbox/inputConsole/posix/UnixTerminal.java deleted file mode 100644 index 6ad9039..0000000 --- a/src/dorkbox/inputConsole/posix/UnixTerminal.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2010 dorkbox, llc - * - * 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 dorkbox.inputConsole.posix; - -import java.io.IOException; -import java.io.Reader; -import java.nio.ByteBuffer; - -import com.sun.jna.Native; - -import dorkbox.inputConsole.Encoding; -import dorkbox.inputConsole.Terminal; - -/** - * Terminal that is used for unix platforms. Terminal initialization is handled via JNA and ioctl/tcgetattr/tcsetattr/cfmakeraw. - * - * This implementation should work for an reasonable POSIX system. - */ -public class UnixTerminal extends Terminal { - - private volatile TermiosStruct termInfoDefault = new TermiosStruct(); - private volatile TermiosStruct termInfo = new TermiosStruct(); - - private final Reader reader; - - private final PosixTerminalControl term; - private ByteBuffer windowSizeBuffer = ByteBuffer.allocate(8); - - - public UnixTerminal() throws Exception { - String encoding = Encoding.get(); - this.reader = new InputStreamReader(System.in, encoding); - - this.term = (PosixTerminalControl) Native.loadLibrary("c", PosixTerminalControl.class); - - // save off the defaults - if (this.term.tcgetattr(0, this.termInfoDefault) != 0) { - throw new IOException("Failed to get terminal info"); - } - } - - @Override - public void init() throws IOException { - - // COMPARABLE TO (from upstream) - //settings.set("-icanon min 1 -ixon"); - //settings.set("dsusp undef"); - - /* - * NOT done in constructor, since our unit test DOES NOT use this! - * - * Set the console to be character-buffered instead of line-buffered. - * Allow ctrl-s and ctrl-q keypress to be used (as forward search) - */ - -// raw mode -// t->c_iflag &= ~(IMAXBEL|IXOFF|INPCK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IGNPAR); -// t->c_iflag |= IGNBRK; -// t->c_oflag &= ~OPOST; -// t->c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|ICANON|ISIG|IEXTEN|NOFLSH|TOSTOP|PENDIN); -// t->c_cflag &= ~(CSIZE|PARENB); -// t->c_cflag |= CS8|CREAD; -// t->c_cc[VMIN] = 1; -// t->c_cc[VTIME] = 0; - - if (this.term.tcgetattr(0, this.termInfo) != 0) { - throw new IOException("Failed to get terminal info"); - } - - this.termInfo.c_iflag &= ~PosixTerminalControl.IXON; // DISABLE - flow control mediated by ^S and ^Q - // struct.c_iflag |= PosixTerminalControl.IUTF8; // DISABLE - flow control mediated by ^S and ^Q - - this.termInfo.c_lflag &= ~PosixTerminalControl.ICANON; // DISABLE - canonical mode (pass chars straight through to terminal) - // struct.c_lflag &= ~PosixTerminalControl.ISIG; // DISABLE - When any of the characters INTR, QUIT, SUSP, or DSUSP are received, - // generate the corresponding signal. - - // If MIN > 0 and TIME = 0, MIN sets the number of characters to receive before the read is satisfied. As TIME is zero, the timer is - // not used. - this.termInfo.c_cc[PosixTerminalControl.VMIN] = 1; // Minimum number of characters for noncanonical read (MIN). - this.termInfo.c_cc[PosixTerminalControl.VTIME] = 0; // Timeout in deciseconds for noncanonical read (TIME). - - this.termInfo.c_cc[PosixTerminalControl.VSUSP] = 0; // suspend disabled - this.termInfo.c_cc[PosixTerminalControl.VEOF] = 0; // eof disabled - this.termInfo.c_cc[PosixTerminalControl.VEOL] = 0; // eol disabled - - if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { - throw new IOException("Can not set terminal flags"); - } - } - - /** - * Restore the original terminal configuration, which can be used when shutting down the console reader. The ConsoleReader cannot be - * used after calling this method. - */ - @Override - public final void restore() throws IOException { - if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfoDefault) != 0) { - throw new IOException("Can not reset terminal to defaults"); - } - } - - /** - * Returns number of columns in the terminal. - */ - @Override - public final int getWidth() { - if (this.term.ioctl(0, PosixTerminalControl.TIOCGWINSZ, this.windowSizeBuffer) != 0) { - return DEFAULT_WIDTH; - } - - return (short) (0x000000FF & this.windowSizeBuffer.get(2) + (0x000000FF & this.windowSizeBuffer.get(3)) * 256); - } - - /** - * Returns number of rows in the terminal. - */ - @Override - public final int getHeight() { - if (this.term.ioctl(0, PosixTerminalControl.TIOCGWINSZ, this.windowSizeBuffer) != 0) { - return DEFAULT_HEIGHT; - } - - return (short) (0x000000FF & this.windowSizeBuffer.get(0) + (0x000000FF & this.windowSizeBuffer.get(1)) * 256); - } - - @Override - public final synchronized void setEchoEnabled(final boolean enabled) { - // have to reget them, since flags change everything - if (this.term.tcgetattr(0, this.termInfo) != 0) { - this.logger.error("Failed to get terminal info"); - } - - if (enabled) { - this.termInfo.c_lflag |= PosixTerminalControl.ECHO; // ENABLE Echo input characters. - } else { - this.termInfo.c_lflag &= ~PosixTerminalControl.ECHO; // DISABLE Echo input characters. - } - - if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { - this.logger.error("Can not set terminal flags"); - } - - super.setEchoEnabled(enabled); - } - - // public final void disableInterruptCharacter() { - // // have to re-get them, since flags change everything - // if (this.term.tcgetattr(0, this.termInfo) !=0) { - // this.logger.error("Failed to get terminal info"); - // } - // - // this.termInfo.c_cc[PosixTerminalControl.VINTR] = 0; // interrupt disabled - // - // if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { - // this.logger.error("Can not set terminal flags"); - // } - // } - // - // public final void enableInterruptCharacter() { - // // have to re-get them, since flags change everything - // if (this.term.tcgetattr(0, this.termInfo) !=0) { - // this.logger.error("Failed to get terminal info"); - // } - // - // this.termInfo.c_cc[PosixTerminalControl.VINTR] = 3; // interrupt is ctrl-c - // - // if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { - // this.logger.error("Can not set terminal flags"); - // } - // } - - @Override - public final int read() { - try { - return this.reader.read(); - } catch (IOException ignored) { - return -1; - } - } -} diff --git a/src/dorkbox/inputConsole/windows/WindowsTerminal.java b/src/dorkbox/inputConsole/windows/WindowsTerminal.java deleted file mode 100644 index 047868d..0000000 --- a/src/dorkbox/inputConsole/windows/WindowsTerminal.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2002-2012, the original author or authors. - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * http://www.opensource.org/licenses/bsd-license.php - * - * @author Marc Prud'hommeaux - * @author Jason Dillon - */ -package dorkbox.inputConsole.windows; - -import java.io.IOException; -import java.io.PrintStream; - -import dorkbox.inputConsole.Terminal; -import org.fusesource.jansi.internal.Kernel32.INPUT_RECORD; -import org.fusesource.jansi.internal.Kernel32.KEY_EVENT_RECORD; -import org.fusesource.jansi.internal.WindowsSupport; - -/** - * Terminal implementation for Microsoft Windows. Terminal initialization in {@link #init} is accomplished by calling the Win32 APIs SetConsoleMode and GetConsoleMode to disable - * character echoing. - *

- * - * @since 2.0 (customized) - */ -public class WindowsTerminal extends Terminal { - - private volatile int originalMode; - private final PrintStream out; - - public WindowsTerminal() { - this.out = System.out; - } - - @Override - public final void init() throws IOException { - this.originalMode = WindowsSupport.getConsoleMode(); - - // Must set these four modes at the same time to make it work fine. - WindowsSupport.setConsoleMode(this.originalMode | - ConsoleMode.ENABLE_LINE_INPUT.code | - ConsoleMode.ENABLE_ECHO_INPUT.code | - ConsoleMode.ENABLE_PROCESSED_INPUT.code | - ConsoleMode.ENABLE_WINDOW_INPUT.code); - } - - /** - * Restore the original terminal configuration, which can be used when shutting down the console reader. The ConsoleReader cannot be - * used after calling this method. - */ - @Override - public final void restore() throws IOException { - // restore the old console mode - WindowsSupport.setConsoleMode(this.originalMode); - } - - @Override - public final int getWidth() { - int w = WindowsSupport.getWindowsTerminalWidth(); - return w < 1 ? DEFAULT_WIDTH : w; - } - - @Override - public final int getHeight() { - int h = WindowsSupport.getWindowsTerminalHeight(); - return h < 1 ? DEFAULT_HEIGHT : h; - } - - @Override - public final int read() { - int input = readInput(); - - if (isEchoEnabled()) { - char asChar = (char) input; - if (asChar == '\n') { - this.out.println(); - } else { - this.out.print(asChar); - } - // have to flush, otherwise we'll never see the chars on screen - this.out.flush(); - } - - return input; - } - - private int readInput() { - // this HOOKS the input event, and prevents it from going to the console "proper" - try { - INPUT_RECORD[] events; - while (true) { - // we ALWAYS read until we have an event we care about! - events = WindowsSupport.readConsoleInput(1); - - if (events != null) { - for (int i = 0; i < events.length; i++) { - KEY_EVENT_RECORD keyEvent = events[i].keyEvent; - // Log.trace(keyEvent.keyDown? "KEY_DOWN" : "KEY_UP", "key code:", keyEvent.keyCode, "char:", (long)keyEvent.uchar); - if (keyEvent.keyDown) { - if (keyEvent.uchar > 0) { - char uchar = keyEvent.uchar; - if (uchar == '\r') { - // we purposefully swallow input after \r, and substitute it with \n - return '\n'; - } - - return uchar; - } - } - } - } - } - } catch (IOException e) { - this.logger.error("Windows console input error: ", e); - } - - return -1; - } -} diff --git a/test/com/dorkbox/console/AnsiConsoleExample.java b/test/com/dorkbox/console/AnsiConsoleExample.java new file mode 100644 index 0000000..5f69781 --- /dev/null +++ b/test/com/dorkbox/console/AnsiConsoleExample.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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 com.dorkbox.console; + +import java.io.FileInputStream; +import java.io.IOException; + +import dorkbox.console.output.Ansi; + +/** + * + * @author Hiram Chirino + */ +public class AnsiConsoleExample { + + private AnsiConsoleExample() {} + + public static void main(String[] args) throws IOException { + String file = "jansi.ans"; + if( args.length>0 ) + file = args[0]; + + // Allows us to disable ANSI processing. + if( "true".equals(System.getProperty("jansi", "true")) ) { + Ansi.systemInstall(); + } + + FileInputStream f = new FileInputStream(file); + int c; + while( (c=f.read())>=0 ) { + System.out.write(c); + } + f.close(); + } + +} diff --git a/test/com/dorkbox/console/AnsiConsoleExample2.java b/test/com/dorkbox/console/AnsiConsoleExample2.java new file mode 100644 index 0000000..e00f698 --- /dev/null +++ b/test/com/dorkbox/console/AnsiConsoleExample2.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2009, Progress Software Corporation and/or its + * subsidiaries or affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a asValue 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 com.dorkbox.console; + +import static dorkbox.console.output.Ansi.ansi; + +import java.io.FileInputStream; +import java.io.IOException; + +import dorkbox.console.output.Ansi; + +/** + * + * @author Hiram Chirino + */ +public class AnsiConsoleExample2 { + + private AnsiConsoleExample2() {} + + public static void main(String[] args) throws IOException { + String file = "jansi.ans"; + if( args.length>0 ) + file = args[0]; + + // Allows us to disable ANSI processing. + if( "true".equals(System.getProperty("jansi", "true")) ) { + Ansi.systemInstall(); + } + + System.out.print(ansi().reset().eraseScreen().cursor(1, 1)); + System.out.print("======================================================================="); + FileInputStream f = new FileInputStream(file); + int c; + while( (c=f.read())>=0 ) { + System.out.write(c); + } + f.close(); + System.out.println("======================================================================="); + } + +} diff --git a/test/com/dorkbox/console/AnsiRenderWriterTest.java b/test/com/dorkbox/console/AnsiRenderWriterTest.java new file mode 100644 index 0000000..a85bd9a --- /dev/null +++ b/test/com/dorkbox/console/AnsiRenderWriterTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009 the original author or authors. + * + * 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 com.dorkbox.console; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import dorkbox.console.output.AnsiRenderWriter; + +/** + * Tests for the {@link AnsiRenderWriter} class. + * + * @author Jason Dillon + */ +public class AnsiRenderWriterTest +{ + private ByteArrayOutputStream baos; + + private AnsiRenderWriter out; + + @Before + public void setUp() { + baos = new ByteArrayOutputStream(); + out = new AnsiRenderWriter(baos); + } + + @After + public void tearDown() { + out = null; + baos = null; + } + + @Test + public void testRenderNothing() { + out.print("foo"); + out.flush(); + + String result = new String(baos.toByteArray()); + assertEquals("foo", result); + } +} diff --git a/test/com/dorkbox/console/AnsiRendererTest.java b/test/com/dorkbox/console/AnsiRendererTest.java new file mode 100644 index 0000000..68a0042 --- /dev/null +++ b/test/com/dorkbox/console/AnsiRendererTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2009 the original author or authors. + * + * 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 com.dorkbox.console; + +import static dorkbox.console.output.AnsiRenderer.render; +import static java.awt.Font.BOLD; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import dorkbox.console.output.Ansi; +import dorkbox.console.output.AnsiRenderer; +import dorkbox.console.output.Color; + +/** + * Tests for the {@link AnsiRenderer} class. + * + * @author Jason Dillon + */ +public class AnsiRendererTest +{ + public static + boolean test(final String text) { + return text != null && text.contains(AnsiRenderer.BEGIN_TOKEN); + } + + @Before + public void setUp() { + Ansi.setEnabled(true); + } + + @Test + public void testTest() throws Exception { + assertFalse(test("foo")); + assertTrue(test("@|foo|")); + assertTrue(test("@|foo")); + } + + @Test + public void testRender() { + String str = render("@|bold foo|@"); + System.out.println(str); + assertEquals(Ansi.ansi().a(BOLD).a("foo").reset().toString(), str); + assertEquals(Ansi.ansi().bold().a("foo").reset().toString(), str); + } + + @Test + public void testRender2() { + String str = render("@|bold,red foo|@"); + System.out.println(str); + assertEquals(Ansi.ansi().a(BOLD).fg(Color.RED).a("foo").reset().toString(), str); + assertEquals(Ansi.ansi().bold().fgRed().a("foo").reset().toString(), str); + } + + @Test + public void testRender3() { + String str = render("@|bold,red foo bar baz|@"); + System.out.println(str); + assertEquals(Ansi.ansi().a(BOLD).fg(Color.RED).a("foo bar baz").reset().toString(), str); + assertEquals(Ansi.ansi().bold().fgRed().a("foo bar baz").reset().toString(), str); + } + + @Test + public void testRender4() { + String str = render("@|bold,red foo bar baz|@ ick @|bold,red foo bar baz|@"); + System.out.println(str); + assertEquals(Ansi.ansi() + .a(BOLD).fg(Color.RED).a("foo bar baz").reset() + .a(" ick ") + .a(BOLD).fg(Color.RED).a("foo bar baz").reset() + .toString(), str); + } + + @Test + public void testRender5() { + // Check the ansi() render method. + String str = Ansi.ansi().render("@|bold Hello|@").toString(); + System.out.println(str); + assertEquals(Ansi.ansi().a(BOLD).a("Hello").reset().toString(), str); + } + + + @Test + public void testRenderNothing() { + assertEquals("foo", render("foo")); + } + + @Test + public void testRenderInvalidMissingEnd() { + String str = render("@|bold foo"); + assertEquals("@|bold foo", str); + } + + @Test + public void testRenderInvalidMissingText() { + String str = render("@|bold|@"); + assertEquals("@|bold|@", str); + } +} diff --git a/test/com/dorkbox/console/AnsiRendererTestExample.java b/test/com/dorkbox/console/AnsiRendererTestExample.java new file mode 100644 index 0000000..c6c730e --- /dev/null +++ b/test/com/dorkbox/console/AnsiRendererTestExample.java @@ -0,0 +1,172 @@ +package com.dorkbox.console; + +import static dorkbox.console.output.Ansi.ansi; + +import java.io.IOException; + +import dorkbox.console.Console; +import dorkbox.console.output.Ansi; +import dorkbox.console.output.Erase; + +/** + * System output for visual confirmation of ANSI codes. + *

+ * Must enable assertions to verify no errors!! (ie: java -ea -jar blah.jar) + */ +public +class AnsiRendererTestExample { + public static + void main(String[] args) throws IOException { + Ansi.systemInstall(); + Ansi.setEnabled(true); + + System.out.println(ansi() + .fgBlack().a("black").bgBlack().a("black") + .reset() + .fgBrightBlack().a("b-black").bgBrightBlack().a("b-black") + .reset()); + + System.out.println(ansi() + .fgBlue().a("blue").bgBlue().a("blue") + .reset() + .fgBrightBlue().a("b-blue").bgBrightBlue().a("b-blue") + .reset()); + + System.out.println(ansi() + .fgCyan().a("cyan").bgCyan().a("cyan") + .reset() + .fgBrightCyan().a("b-cyan").bgBrightCyan().a("b-cyan") + .reset()); + + System.out.println(ansi() + .fgGreen().a("green").bgGreen().a("green") + .reset() + .fgBrightGreen().a("b-green").bgBrightGreen().a("b-green") + .reset()); + + System.out.println(ansi() + .fgMagenta().a("magenta").bgMagenta().a("magenta") + .reset() + .fgBrightMagenta().a("b-magenta").bgBrightMagenta().a("b-magenta") + .reset()); + + System.out.println(ansi() + .fgRed().a("red").bgRed().a("red") + .reset() + .fgBrightRed().a("b-red").bgBrightRed().a("b-red") + .reset()); + + System.out.println(ansi() + .fgYellow().a("yellow").bgYellow().a("yellow") + .reset() + .fgBrightYellow().a("b-yellow").bgBrightYellow().a("b-yellow") + .reset()); + + System.out.println(ansi() + .fgWhite().a("white").bgWhite().a("white") + .reset() + .fgBrightWhite().a("b-white").bgBrightWhite().a("b-white") + .reset()); + + + System.out.println(ansi()); // reset the ansi stream. Can ALSO have ansi().reset(), but that would be redundant + + System.out.println("The following line should be blank except for the first '>'"); + System.out.println(ansi() + .a(">THIS SHOULD BE BLANK") + .cursorToColumn(2) + .eraseLine()); + + System.out.println("The following line should be blank"); + System.out.println(ansi() + .a(">THIS SHOULD BE BLANK") + .eraseLine(Erase.ALL)); + + System.out.println(ansi() + .a(">THIS SHOULD BE BLANK") + .eraseLine(Erase.BACKWARD) + .a("Everything before this should be blank")); + + System.out.println(ansi() + .a("Everything after this should be blank") + .saveCursorPosition() + .a(">THIS SHOULD BE BLANK") + .restoreCursorPosition() + .eraseLine(Erase.FORWARD)); + + System.out.println("00000000000000000000000000"); + System.out.println("00000000000000000000000000"); + System.out.println("00000000000000000000000000"); + System.out.println("00000000000000000000000000"); + System.out.println("00000000000000000000000000"); + + System.out.println(ansi() + .a("Should have two blank spots in the above 0's") + .saveCursorPosition() + .cursorUp(4) + .cursorLeft(30) + .a(" ") + .cursorDownLine() + .cursorRight(5) + .a(" ") + .restoreCursorPosition()); + + + // verify the output renderer +// String str = render("@|bold foo|@foo"); +// System.out.println(str); +// assertEquals(ansi().a(BOLD).a("foo").reset().a("foo").toString(), str); +// assertEquals(ansi().bold().a("foo").reset().a("foo").toString(), str); +// +// +// str = render("@|bold,red foo|@"); +// System.out.println(str); +// assertEquals(ansi().a(BOLD).fg(RED).a("foo").reset().toString(), str); +// assertEquals(ansi().bold().fgRed().a("foo").reset().toString(), str); +// +// str = render("@|bold,red foo bar baz|@"); +// System.out.println(str); +// assertEquals(ansi().a(BOLD).fg(RED).a("foo bar baz").reset().toString(), str); +// assertEquals(ansi().bold().fgRed().a("foo bar baz").reset().toString(), str); +// +// +// str = render("@|bold,red foo bar baz|@ ick @|bold,red foo bar baz|@"); +// System.out.println(str); +// String expected = ansi().a(BOLD) +// .fg(RED) +// .a("foo bar baz") +// .reset() +// .a(" ick ") +// .a(BOLD) +// .fg(RED) +// .a("foo bar baz") +// .reset() +// .toString(); +// +// assertEquals(expected, str); +// +// +// str = render("@|bold foo"); // shouldn't work +// System.err.println(str + " <- shouldn't work"); +// +// str = render("@|bold|@"); // shouldn't work +// System.err.println(str + " <- shouldn't work"); +// +// Ansi.systemUninstall(); +// +// str = render("@|bold foo|@foo"); +// System.out.println(str + " <- shouldn't work"); + + + System.err.println("ver : " + Console.getVersion()); + + + System.out.println("Now testing the input console. '?' to quit"); + Console.ENABLE_ECHO = true; + + int read; + while ((read = Console.in.read()) != '?') { +// System.err.println("READ :" + read + " (" + (char) read + ")"); + } + } +} diff --git a/test/com/dorkbox/console/AnsiStringTest.java b/test/com/dorkbox/console/AnsiStringTest.java new file mode 100644 index 0000000..8398991 --- /dev/null +++ b/test/com/dorkbox/console/AnsiStringTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 the original author or authors. + * + * 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 com.dorkbox.console; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import dorkbox.console.output.Ansi; +import dorkbox.console.output.AnsiString; +import dorkbox.console.output.Attribute; + +/** + * Tests for {@link AnsiString}. + * + * @author Jason Dillon + */ +public class AnsiStringTest +{ + @Test + public void testNotEncoded() throws Exception { + AnsiString as = new AnsiString("foo"); + assertEquals("foo", as.getEncoded()); + assertEquals("foo", as.getPlain()); + assertEquals(3, as.length()); + } + + @Test + public void testEncoded() throws Exception { + AnsiString as = new AnsiString(Ansi.ansi().a(Attribute.BOLD).a("foo").reset().toString()); + assertEquals("foo", as.getPlain()); + assertEquals(3, as.length()); + } +} diff --git a/test/com/dorkbox/console/AnsiTest.java b/test/com/dorkbox/console/AnsiTest.java new file mode 100644 index 0000000..35155be --- /dev/null +++ b/test/com/dorkbox/console/AnsiTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009 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 com.dorkbox.console; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import dorkbox.console.output.Ansi; +import dorkbox.console.output.Color; + +/** + * Tests for the {@link Ansi} class. + * + * @author Jason Dillon + */ +public class AnsiTest +{ + @Test + public void testSetEnabled() throws Exception { + Ansi.setEnabled(false); + new Thread() + { + @Override + public void run() { + assertEquals(false, Ansi.isEnabled()); + } + }.run(); + + Ansi.setEnabled(true); + new Thread() + { + @Override + public void run() { + assertEquals(true, Ansi.isEnabled()); + } + }.run(); + } + + @Test + public void testClone() throws CloneNotSupportedException { + Ansi ansi = Ansi.ansi().a("Some text").bg(Color.BLACK).fg(Color.WHITE); + Ansi clone = new Ansi(ansi); + + assertEquals(ansi.a("test").reset().toString(), clone.a("test").reset().toString()); + } +} diff --git a/test/com/dorkbox/console/HtmlAnsiOutputStreamTest.java b/test/com/dorkbox/console/HtmlAnsiOutputStreamTest.java new file mode 100644 index 0000000..01bc573 --- /dev/null +++ b/test/com/dorkbox/console/HtmlAnsiOutputStreamTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2009 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 com.dorkbox.console; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Test; + +import dorkbox.console.output.HtmlAnsiOutputStream; + +/** + * @author Daniel Doubrovkine + */ +public class HtmlAnsiOutputStreamTest { + + @Test + public void testNoMarkup() throws IOException { + assertEquals("line", colorize("line")); + } + + @Test + public void testClear() throws IOException { + assertEquals("", colorize("")); + assertEquals("hello world", colorize("hello world")); + } + + @Test + public void testBold() throws IOException { + assertEquals("hello world", colorize("hello world")); + } + + @Test + public void testGreen() throws IOException { + assertEquals("hello world", + colorize("hello world")); + } + + @Test + public void testGreenOnWhite() throws IOException { + assertEquals("hello world", + colorize("hello world")); + } + + @Test + public void testEscapeHtml() throws IOException { + assertEquals(""", colorize("\"")); + assertEquals("&", colorize("&")); + assertEquals("<", colorize("<")); + assertEquals(">", colorize(">")); + assertEquals(""&<>", colorize("\"&<>")); + } + + @Test + public void testResetOnOpen() throws IOException { + assertEquals("red", + colorize("red")); + } + + @Test + public void testUTF8Character() throws IOException { + assertEquals("\u3053\u3093\u306b\u3061\u306f", + colorize("\u3053\u3093\u306b\u3061\u306f")); + } + + private String colorize(String text) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + HtmlAnsiOutputStream hos = new HtmlAnsiOutputStream(os); + hos.write(text.getBytes("UTF-8")); + hos.close(); + return new String(os.toByteArray(), "UTF-8"); + } +} diff --git a/test/com/dorkbox/console/jansi.ans b/test/com/dorkbox/console/jansi.ans new file mode 100644 index 0000000..d9d3c52 --- /dev/null +++ b/test/com/dorkbox/console/jansi.ans @@ -0,0 +1,8 @@ +[?7h +ÚÄÄ¿ÚÄÄÄÄÄ¿ ÚÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÂÄÄ¿ +³ÛÛÃÙÛÛÛÛÛÀÂÙÛÛÛÛÛÀÂÙÛÛÛÛÛÛ³Þݳ +ÚÄÄ¿ ³ÛÛ³ÛÛÜÜÜÛÛ³ÛÛÚÄ¿ÛÛ³ÛÛÜÜÜÜ ³Üܳ +³±±ÀÄÙ±Û³±ÛÚÄ¿±Û³±Û³ ³±Û³ ßßßß±Û³±Û³ +À¿²²²²²Ú´²²³ ³²²³²²³ ³²²³ß²²²²²ß³²²³ +ÀÄÄÄÄÄÙÀÄÄÙ ÀÄÄÁÄÄÙ ÀÄÄÁÄÄÄÄÄÄÄÁÄÄÙ +