Fixed Unsupported console in IDE to more closely emulate single
character input. Cleaned up terminal class heirarchy
This commit is contained in:
parent
d4e211974d
commit
e412d8de12
@ -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);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
124
src/dorkbox/console/input/Input.java
Normal file
124
src/dorkbox/console/input/Input.java
Normal 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() {
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
293
src/dorkbox/console/input/SupportedTerminal.java
Normal file
293
src/dorkbox/console/input/SupportedTerminal.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
else {
|
||||
return this.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) {
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user