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 extends Terminal> 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
+ *
+ * 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
+ *
+ * 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