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;
|
package dorkbox.console;
|
||||||
|
|
||||||
import static dorkbox.console.Input.readLinePassword;
|
|
||||||
|
|
||||||
import java.io.FilterOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintStream;
|
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.AnsiOutputStream;
|
||||||
import dorkbox.console.output.WindowsAnsiOutputStream;
|
|
||||||
import dorkbox.console.util.posix.CLibraryPosix;
|
|
||||||
import dorkbox.console.util.windows.Kernel32;
|
|
||||||
import dorkbox.util.Property;
|
import dorkbox.util.Property;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a fluent API for generating ANSI escape sequences and providing access to streams that support it.
|
* Provides access to single character input streams and ANSI capable output streams.
|
||||||
* <p>
|
|
||||||
* See: https://en.wikipedia.org/wiki/ANSI_escape_code
|
|
||||||
*
|
*
|
||||||
* @author dorkbox, llc
|
* @author dorkbox, llc
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public
|
public
|
||||||
class Console {
|
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
|
@Property
|
||||||
public static boolean ENABLE_ANSI = true;
|
public static boolean ENABLE_ANSI = true;
|
||||||
@ -53,13 +49,14 @@ class Console {
|
|||||||
public static boolean FORCE_ENABLE_ANSI = false;
|
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
|
@Property
|
||||||
public static volatile boolean ENABLE_ECHO = true;
|
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
|
@Property
|
||||||
public static volatile boolean ENABLE_INTERRUPT = false;
|
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:
|
* Valid options are:
|
||||||
* AUTO - automatically determine which OS/console type to use
|
* AUTO - automatically determine which OS/console type to use
|
||||||
* UNIX - try to control a UNIX console
|
* UNIX - try to control a UNIX console
|
||||||
@ -83,16 +81,6 @@ class Console {
|
|||||||
public static final String INPUT_CONSOLE_TYPE = "AUTO";
|
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.
|
* Gets the version number.
|
||||||
*/
|
*/
|
||||||
@ -101,108 +89,49 @@ class Console {
|
|||||||
return "2.9";
|
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
|
public static
|
||||||
int read() {
|
Terminal in() {
|
||||||
return Input.read();
|
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
|
public static
|
||||||
String readLine() {
|
InputStream inputStream() {
|
||||||
return Input.readLine();
|
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
|
public static
|
||||||
char[] readLineChars() {
|
PrintStream out() {
|
||||||
return Input.readLineChars();
|
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
|
public static
|
||||||
char[] readPassword() {
|
PrintStream err() {
|
||||||
return readLinePassword();
|
return Ansi.err;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,115 +140,11 @@ class Console {
|
|||||||
*/
|
*/
|
||||||
public static synchronized
|
public static synchronized
|
||||||
void reset() {
|
void reset() {
|
||||||
if (installed >= 1) {
|
|
||||||
try {
|
try {
|
||||||
System.out.write(AnsiOutputStream.RESET_CODE);
|
Ansi.out.write(AnsiOutputStream.RESET_CODE);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
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.
|
* This implementation should work for an reasonable POSIX system.
|
||||||
*/
|
*/
|
||||||
public
|
public
|
||||||
class PosixTerminal extends Terminal {
|
class PosixTerminal extends SupportedTerminal {
|
||||||
|
|
||||||
private final Termios original = new Termios();
|
private final Termios original = new Termios();
|
||||||
private Termios termInfo = new Termios();
|
private Termios termInfo = new Termios();
|
||||||
@ -112,14 +112,8 @@ class PosixTerminal extends Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final
|
protected
|
||||||
int read() {
|
void doSetEchoEnabled(final boolean enabled) {
|
||||||
CLibraryPosix.read(0, inputRef, 1);
|
|
||||||
return inputRef.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
void setEchoEnabled(final boolean enabled) {
|
|
||||||
// have to re-get them, since flags change everything
|
// have to re-get them, since flags change everything
|
||||||
if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) {
|
if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) {
|
||||||
this.logger.error("Failed to get terminal info");
|
this.logger.error("Failed to get terminal info");
|
||||||
@ -137,8 +131,9 @@ class PosixTerminal extends Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
@Override
|
||||||
void setInterruptEnabled(final boolean enabled) {
|
protected
|
||||||
|
void doSetInterruptEnabled(final boolean enabled) {
|
||||||
// have to re-get them, since flags change everything
|
// have to re-get them, since flags change everything
|
||||||
if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) {
|
if (CLibraryPosix.tcgetattr(0, this.termInfo) != 0) {
|
||||||
this.logger.error("Failed to get terminal info");
|
this.logger.error("Failed to get terminal info");
|
||||||
@ -155,4 +150,11 @@ class PosixTerminal extends Terminal {
|
|||||||
this.logger.error("Can not set terminal flags");
|
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 java.io.IOException;
|
||||||
|
|
||||||
|
import dorkbox.console.Console;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public abstract
|
public abstract
|
||||||
class Terminal {
|
class Terminal {
|
||||||
|
|
||||||
public static final String CONSOLE_ERROR_INIT = "Unable to initialize the input console.";
|
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;
|
static final int DEFAULT_WIDTH = 80;
|
||||||
protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
|
static final int DEFAULT_HEIGHT = 24;
|
||||||
|
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
protected
|
|
||||||
Terminal() {
|
Terminal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract
|
||||||
|
void doSetInterruptEnabled(final boolean enabled);
|
||||||
|
|
||||||
|
protected abstract
|
||||||
|
void doSetEchoEnabled(final boolean enabled);
|
||||||
|
|
||||||
public abstract
|
public abstract
|
||||||
void restore() throws IOException;
|
void restore() throws IOException;
|
||||||
|
|
||||||
@ -38,16 +47,72 @@ class Terminal {
|
|||||||
public abstract
|
public abstract
|
||||||
int getHeight();
|
int getHeight();
|
||||||
|
|
||||||
// NOT THREAD SAFE
|
/**
|
||||||
public abstract
|
* Enables or disables CTRL-C behavior in the console
|
||||||
void setEchoEnabled(final boolean enabled);
|
*/
|
||||||
|
public final
|
||||||
public abstract
|
void setInterruptEnabled(final boolean enabled) {
|
||||||
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
|
public abstract
|
||||||
int read();
|
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.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import dorkbox.util.FastThreadLocal;
|
||||||
import dorkbox.util.bytes.ByteBuffer2;
|
import dorkbox.util.bytes.ByteBuffer2;
|
||||||
|
|
||||||
|
@SuppressWarnings("Duplicates")
|
||||||
public
|
public
|
||||||
class UnsupportedTerminal extends Terminal {
|
class UnsupportedTerminal extends Terminal {
|
||||||
|
|
||||||
private final ByteBuffer2 buffer = new ByteBuffer2(8, -1);
|
private static final char[] newLine;
|
||||||
private final InputStream in = System.in;
|
static {
|
||||||
private int readerCount = -1;
|
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
|
public
|
||||||
UnsupportedTerminal() {
|
UnsupportedTerminal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected
|
||||||
|
void doSetInterruptEnabled(final boolean enabled) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected
|
||||||
|
void doSetEchoEnabled(final boolean enabled) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final
|
public final
|
||||||
void restore() {
|
void restore() {
|
||||||
@ -48,52 +78,99 @@ class UnsupportedTerminal extends Terminal {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
|
* Reads single character input from the console.
|
||||||
|
*
|
||||||
|
* @return -1 if no data or problems
|
||||||
|
*/
|
||||||
public
|
public
|
||||||
void setEchoEnabled(final boolean enabled) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void setInterruptEnabled(final boolean enabled) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
int read() {
|
int read() {
|
||||||
// if we are reading data (because we are in IDE mode), we want to return ALL the chars of the line!
|
// so, 'readCount' is REALLY the index at which we return letters (until the whole string is returned)
|
||||||
|
ByteBuffer2 buffer = this.buffer.get();
|
||||||
// so, 'readerCount' is REALLY the index at which we return letters (until the whole string is returned)
|
if (this.readCount.get() == 0) {
|
||||||
if (this.readerCount == -1) {
|
|
||||||
// we have to wait for more data.
|
// we have to wait for more data.
|
||||||
try {
|
try {
|
||||||
InputStream sysIn = this.in;
|
InputStream sysIn = System.in;
|
||||||
int read;
|
int read;
|
||||||
char asChar;
|
char asChar;
|
||||||
this.buffer.clearSecure();
|
buffer.clearSecure();
|
||||||
|
|
||||||
while ((read = sysIn.read()) != -1) {
|
while ((read = sysIn.read()) != -1) {
|
||||||
asChar = (char) read;
|
asChar = (char) read;
|
||||||
if (asChar == '\n') {
|
if (asChar == '\n') {
|
||||||
this.readerCount = this.buffer.position();
|
int position = buffer.position();
|
||||||
this.buffer.rewind();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.buffer.writeChar(asChar);
|
buffer.writeChar(asChar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EACH thread will have it's own count!
|
// // EACH thread will have it's own count!
|
||||||
if (this.readerCount == this.buffer.position()) {
|
// if (this.readCount == buffer.position()) {
|
||||||
this.readerCount = -1;
|
// this.readCount = -1;
|
||||||
return '\n';
|
// 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.
|
* Terminal implementation for Microsoft Windows.
|
||||||
*/
|
*/
|
||||||
public
|
public
|
||||||
class WindowsTerminal extends Terminal {
|
class WindowsTerminal extends SupportedTerminal {
|
||||||
|
|
||||||
// Console mode constants copied <tt>wincon.h</tt>.
|
// 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.
|
// 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
|
@Override
|
||||||
public
|
protected
|
||||||
void setEchoEnabled(final boolean enabled) {
|
void doSetEchoEnabled(final boolean enabled) {
|
||||||
// only way to do this, console modes DO NOT work
|
// only way to do this, console modes DO NOT work
|
||||||
echoEnabled = enabled;
|
echoEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
protected
|
||||||
void setInterruptEnabled(final boolean enabled) {
|
void doSetInterruptEnabled(final boolean enabled) {
|
||||||
IntByReference mode = new IntByReference();
|
IntByReference mode = new IntByReference();
|
||||||
GetConsoleMode(console, mode);
|
GetConsoleMode(console, mode);
|
||||||
|
|
||||||
@ -138,8 +138,8 @@ class WindowsTerminal extends Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final
|
protected final
|
||||||
int read() {
|
int doRead() {
|
||||||
int input = readInput();
|
int input = readInput();
|
||||||
|
|
||||||
if (echoEnabled) {
|
if (echoEnabled) {
|
||||||
|
@ -32,9 +32,16 @@ package dorkbox.console.output;
|
|||||||
|
|
||||||
import static dorkbox.console.output.AnsiOutputStream.ATTRIBUTE_RESET;
|
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 java.util.ArrayList;
|
||||||
|
|
||||||
import dorkbox.console.Console;
|
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.
|
* 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 dorkbox, llc
|
||||||
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
|
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||||
public
|
public
|
||||||
class Ansi {
|
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 {
|
static {
|
||||||
// make SURE that our console in/out/err are correctly setup BEFORE accessing methods in this class
|
// make SURE that our console in/out/err are correctly setup BEFORE accessing methods in this class
|
||||||
Console.getVersion();
|
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");
|
private static final String NEW_LINE = System.getProperty("line.separator");
|
||||||
@ -57,6 +88,17 @@ class Ansi {
|
|||||||
private final StringBuilder builder;
|
private final StringBuilder builder;
|
||||||
private final ArrayList<Integer> attributeOptions = new ArrayList<Integer>(8);
|
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
|
* Creates a new Ansi object
|
||||||
*/
|
*/
|
||||||
@ -872,4 +914,89 @@ class Ansi {
|
|||||||
builder.append(command);
|
builder.append(command);
|
||||||
return this;
|
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