Fixed Unsupported console in IDE to more closely emulate single

character input. Cleaned up terminal class heirarchy
This commit is contained in:
nathan 2016-05-31 01:07:57 +02:00
parent d4e211974d
commit e412d8de12
9 changed files with 784 additions and 741 deletions

View File

@ -15,32 +15,28 @@
*/
package dorkbox.console;
import static dorkbox.console.Input.readLinePassword;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import dorkbox.console.input.Input;
import dorkbox.console.input.Terminal;
import dorkbox.console.output.Ansi;
import dorkbox.console.output.AnsiOutputStream;
import dorkbox.console.output.WindowsAnsiOutputStream;
import dorkbox.console.util.posix.CLibraryPosix;
import dorkbox.console.util.windows.Kernel32;
import dorkbox.util.Property;
/**
* Provides a fluent API for generating ANSI escape sequences and providing access to streams that support it.
* <p>
* See: https://en.wikipedia.org/wiki/ANSI_escape_code
* Provides access to single character input streams and ANSI capable output streams.
*
* @author dorkbox, llc
*/
@SuppressWarnings("unused")
public
class Console {
/**
* If true, allows an ANSI output stream to be created, otherwise a NO-OP stream is created instead
* If true, allows an ANSI output stream to be created on System.out/err, If true, allows an ANSI output stream to be created on
* System.out/err, otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences.
*/
@Property
public static boolean ENABLE_ANSI = true;
@ -53,13 +49,14 @@ class Console {
public static boolean FORCE_ENABLE_ANSI = false;
/**
* Enables or disables character echo to stdout in the console, should call {@link #setEchoEnabled(boolean)} after initialization
* Enables or disables character echo to stdout in the console, should call {@link Terminal#setEchoEnabled(boolean)} after
* initialization
*/
@Property
public static volatile boolean ENABLE_ECHO = true;
/**
* Enables or disables CTRL-C behavior in the console, should call {@link #setInterruptEnabled(boolean)} after initialization
* Enables or disables CTRL-C behavior in the console, should call {@link Terminal#setInterruptEnabled(boolean)} after initialization
*/
@Property
public static volatile boolean ENABLE_INTERRUPT = false;
@ -73,6 +70,7 @@ class Console {
/**
* Used to determine what console to use/hook when AUTO is not correctly working.
* Valid options are:
* AUTO - automatically determine which OS/console type to use
* UNIX - try to control a UNIX console
@ -83,16 +81,6 @@ class Console {
public static final String INPUT_CONSOLE_TYPE = "AUTO";
private static final PrintStream original_out = System.out;
private static final PrintStream original_err = System.err;
// protected by synchronize
private static int installed = 0;
private static PrintStream out;
private static PrintStream err;
/**
* Gets the version number.
*/
@ -101,108 +89,49 @@ class Console {
return "2.9";
}
/**
* Reads single character input from the console.
* If the standard in supports single character input, then a terminal will be returned that supports it, otherwise a buffered (aka
* 'normal') input will be returned
*
* @return -1 if no data or problems
* @return a terminal that supports single character input or the default buffered input
*/
public static
int read() {
return Input.read();
Terminal in() {
return Input.terminal;
}
/**
* Reads a line of characters from the console, defined as everything before the 'ENTER' key is pressed
* If the standard in supports single character input, then an InputStream will be returned that supports it, otherwise a buffered (aka
* 'normal') InputStream will be returned
*
* @return null if no data
* @return an InputStream that supports single character input or the default buffered input
*/
public static
String readLine() {
return Input.readLine();
InputStream inputStream() {
return Input.wrappedInputStream;
}
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
* If the standard out natively supports ANSI escape codes, then this just returns System.out (wrapped to reset ANSI stream on close),
* otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences.
*
* @return empty char[] if no data
* @return a PrintStream which is ANSI aware.
*/
public static
char[] readLineChars() {
return Input.readLineChars();
PrintStream out() {
return Ansi.out;
}
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
* If the standard out natively supports ANSI escape codes, then this just returns System.err (wrapped to reset ANSI stream on close),
* otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences.
*
* @return empty char[] if no data
* @return a PrintStream which is ANSI aware.
*/
public static
char[] readPassword() {
return readLinePassword();
}
/**
* Reads an InputStream capable of reading a single character at a time
*/
public static
InputStream getInputStream() {
return Input.getInputStream();
}
/**
* Enables or disables CTRL-C behavior in the console
*/
public static
void setInterruptEnabled(final boolean enabled) {
Console.ENABLE_INTERRUPT = enabled;
Input.setInterruptEnabled(enabled);
}
/**
* Enables or disables character echo to stdout
*/
public static
void setEchoEnabled(final boolean enabled) {
Console.ENABLE_ECHO = enabled;
Input.setEchoEnabled(enabled);
}
/**
* Override System.err and System.out with an ANSI capable {@link java.io.PrintStream}.
*/
public static synchronized
void systemInstall() {
installed++;
if (installed == 1) {
out = out();
err = err();
System.setOut(out);
System.setErr(err);
}
}
/**
* un-does a previous {@link #systemInstall()}.
* <p>
* 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 (out != null && out != original_out) {
out.close();
System.setOut(original_out);
}
if (err != null && err != original_err) {
err.close();
System.setErr(original_err);
}
}
PrintStream err() {
return Ansi.err;
}
/**
@ -211,115 +140,11 @@ class Console {
*/
public static synchronized
void reset() {
if (installed >= 1) {
try {
System.out.write(AnsiOutputStream.RESET_CODE);
} catch (IOException e) {
e.printStackTrace();
}
try {
Ansi.out.write(AnsiOutputStream.RESET_CODE);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 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() {
if (out == null) {
out = createPrintStream(original_out, 1); // STDOUT_FILENO
}
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() {
if (err == null) {
err = createPrintStream(original_err, 2); // STDERR_FILENO
}
return err;
}
private static boolean isXterm() {
String term = System.getenv("TERM");
return "xterm".equalsIgnoreCase(term);
}
private static
PrintStream createPrintStream(final OutputStream stream, int fileno) {
if (!ENABLE_ANSI) {
// Use the ANSIOutputStream to strip out the ANSI escape sequences.
return new PrintStream(new AnsiOutputStream(stream));
}
if (!isXterm()) {
String os = System.getProperty("os.name");
if (os.startsWith("Windows")) {
// 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 deafultPrintStream(stream);
}
// On windows we know the console does not interpret ANSI codes..
try {
return new PrintStream(new WindowsAnsiOutputStream(stream, fileno));
} catch (Throwable ignore) {
// 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.
if (!FORCE_ENABLE_ANSI) {
return new PrintStream(new AnsiOutputStream(stream));
}
} else {
// We must be on some unix variant..
try {
// If we can detect that stdout is not a tty.. then setup to strip the ANSI sequences..
if (!FORCE_ENABLE_ANSI && CLibraryPosix.isatty(fileno) == 0) {
return new PrintStream(new AnsiOutputStream(stream));
}
} catch (Throwable ignore) {
// These errors happen if the JNI lib is not available for your platform.
}
}
}
// By default we assume the terminal can handle ANSI codes.
// Just wrap it up so that when we get closed, we reset the attributes.
return deafultPrintStream(stream);
}
private static
PrintStream deafultPrintStream(final OutputStream stream) {
return new PrintStream(new FilterOutputStream(stream) {
@Override
public
void close() throws IOException {
write(AnsiOutputStream.RESET_CODE);
flush();
super.close();
}
});
}
}

View File

@ -1,470 +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.console;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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.console.util.CharHolder;
import dorkbox.objectPool.ObjectPool;
import dorkbox.objectPool.PoolableObject;
import dorkbox.util.OS;
import dorkbox.util.bytes.ByteBuffer2;
import dorkbox.util.bytes.ByteBuffer2Poolable;
class Input {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Input.class);
private static final char[] emptyLine = new char[0];
private static final List<CharHolder> charInputBuffers = new ArrayList<CharHolder>();
private final static ObjectPool<CharHolder> charInputPool = ObjectPool.NonBlocking(new PoolableObject<CharHolder>() {
@Override
public
CharHolder create() {
return new CharHolder();
}
@Override
public
void onReturn(final CharHolder object) {
// dump the chars in the buffer (safer for passwords, etc)
object.character = (char) 0;
charInputBuffers.remove(object);
}
@Override
public
void onTake(final CharHolder object) {
charInputBuffers.add(object);
}
});
private static final List<ByteBuffer2> lineInputBuffers = new ArrayList<ByteBuffer2>();
private final static ObjectPool<ByteBuffer2> lineInputPool = ObjectPool.NonBlocking(new ByteBuffer2Poolable() {
@Override
public
void onReturn(final ByteBuffer2 object) {
// dump the chars in the buffer (safer for passwords, etc)
object.clearSecure();
lineInputBuffers.remove(object);
}
@Override
public
void onTake(final ByteBuffer2 object) {
lineInputBuffers.add(object);
}
});
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();
static {
String type = Console.INPUT_CONSOLE_TYPE.toUpperCase(Locale.ENGLISH);
Throwable didFallbackE = null;
Terminal term;
try {
if (type.equals("UNIX")) {
term = new PosixTerminal();
}
else if (type.equals("WINDOWS")) {
term = new WindowsTerminal();
}
else if (type.equals("NONE")) {
term = new UnsupportedTerminal();
}
else {
// if these cannot be created, because we are in an IDE, an error will be thrown
if (OS.isWindows()) {
term = new WindowsTerminal();
}
else {
term = new PosixTerminal();
}
}
} catch (Exception e) {
didFallbackE = e;
term = new UnsupportedTerminal();
}
terminal = term;
if (logger.isDebugEnabled()) {
logger.debug("Created Terminal: {} ({}w x {}h)", Input.terminal.getClass().getSimpleName(),
Input.terminal.getWidth(), Input.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_INTERRUPT);
Thread consoleThread = new Thread(new Runnable() {
@Override
public
void run() {
Input.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);
}
static
void setInterruptEnabled(final boolean enabled) {
terminal.setInterruptEnabled(enabled);
}
static
void setEchoEnabled(final boolean enabled) {
terminal.setEchoEnabled(enabled);
}
private static InputStream wrappedInputStream = new InputStream() {
@Override
public
int read() throws IOException {
return Input.read();
}
@Override
public
void close() throws IOException {
Input.release0();
}
};
static
InputStream getInputStream() {
return wrappedInputStream;
}
private
Input() {
}
/**
* Reads single character input from the console.
*
* @return -1 if no data or problems
*/
static
int read() {
CharHolder holder;
synchronized (inputLock) {
// don't want to register a read() WHILE we are still processing the current input.
// also adds it to the global list of char inputs
holder = charInputPool.take();
}
synchronized (inputLockSingle) {
try {
inputLockSingle.wait();
} catch (InterruptedException e) {
return -1;
}
}
char c = holder.character;
// also clears and removes from the global list of char inputs
charInputPool.put(holder);
return c;
}
/**
* @return empty char[] if no data or problems
*/
static
char[] readLineChars() {
ByteBuffer2 buffer;
synchronized (inputLock) {
// don't want to register a readLine() WHILE we are still processing the current line info.
// also adds it to the global list of line inputs
buffer = lineInputPool.take();
}
synchronized (inputLockLine) {
try {
inputLockLine.wait();
} catch (InterruptedException e) {
return emptyLine;
}
}
int len = buffer.position();
if (len == 0) {
return emptyLine;
}
buffer.rewind();
char[] readChars = buffer.readChars(len / 2); // java always stores chars in 2 bytes
// also clears and removes from the global list of line inputs
lineInputPool.put(buffer);
return readChars;
}
/**
* Reads line input from the console
*
* @return empty char[] if no data
*/
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;
}
/**
* Reads a single line of characters, defined as everything before the 'ENTER' key is pressed
* @return null if no data
*/
static
String readLine() {
char[] line = Input.readLineChars();
if (line == null) {
return null;
}
return new String(line);
}
/**
* releases any thread still waiting.
*/
private static
void release0() {
synchronized (inputLockSingle) {
inputLockSingle.notifyAll();
}
synchronized (inputLockLine) {
inputLockLine.notifyAll();
}
}
private static
void run() {
final Logger logger2 = logger;
final PrintStream out = System.out;
final char overWriteChar = ' ';
Ansi ansi = null;
int typedChar;
char asChar;
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 (CharHolder holder : charInputBuffers) {
holder.character = asChar; // copy by value
}
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 (Console.ENABLE_BACKSPACE && asChar == '\b') {
int position = 0;
char[] overwrite = null;
// clear ourself + one extra.
for (ByteBuffer2 buffer : lineInputBuffers) {
// 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 (Console.ENABLE_ANSI && overwrite != null) {
if (ansi == null) {
ansi = Ansi.ansi();
}
// 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, which we changed)
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 : lineInputBuffers) {
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
* <p/>
* 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;
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.io.InputStream;
import java.util.Locale;
import dorkbox.console.Console;
import dorkbox.util.OS;
public
class Input {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Console.class);
public final static Terminal terminal;
static {
String type = Console.INPUT_CONSOLE_TYPE.toUpperCase(Locale.ENGLISH);
Throwable didFallbackE = null;
Terminal term;
try {
if (type.equals("UNIX")) {
term = new PosixTerminal();
}
else if (type.equals("WINDOWS")) {
term = new WindowsTerminal();
}
else if (type.equals("NONE")) {
term = new UnsupportedTerminal();
}
else {
// if these cannot be created, because we are in an IDE, an error will be thrown
if (OS.isWindows()) {
term = new WindowsTerminal();
}
else {
term = new PosixTerminal();
}
}
} catch (Exception e) {
didFallbackE = e;
term = new UnsupportedTerminal();
}
terminal = term;
boolean debugEnabled = logger.isDebugEnabled();
if (didFallbackE != null && !didFallbackE.getMessage().equals(Terminal.CONSOLE_ERROR_INIT)) {
logger.error("Failed to construct terminal, falling back to unsupported.", didFallbackE);
} else if (debugEnabled && term instanceof UnsupportedTerminal) {
logger.debug("Terminal is UNSUPPORTED (best guess). Unable to support single key input. Only line input available.");
} else if (debugEnabled) {
logger.debug("Created Terminal: {} ({}w x {}h)", Input.terminal.getClass().getSimpleName(),
Input.terminal.getWidth(), Input.terminal.getHeight());
}
if (term instanceof SupportedTerminal) {
// echo and backspace
term.setEchoEnabled(Console.ENABLE_ECHO);
term.setInterruptEnabled(Console.ENABLE_INTERRUPT);
Thread consoleThread = new Thread((SupportedTerminal)term);
consoleThread.setDaemon(true);
consoleThread.setName("Console Input Reader");
consoleThread.start();
// has to be NOT DAEMON thread, since it must run before the app closes.
// alternatively, shut everything down when the JVM closes.
Thread shutdownThread = new Thread() {
@Override
public
void run() {
// called when the JVM is shutting down.
terminal.close();
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);
}
}
public final static InputStream wrappedInputStream = new InputStream() {
@Override
public
int read() throws IOException {
return terminal.read();
}
@Override
public
void close() throws IOException {
terminal.close();
}
};
private
Input() {
}
}

View File

@ -29,7 +29,7 @@ import dorkbox.console.util.posix.Termios;
* This implementation should work for an reasonable POSIX system.
*/
public
class PosixTerminal extends Terminal {
class PosixTerminal extends SupportedTerminal {
private final Termios original = new Termios();
private Termios termInfo = new Termios();
@ -112,14 +112,8 @@ class PosixTerminal extends Terminal {
}
@Override
public final
int read() {
CLibraryPosix.read(0, inputRef, 1);
return inputRef.getValue();
}
public
void setEchoEnabled(final boolean enabled) {
protected
void doSetEchoEnabled(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");
@ -137,8 +131,9 @@ class PosixTerminal extends Terminal {
}
}
public
void setInterruptEnabled(final boolean enabled) {
@Override
protected
void doSetInterruptEnabled(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");
@ -155,4 +150,11 @@ class PosixTerminal extends Terminal {
this.logger.error("Can not set terminal flags");
}
}
@Override
protected final
int doRead() {
CLibraryPosix.read(0, inputRef, 1);
return inputRef.getValue();
}
}

View File

@ -0,0 +1,293 @@
/*
* 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.PrintStream;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import dorkbox.console.Console;
import dorkbox.console.output.Ansi;
import dorkbox.console.util.CharHolder;
import dorkbox.util.FastThreadLocal;
import dorkbox.util.bytes.ByteBuffer2;
public abstract
class SupportedTerminal extends Terminal implements Runnable {
private final PrintStream out = System.out;
private static final char[] emptyLine = new char[0];
protected final Object inputLockLine = new Object();
protected final Object inputLockSingle = new Object();
protected final List<CharHolder> charInputBuffers = new ArrayList<CharHolder>();
protected final FastThreadLocal<CharHolder> charInput = new FastThreadLocal<CharHolder>() {
@Override
public
CharHolder initialValue() {
return new CharHolder();
}
};
private final List<ByteBuffer2> lineInputBuffers = new ArrayList<ByteBuffer2>();
private final FastThreadLocal<ByteBuffer2> lineInput = new FastThreadLocal<ByteBuffer2>() {
@Override
public
ByteBuffer2 initialValue() {
return new ByteBuffer2(8, -1);
}
};
public
SupportedTerminal() {
}
/**
* Reads single character input from the console.
*
* @return -1 if no data or problems
*/
public final
int read() {
CharHolder holder = charInput.get();
synchronized (inputLockSingle) {
// don't want to register a read() WHILE we are still processing the current input.
// also adds it to the global list of char inputs
charInputBuffers.add(holder);
try {
inputLockSingle.wait();
} catch (InterruptedException e) {
return -1;
}
char c = holder.character;
// also clears and removes from the global list of char inputs
charInputBuffers.remove(holder);
return c;
}
}
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
*
* @return empty char[] if no data
*/
public final
char[] readLineChars() {
ByteBuffer2 buffer = lineInput.get();
synchronized (inputLockLine) {
// don't want to register a readLine() WHILE we are still processing the current line info.
// also adds it to the global list of line inputs
lineInputBuffers.add(buffer);
try {
inputLockLine.wait();
} catch (InterruptedException e) {
return emptyLine;
}
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();
// also clears and removes from the global list of line inputs
lineInputBuffers.remove(buffer);
return readChars;
}
}
/**
* releases any thread still waiting.
*/
public final
void close() {
synchronized (inputLockSingle) {
inputLockSingle.notifyAll();
}
synchronized (inputLockLine) {
inputLockLine.notifyAll();
}
}
/**
* Reads a single character from whatever underlying stream is available.
*/
protected abstract int doRead();
public
void run() {
final Logger logger2 = logger;
final char overWriteChar = ' ';
Ansi ansi = null;
int typedChar;
char asChar;
while ((typedChar = doRead()) != -1) {
// 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 (CharHolder holder : charInputBuffers) {
holder.character = asChar; // copy by value
}
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 (Console.ENABLE_BACKSPACE && asChar == '\b') {
int position = 0;
char[] overwrite = null;
// clear ourself + one extra.
for (ByteBuffer2 buffer : lineInputBuffers) {
// 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 (Console.ENABLE_ANSI && overwrite != null) {
if (ansi == null) {
ansi = Ansi.ansi();
}
// 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, which we changed)
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 : lineInputBuffers) {
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
* <p/>
* 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;
}
}

View File

@ -17,18 +17,27 @@ package dorkbox.console.input;
import java.io.IOException;
import dorkbox.console.Console;
@SuppressWarnings("unused")
public abstract
class Terminal {
public static final String CONSOLE_ERROR_INIT = "Unable to initialize the input console.";
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());
static final String CONSOLE_ERROR_INIT = "Unable to initialize the input console.";
static final int DEFAULT_WIDTH = 80;
static final int DEFAULT_HEIGHT = 24;
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
protected
Terminal() {
}
abstract
void doSetInterruptEnabled(final boolean enabled);
protected abstract
void doSetEchoEnabled(final boolean enabled);
public abstract
void restore() throws IOException;
@ -38,16 +47,72 @@ class Terminal {
public abstract
int getHeight();
// NOT THREAD SAFE
public abstract
void setEchoEnabled(final boolean enabled);
public abstract
void setInterruptEnabled(final boolean enabled);
/**
* Enables or disables CTRL-C behavior in the console
*/
public final
void setInterruptEnabled(final boolean enabled) {
Console.ENABLE_INTERRUPT = enabled;
doSetInterruptEnabled(enabled);
}
/**
* @return a character from whatever underlying input method the terminal has available.
* Enables or disables character echo to stdout
*/
public final
void setEchoEnabled(final boolean enabled) {
Console.ENABLE_ECHO = enabled;
doSetEchoEnabled(enabled);
}
/**
* Reads single character input from the console.
*
* @return -1 if no data or problems
*/
public abstract
int read();
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
*
* @return empty char[] if no data
*/
public abstract
char[] readLineChars();
/**
* Reads a single line of characters, defined as everything before the 'ENTER' key is pressed
* @return null if no data
*/
public
String readLine() {
char[] line = readLineChars();
if (line == null) {
return null;
}
return new String(line);
}
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
*
* @return empty char[] if no data
*/
public
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;
}
/**
* releases any thread still waiting.
*/
public abstract
void close();
}

View File

@ -18,19 +18,49 @@ package dorkbox.console.input;
import java.io.IOException;
import java.io.InputStream;
import dorkbox.util.FastThreadLocal;
import dorkbox.util.bytes.ByteBuffer2;
@SuppressWarnings("Duplicates")
public
class UnsupportedTerminal extends Terminal {
private final ByteBuffer2 buffer = new ByteBuffer2(8, -1);
private final InputStream in = System.in;
private int readerCount = -1;
private static final char[] newLine;
static {
newLine = new char[1];
newLine[0] = '\n';
}
private final FastThreadLocal<ByteBuffer2> buffer = new FastThreadLocal<ByteBuffer2>() {
@Override
public
ByteBuffer2 initialValue() {
return new ByteBuffer2(8, -1);
}
};
private final FastThreadLocal<Integer> readCount = new FastThreadLocal<Integer>() {
@Override
public
Integer initialValue() {
return 0;
}
};
public
UnsupportedTerminal() {
}
@Override
protected
void doSetInterruptEnabled(final boolean enabled) {
}
@Override
protected
void doSetEchoEnabled(final boolean enabled) {
}
@Override
public final
void restore() {
@ -48,52 +78,99 @@ class UnsupportedTerminal extends Terminal {
return 0;
}
@Override
/**
* Reads single character input from the console.
*
* @return -1 if no data or problems
*/
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)
if (this.readerCount == -1) {
// so, 'readCount' is REALLY the index at which we return letters (until the whole string is returned)
ByteBuffer2 buffer = this.buffer.get();
if (this.readCount.get() == 0) {
// we have to wait for more data.
try {
InputStream sysIn = this.in;
InputStream sysIn = System.in;
int read;
char asChar;
this.buffer.clearSecure();
buffer.clearSecure();
while ((read = sysIn.read()) != -1) {
asChar = (char) read;
if (asChar == '\n') {
this.readerCount = this.buffer.position();
this.buffer.rewind();
int position = buffer.position();
this.readCount.set(position);
if (position == 0) {
// only send a NEW LINE if it was the ONLY thing pressed (this is to MOST ACCURATELY simulate single char input
return '\n';
}
buffer.rewind();
break;
}
else {
this.buffer.writeChar(asChar);
buffer.writeChar(asChar);
}
}
} catch (IOException ignored) {
}
}
// EACH thread will have it's own count!
if (this.readerCount == this.buffer.position()) {
this.readerCount = -1;
return '\n';
// // EACH thread will have it's own count!
// if (this.readCount == buffer.position()) {
// this.readCount = -1;
// return '\n';
// }
// else {
// return buffer.readChar();
// }
readCount.set(this.readCount.get() - 2); // 2 bytes per char in the stream
return buffer.readChar();
}
/**
* Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed
*
* @return empty char[] if no data
*/
public
char[] readLineChars() {
ByteBuffer2 buffer = this.buffer.get();
buffer.clearSecure();
int position = 0;
// we have to wait for more data.
try {
final InputStream sysIn = System.in;
int read;
char asChar;
while ((read = sysIn.read()) != -1) {
asChar = (char) read;
if (asChar == '\n') {
buffer.rewind();
break;
}
buffer.writeChar(asChar);
position = buffer.position();
}
} catch (IOException ignored) {
}
else {
return this.buffer.readChar();
if (position == 0) {
// only send a NEW LINE if it was the ONLY thing pressed (this is to MOST ACCURATELY simulate single char input
return newLine;
}
char[] chars = buffer.readChars(position/2); // 2 bytes per char
buffer.clearSecure();
return chars;
}
@Override
public
void close() {
}
}

View File

@ -35,7 +35,7 @@ import dorkbox.console.util.windows.Kernel32;
* Terminal implementation for Microsoft Windows.
*/
public
class WindowsTerminal extends Terminal {
class WindowsTerminal extends SupportedTerminal {
// Console mode constants copied <tt>wincon.h</tt>.
// There are OTHER options, however they DO NOT work with unbuffered input or we just don't care about them.
@ -113,15 +113,15 @@ class WindowsTerminal extends Terminal {
}
@Override
public
void setEchoEnabled(final boolean enabled) {
protected
void doSetEchoEnabled(final boolean enabled) {
// only way to do this, console modes DO NOT work
echoEnabled = enabled;
}
@Override
public
void setInterruptEnabled(final boolean enabled) {
protected
void doSetInterruptEnabled(final boolean enabled) {
IntByReference mode = new IntByReference();
GetConsoleMode(console, mode);
@ -138,8 +138,8 @@ class WindowsTerminal extends Terminal {
}
@Override
public final
int read() {
protected final
int doRead() {
int input = readInput();
if (echoEnabled) {

View File

@ -32,9 +32,16 @@ package dorkbox.console.output;
import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_RESET;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import dorkbox.console.Console;
import dorkbox.console.util.posix.CLibraryPosix;
import dorkbox.console.util.windows.Kernel32;
import dorkbox.util.OS;
/**
* Provides a fluent API for generating ANSI escape sequences and providing access to streams that support it.
@ -44,12 +51,36 @@ import dorkbox.console.Console;
* @author dorkbox, llc
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
*/
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "WeakerAccess"})
public
class Ansi {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Console.class);
private static final PrintStream original_out = System.out;
private static final PrintStream original_err = System.err;
public static final PrintStream out = createPrintStream(original_out, 1); // STDOUT_FILENO;
public static final PrintStream err = createPrintStream(original_err, 2); // STDERR_FILENO
static {
// make SURE that our console in/out/err are correctly setup BEFORE accessing methods in this class
Console.getVersion();
System.setOut(out);
System.setErr(err);
// don't forget we have to shut down the ansi console as well
Thread shutdownThread = new Thread() {
@Override
public
void run() {
// called when the JVM is shutting down.
restoreSystemStreams();
}
};
shutdownThread.setName("Console ANSI stream Shutdown");
Runtime.getRuntime().addShutdownHook(shutdownThread);
}
private static final String NEW_LINE = System.getProperty("line.separator");
@ -57,6 +88,17 @@ class Ansi {
private final StringBuilder builder;
private final ArrayList<Integer> attributeOptions = new ArrayList<Integer>(8);
/**
* Restores System.err/out PrintStreams to their ORIGINAL configuration. Useful when using ANSI functionality but do not want to
* hook into the system.
*/
public static
void restoreSystemStreams() {
System.setOut(original_out);
System.setErr(original_err);
}
/**
* Creates a new Ansi object
*/
@ -872,4 +914,89 @@ class Ansi {
builder.append(command);
return this;
}
private static boolean isXterm() {
String term = System.getenv("TERM");
return "xterm".equalsIgnoreCase(term);
}
private static
PrintStream createPrintStream(final OutputStream stream, final int fileno) {
String type = fileno == 1 ? "OUT" : "ERR";
if (!Console.ENABLE_ANSI) {
// Use the ANSIOutputStream to strip out the ANSI escape sequences.
return getStripPrintStream(stream, type);
}
if (!isXterm()) {
if (OS.isWindows()) {
// 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 defaultPrintStream(stream, type);
}
// On windows we know the console does not interpret ANSI codes..
try {
PrintStream printStream = new PrintStream(new WindowsAnsiOutputStream(stream, fileno));
if (logger.isDebugEnabled()) {
logger.debug("Created a Windows ANSI PrintStream for {}", type);
}
return printStream;
} catch (Throwable ignore) {
// 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.
if (!Console.FORCE_ENABLE_ANSI) {
return getStripPrintStream(stream, type);
}
} else {
// We must be on some unix variant..
try {
// If we can detect that stdout is not a tty.. then setup to strip the ANSI sequences..
if (!Console.FORCE_ENABLE_ANSI && CLibraryPosix.isatty(fileno) == 0) {
return getStripPrintStream(stream, type);
}
} catch (Throwable ignore) {
// These errors happen if the JNI lib is not available for your platform.
}
}
}
// By default we assume the terminal can handle ANSI codes.
// Just wrap it up so that when we get closed, we reset the attributes.
return defaultPrintStream(stream, type);
}
private static
PrintStream getStripPrintStream(final OutputStream stream, final String type) {
if (logger.isDebugEnabled()) {
logger.debug("Created a strip-ANSI PrintStream for {}", type);
}
return new PrintStream(new AnsiOutputStream(stream));
}
private static
PrintStream defaultPrintStream(final OutputStream stream, final String type) {
if (logger.isDebugEnabled()) {
logger.debug("Created ANSI PrintStream for {}", type);
}
return new PrintStream(new FilterOutputStream(stream) {
@Override
public
void close() throws IOException {
write(AnsiOutputStream.RESET_CODE);
flush();
super.close();
}
});
}
}