src.org.python.util.JLineConsole Maven / Gradle / Ivy
Show all versions of jython-standalone Show documentation
// Copyright (c) 2013 Jython Developers
package org.python.util;
import java.io.EOFException;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.List;
import jline.ConsoleReader;
import jline.Terminal;
import jline.WindowsTerminal;
import jnr.constants.platform.Errno;
import org.python.core.PlainConsole;
import org.python.core.PyObject;
/**
* This class uses JLine to provide readline like
* functionality to its console without requiring native readline support.
*/
public class JLineConsole extends PlainConsole {
/** Main interface to JLine. */
public ConsoleReader reader;
/** Callable object set by readline.set_startup_hook
. */
protected PyObject startup_hook;
/** Not currently set by readline.set_pre_input_hook
. Why not? */
protected PyObject pre_input_hook;
/** Whether reader is a WindowsTerminal. */
private boolean windows;
/** The ctrl-z character String. */
protected static final String CTRL_Z = "\u001a";
/** Stream wrapping System.out in order to capture the last prompt. */
private ConsoleOutputStream outWrapper;
/**
* Errno strerrors possibly caused by a SIGSTP (ctrl-z). They may propagate up to IOException
* messages.
*/
private static final List SUSPENDED_STRERRORS = Arrays.asList(
Errno.EINTR.description(), Errno.EIO.description());
/**
* Construct an instance of the console class specifying the character encoding. This encoding
* must be one supported by the JVM.
*
* Most of the initialisation is deferred to the {@link #install()} method so that any prior
* console can uninstall itself before we change system console settings and
* System.in
.
*
* @param encoding name of a supported encoding or null
for
* Charset.defaultCharset()
*/
public JLineConsole(String encoding) {
/*
* Super-class needs the encoding in order to re-encode the characters that
* jline.ConsoleReader.readLine() has decoded.
*/
super(encoding);
/*
* Communicate the specified encoding to JLine. jline.ConsoleReader.readLine() edits a line
* of characters, decoded from stdin.
*/
System.setProperty("jline.WindowsTerminal.input.encoding", this.encoding);
System.setProperty("input.encoding", this.encoding);
// ... not "jline.UnixTerminal.input.encoding" as you might think, not even in JLine2
}
/**
* {@inheritDoc}
*
* This implementation overrides that by setting System.in
to a
* FilterInputStream
object that wraps JLine, and System.out
* with a layer that buffers incomplete lines for use as the console prompt.
*/
@Override
public void install() {
Terminal.setupTerminal();
String userHomeSpec = System.getProperty("user.home", ".");
// Configure a ConsoleReader (the object that does most of the line editing).
try {
/*
* Wrap System.out in the specified encoding. jline.ConsoleReader.readLine() echoes the
* line through this Writer.
*/
Writer out = new PrintWriter(new OutputStreamWriter(System.out, encoding));
// Get the key bindings (built in ones treat TAB Pythonically).
InputStream bindings = getBindings(userHomeSpec, getClass().getClassLoader());
// Create the reader as unbuffered as possible
InputStream in = new FileInputStream(FileDescriptor.in);
reader = new ConsoleReader(in, out, bindings);
// We find the bell too noisy
reader.setBellEnabled(false);
/*
* Everybody else, using sys.stdout or java.lang.System.out gets to write on a special
* PrintStream that keeps the last incomplete line in case it turns out to be a console
* prompt.
*/
outWrapper = new ConsoleOutputStream(System.out, reader.getTermwidth());
System.setOut(new PrintStream(outWrapper, true, encoding));
} catch (IOException e) {
throw new RuntimeException(e);
}
// Access and load (if possible) the line history.
try {
File historyFile = new File(userHomeSpec, ".jline-jython.history");
reader.getHistory().setHistoryFile(historyFile);
} catch (IOException e) {
// oh well, no history from file
}
// Check for OS type
windows = reader.getTerminal() instanceof WindowsTerminal;
// Replace System.in
FilterInputStream wrapper = new Stream();
System.setIn(wrapper);
}
/**
* Class to wrap the line-oriented interface to JLine with an InputStream that can replace
* System.in.
*/
private class Stream extends ConsoleInputStream {
/** Create a System.in replacement with JLine that adds system-specific line endings */
Stream() {
super(System.in, encodingCharset, EOLPolicy.ADD, LINE_SEPARATOR);
}
@Override
protected CharSequence getLine() throws IOException, EOFException {
// Get a line and hope to be done. The prompt is the current partial output line.
String prompt = outWrapper.getPrompt(encodingCharset).toString();
String line = readerReadLine(prompt);
if (!isEOF(line)) {
return line;
} else {
// null or ctrl-z on Windows indicates EOF
throw new EOFException();
}
}
}
/**
* Wrapper on reader.readLine(prompt) that deals with retries (on Unix) when the user enters
* ctrl-Z to background Jython, then brings it back to the foreground. The inherited
* implementation says this is necessary and effective on BSD Unix.
*
* @param prompt to display
* @return line of text read in
* @throws IOException if an error occurs (other than an end of suspension)
* @throws EOFException if an EOF is detected
*/
private String readerReadLine(String prompt) throws IOException, EOFException {
// We must be prepared to try repeatedly since the read may be interrupted.
while (true) {
try {
// If there's a hook, call it
if (startup_hook != null) {
startup_hook.__call__();
}
// Send the cursor to the start of the line (no prompt, empty buffer).
reader.setDefaultPrompt(null);
reader.redrawLine();
// The prompt is whatever was already on the line.
String line = reader.readLine(prompt);
return line;
} catch (IOException ioe) {
// Something went wrong, or we were interrupted (seems only BSD throws this)
if (!fromSuspend(ioe)) {
// The interruption is not the result of (the end of) a ctrl-Z suspension
throw ioe;
} else {
// The interruption seems to be (return from) a ctrl-Z suspension:
try {
// Must reset JLine and continue (not repeating the prompt)
reader.getTerminal().initializeTerminal();
prompt = "";
} catch (Exception e) {
// Do our best to say what went wrong
throw new IOException("Failed to re-initialize JLine: " + e.getMessage());
}
}
}
}
}
/**
* Return the JLine bindings file.
*
* This handles loading the user's custom key bindings (normally JLine does) so it can fall back
* to Jython's (which disable tab completion) when the user's are not available.
*
* @return an InputStream of the JLine bindings file.
*/
protected static InputStream getBindings(String userHomeSpec, ClassLoader loader) {
// The key bindings file may be specified explicitly
String bindingsFileSpec = System.getProperty("jline.keybindings");
File bindingsFile;
if (bindingsFileSpec != null) {
// Bindings file explicitly specified
bindingsFile = new File(bindingsFileSpec);
} else {
// Otherwise try ~/.jlinebindings.properties
bindingsFile = new File(userHomeSpec, ".jlinebindings.properties");
}
// See if that file really exists (and can be read)
try {
if (bindingsFile.isFile()) {
try {
return new FileInputStream(bindingsFile);
} catch (FileNotFoundException fnfe) {
// Shouldn't really ever happen
fnfe.printStackTrace();
}
}
} catch (SecurityException se) {
// continue
}
// User/specific key bindings could not be read: use the ones from the class path or jar.
return loader.getResourceAsStream("org/python/util/jline-keybindings.properties");
}
/**
* Determine if the IOException was likely caused by a SIGSTP (ctrl-z). Seems only applicable to
* BSD platforms.
*/
private boolean fromSuspend(IOException ioe) {
return !windows && SUSPENDED_STRERRORS.contains(ioe.getMessage());
}
/**
* Determine if line denotes an EOF.
*/
private boolean isEOF(String line) {
return line == null || (windows && CTRL_Z.equals(line));
}
/**
* @return the JLine console reader associated with this interpreter
*/
public ConsoleReader getReader() {
return reader;
}
/**
* @return the startup hook (called prior to each readline)
*/
public PyObject getStartupHook() {
return startup_hook;
}
/**
* Sets the startup hook (called prior to each readline)
*/
public void setStartupHook(PyObject hook) {
startup_hook = hook;
}
}