All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jline.console.ConsoleReader Maven / Gradle / Ivy

/*
 * Copyright (c) 2002-2016, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * http://www.opensource.org/licenses/bsd-license.php
 */
package jline.console;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.ResourceBundle;
import java.util.Stack;

import jline.DefaultTerminal2;
import jline.Terminal;
import jline.Terminal2;
import jline.TerminalFactory;
import jline.UnixTerminal;
import jline.console.completer.CandidateListCompletionHandler;
import jline.console.completer.Completer;
import jline.console.completer.CompletionHandler;
import jline.console.history.History;
import jline.console.history.MemoryHistory;
import jline.internal.Ansi;
import jline.internal.Configuration;
import jline.internal.Curses;
import jline.internal.InputStreamReader;
import jline.internal.Log;
import jline.internal.NonBlockingInputStream;
import jline.internal.Nullable;
import jline.internal.TerminalLineSettings;
import jline.internal.Urls;

import static jline.internal.Preconditions.checkNotNull;

/**
 * A reader for console applications. It supports custom tab-completion,
 * saveable command history, and command line editing. On some platforms,
 * platform-specific commands will need to be issued before the reader will
 * function properly. See {@link jline.Terminal#init} for convenience
 * methods for issuing platform-specific setup commands.
 *
 * @author Marc Prud'hommeaux
 * @author Jason Dillon
 * @author Guillaume Nodet
 */
public class ConsoleReader implements Closeable
{
    public static final String JLINE_NOBELL = "jline.nobell";

    public static final String JLINE_ESC_TIMEOUT = "jline.esc.timeout";

    public static final String JLINE_INPUTRC = "jline.inputrc";

    public static final String INPUT_RC = ".inputrc";

    public static final String DEFAULT_INPUT_RC = "/etc/inputrc";

    public static final String JLINE_EXPAND_EVENTS = "jline.expandevents";

    public static final char BACKSPACE = '\b';

    public static final char RESET_LINE = '\r';

    public static final char KEYBOARD_BELL = '\07';

    public static final char NULL_MASK = 0;

    public static final int TAB_WIDTH = 8;

    private static final ResourceBundle
        resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName());

    private static final int ESCAPE = 27;
    private static final int READ_EXPIRED = -2;

    private final Terminal2 terminal;

    private final Writer out;

    private final CursorBuffer buf = new CursorBuffer();
    private boolean cursorOk;

    private String prompt;
    private int    promptLen;

    private boolean expandEvents = Configuration.getBoolean(JLINE_EXPAND_EVENTS, true);

    private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true);

    private boolean handleUserInterrupt = false;

    private boolean handleLitteralNext = true;

    private Character mask;

    private Character echoCharacter;

    private CursorBuffer originalBuffer = null;

    private StringBuffer searchTerm = null;

    private String previousSearchTerm = "";

    private int searchIndex = -1;

    private int parenBlinkTimeout = 500;

    // Reading buffers
    private final StringBuilder opBuffer = new StringBuilder();
    private final Stack pushBackChar = new Stack();

    /*
     * The reader and the nonBlockingInput go hand-in-hand.  The reader wraps
     * the nonBlockingInput, but we have to retain a handle to it so that
     * we can shut down its blocking read thread when we go away.
     */
    private NonBlockingInputStream in;
    private long                   escapeTimeout;
    private Reader                 reader;

    /**
     * Last character searched for with a vi character search
     */
    private char  charSearchChar = 0;           // Character to search for
    private char  charSearchLastInvokeChar = 0; // Most recent invocation key
    private char  charSearchFirstInvokeChar = 0;// First character that invoked

    /**
     * The vi yank buffer
     */
    private String yankBuffer = "";

    private KillRing killRing = new KillRing();

    private String encoding;

    private boolean quotedInsert;

    private boolean recording;

    private String macro = "";

    private String appName;

    private URL inputrcUrl;

    private ConsoleKeys consoleKeys;

    private String commentBegin = null;

    private boolean skipLF = false;

    /**
     * Set to true if the reader should attempt to detect copy-n-paste. The
     * effect of this that an attempt is made to detect if tab is quickly
     * followed by another character, then it is assumed that the tab was
     * a literal tab as part of a copy-and-paste operation and is inserted as
     * such.
     */
    private boolean copyPasteDetection = false;

    /*
     * Current internal state of the line reader
     */
    private State   state = State.NORMAL;

    /**
     * Possible states in which the current readline operation may be in.
     */
    private static enum State {
        /**
         * The user is just typing away
         */
        NORMAL,
        /**
         * In the middle of a emacs seach
         */
        SEARCH,
        FORWARD_SEARCH,
        /**
         * VI "yank-to" operation ("y" during move mode)
         */
        VI_YANK_TO,
        /**
         * VI "delete-to" operation ("d" during move mode)
         */
        VI_DELETE_TO,
        /**
         * VI "change-to" operation ("c" during move mode)
         */
        VI_CHANGE_TO
    }

    public ConsoleReader() throws IOException {
        this(null, new FileInputStream(FileDescriptor.in), System.out, null);
    }

    public ConsoleReader(final InputStream in, final OutputStream out) throws IOException {
        this(null, in, out, null);
    }

    public ConsoleReader(final InputStream in, final OutputStream out, final Terminal term) throws IOException {
        this(null, in, out, term);
    }

    public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term) throws IOException {
        this(appName, in, out, term, null);
    }

    public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term, final @Nullable String encoding)
        throws IOException
    {
        this.appName = appName != null ? appName : "JLine";
        this.encoding = encoding != null ? encoding : Configuration.getEncoding();
        Terminal terminal = term != null ? term : TerminalFactory.get();
        this.terminal = terminal instanceof Terminal2 ? (Terminal2) terminal : new DefaultTerminal2(terminal);
        String outEncoding = terminal.getOutputEncoding() != null? terminal.getOutputEncoding() : this.encoding;
        this.out = new OutputStreamWriter(terminal.wrapOutIfNeeded(out), outEncoding);
        setInput( in );

        this.inputrcUrl = getInputRc();

        consoleKeys = new ConsoleKeys(this.appName, inputrcUrl);

        if (terminal instanceof UnixTerminal
                && TerminalLineSettings.DEFAULT_TTY.equals(((UnixTerminal) terminal).getSettings().getTtyDevice())
                && Configuration.getBoolean("jline.sigcont", false)) {
            setupSigCont();
        }
    }

    private void setupSigCont() {
        // Check that sun.misc.SignalHandler and sun.misc.Signal exists
        try {
            Class signalClass = Class.forName("sun.misc.Signal");
            Class signalHandlerClass = Class.forName("sun.misc.SignalHandler");
            // Implement signal handler
            Object signalHandler = Proxy.newProxyInstance(getClass().getClassLoader(),
                    new Class[]{signalHandlerClass}, new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            // only method we are proxying is handle()
                            terminal.init();
                            try {
                                drawLine();
                                flush();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            return null;
                        }
                    });
            // Register the signal handler, this code is equivalent to:
            // Signal.handle(new Signal("CONT"), signalHandler);
            signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("CONT"), signalHandler);
        } catch (ClassNotFoundException cnfe) {
            // sun.misc Signal handler classes don't exist
        } catch (Exception e) {
            // Ignore this one too, if the above failed, the signal API is incompatible with what we're expecting
        }
    }

    private static URL getInputRc() throws IOException {
        String path = Configuration.getString(JLINE_INPUTRC);
        if (path == null) {
            File f = new File(Configuration.getUserHome(), INPUT_RC);
            if (!f.exists()) {
                f = new File(DEFAULT_INPUT_RC);
            }
            return f.toURI().toURL();
        } else {
            return Urls.create(path);
        }
    }

    public KeyMap getKeys() {
        return consoleKeys.getKeys();
    }

    void setInput(final InputStream in) throws IOException {
        this.escapeTimeout = Configuration.getLong(JLINE_ESC_TIMEOUT, 100);
        boolean nonBlockingEnabled =
               escapeTimeout > 0L
            && terminal.isSupported()
            && in != null;

        /*
         * If we had a non-blocking thread already going, then shut it down
         * and start a new one.
         */
        if (this.in != null) {
            this.in.shutdown();
        }

        final InputStream wrapped = terminal.wrapInIfNeeded( in );

        this.in = new NonBlockingInputStream(wrapped, nonBlockingEnabled);
        this.reader = new InputStreamReader( this.in, encoding );
    }

    /**
     * Shuts the console reader down.  This method should be called when you
     * have completed using the reader as it shuts down and cleans up resources
     * that would otherwise be "leaked".
     */
    @Override
    public void close() {
        if (in != null) {
            in.shutdown();
        }
    }

    /**
     * Shuts the console reader down.  The same as {@link #close()}.
     * @deprecated Use {@link #close()} instead.
     */
    @Deprecated
    public void shutdown() {
        this.close();
    }
    
    /**
     * Shuts down the ConsoleReader if the JVM attempts to clean it up.
     */
    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        }
        finally {
            super.finalize();
        }
    }

    public InputStream getInput() {
        return in;
    }

    public Writer getOutput() {
        return out;
    }

    public Terminal getTerminal() {
        return terminal;
    }

    public CursorBuffer getCursorBuffer() {
        return buf;
    }

    public void setExpandEvents(final boolean expand) {
        this.expandEvents = expand;
    }

    public boolean getExpandEvents() {
        return expandEvents;
    }

    /**
     * Enables or disables copy and paste detection. The effect of enabling this
     * this setting is that when a tab is received immediately followed by another
     * character, the tab will not be treated as a completion, but as a tab literal.
     * @param onoff true if detection is enabled
     */
    public void setCopyPasteDetection(final boolean onoff) {
        copyPasteDetection = onoff;
    }

    /**
     * @return true if copy and paste detection is enabled.
     */
    public boolean isCopyPasteDetectionEnabled() {
        return copyPasteDetection;
    }

    /**
     * Set whether the console bell is enabled.
     *
     * @param enabled true if enabled; false otherwise
     * @since 2.7
     */
    public void setBellEnabled(boolean enabled) {
        this.bellEnabled = enabled;
    }

    /**
     * Get whether the console bell is enabled
     *
     * @return true if enabled; false otherwise
     * @since 2.7
     */
    public boolean getBellEnabled() {
        return bellEnabled;
    }

    /**
     * Set whether user interrupts (ctrl-C) are handled by having JLine
     * throw {@link UserInterruptException} from {@link #readLine}.
     * Otherwise, the JVM will handle {@code SIGINT} as normal, which
     * usually causes it to exit. The default is {@code false}.
     *
     * @since 2.10
     */
    public void setHandleUserInterrupt(boolean enabled)
    {
        this.handleUserInterrupt = enabled;
    }

    /**
     * Get whether user interrupt handling is enabled
     *
     * @return true if enabled; false otherwise
     * @since 2.10
     */
    public boolean getHandleUserInterrupt()
    {
        return handleUserInterrupt;
    }

    /**
     * Set wether literal next are handled by JLine.
     *
     * @since 2.13
     */
    public void setHandleLitteralNext(boolean handleLitteralNext) {
        this.handleLitteralNext = handleLitteralNext;
    }

    /**
     * Get wether literal next are handled by JLine.
     *
     * @since 2.13
     */
    public boolean getHandleLitteralNext() {
        return handleLitteralNext;
    }

    /**
     * Sets the string that will be used to start a comment when the
     * insert-comment key is struck.
     * @param commentBegin The begin comment string.
     * @since 2.7
     */
    public void setCommentBegin(String commentBegin) {
        this.commentBegin = commentBegin;
    }

    /**
     * @return the string that will be used to start a comment when the
     * insert-comment key is struck.
     * @since 2.7
     */
    public String getCommentBegin() {
        String str = commentBegin;

        if (str == null) {
            str = consoleKeys.getVariable("comment-begin");
            if (str == null) {
                str = "#";
            }
        }
        return str;
    }

    public void setPrompt(final String prompt) {
        this.prompt = prompt;
        this.promptLen = (prompt == null) ? 0 : wcwidth(Ansi.stripAnsi(lastLine(prompt)), 0);
    }

    public String getPrompt() {
        return prompt;
    }

    /**
     * Set the echo character. For example, to have "*" entered when a password is typed:
     * 
     * myConsoleReader.setEchoCharacter(new Character('*'));
     * 
* Setting the character to null will restore normal character echoing.

* Setting the character to Character.valueOf(0) will cause nothing to be echoed. * * @param c the character to echo to the console in place of the typed character. */ public void setEchoCharacter(final Character c) { this.echoCharacter = c; } /** * Returns the echo character. */ public Character getEchoCharacter() { return echoCharacter; } /** * Erase the current line. * * @return false if we failed (e.g., the buffer was empty) */ protected final boolean resetLine() throws IOException { if (buf.cursor == 0) { return false; } StringBuilder killed = new StringBuilder(); while (buf.cursor > 0) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return true; } int wcwidth(CharSequence str, int pos) { return wcwidth(str, 0, str.length(), pos); } int wcwidth(CharSequence str, int start, int end, int pos) { int cur = pos; for (int i = start; i < end;) { int ucs; char c1 = str.charAt(i++); if (!Character.isHighSurrogate(c1) || i >= end) { ucs = c1; } else { char c2 = str.charAt(i); if (Character.isLowSurrogate(c2)) { i++; ucs = Character.toCodePoint(c1, c2); } else { ucs = c1; } } cur += wcwidth(ucs, cur); } return cur - pos; } int wcwidth(int ucs, int pos) { if (ucs == '\t') { return nextTabStop(pos); } else if (ucs < 32) { return 2; } else { int w = WCWidth.wcwidth(ucs); return w > 0 ? w : 0; } } int nextTabStop(int pos) { int tabWidth = TAB_WIDTH; int width = getTerminal().getWidth(); int mod = (pos + tabWidth - 1) % tabWidth; int npos = pos + tabWidth - mod; return npos < width ? npos - pos : width - pos; } int getCursorPosition() { return promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen); } /** * Returns the text after the last '\n'. * prompt is returned if no '\n' characters are present. * null is returned if prompt is null. */ private static String lastLine(String str) { if (str == null) return ""; int last = str.lastIndexOf("\n"); if (last >= 0) { return str.substring(last + 1, str.length()); } return str; } /** * Move the cursor position to the specified absolute index. */ public boolean setCursorPosition(final int position) throws IOException { if (position == buf.cursor) { return true; } return moveCursor(position - buf.cursor) != 0; } /** * Set the current buffer's content to the specified {@link String}. The * visual console will be modified to show the current buffer. * * @param buffer the new contents of the buffer. */ private void setBuffer(final String buffer) throws IOException { // don't bother modifying it if it is unchanged if (buffer.equals(buf.buffer.toString())) { return; } // obtain the difference between the current buffer and the new one int sameIndex = 0; for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1) && (i < l2); i++) { if (buffer.charAt(i) == buf.buffer.charAt(i)) { sameIndex++; } else { break; } } int diff = buf.cursor - sameIndex; if (diff < 0) { // we can't backspace here so try from the end of the buffer moveToEnd(); diff = buf.buffer.length() - sameIndex; } backspace(diff); // go back for the differences killLine(); // clear to the end of the line buf.buffer.setLength(sameIndex); // the new length putString(buffer.substring(sameIndex)); // append the differences } private void setBuffer(final CharSequence buffer) throws IOException { setBuffer(String.valueOf(buffer)); } private void setBufferKeepPos(final String buffer) throws IOException { int pos = buf.cursor; setBuffer(buffer); setCursorPosition(pos); } private void setBufferKeepPos(final CharSequence buffer) throws IOException { setBufferKeepPos(String.valueOf(buffer)); } /** * Output put the prompt + the current buffer */ public void drawLine() throws IOException { String prompt = getPrompt(); if (prompt != null) { rawPrint(prompt); } fmtPrint(buf.buffer, 0, buf.cursor, promptLen); // force drawBuffer to check for weird wrap (after clear screen) drawBuffer(); } /** * Clear the line and redraw it. */ public void redrawLine() throws IOException { tputs("carriage_return"); drawLine(); } /** * Clear the buffer and add its contents to the history. * * @return the former contents of the buffer. */ final String finishBuffer() throws IOException { // FIXME: Package protected because used by tests String str = buf.buffer.toString(); String historyLine = str; if (expandEvents) { try { str = expandEvents(str); // all post-expansion occurrences of '!' must have been escaped, so re-add escape to each historyLine = str.replace("!", "\\!"); // only leading '^' results in expansion, so only re-add escape for that case historyLine = historyLine.replaceAll("^\\^", "\\\\^"); } catch(IllegalArgumentException e) { Log.error("Could not expand event", e); beep(); buf.clear(); str = ""; } } // we only add it to the history if the buffer is not empty // and if mask is null, since having a mask typically means // the string was a password. We clear the mask after this call if (str.length() > 0) { if (mask == null && isHistoryEnabled()) { history.add(historyLine); } else { mask = null; } } history.moveToEnd(); buf.buffer.setLength(0); buf.cursor = 0; return str; } /** * Expand event designator such as !!, !#, !3, etc... * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html */ @SuppressWarnings("fallthrough") protected String expandEvents(String str) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); switch (c) { case '\\': // any '\!' should be considered an expansion escape, so skip expansion and strip the escape character // a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character // otherwise, add the escape if (i + 1 < str.length()) { char nextChar = str.charAt(i+1); if (nextChar == '!' || (nextChar == '^' && i == 0)) { c = nextChar; i++; } } sb.append(c); break; case '!': if (i + 1 < str.length()) { c = str.charAt(++i); boolean neg = false; String rep = null; int i1, idx; switch (c) { case '!': if (history.size() == 0) { throw new IllegalArgumentException("!!: event not found"); } rep = history.get(history.index() - 1).toString(); break; case '#': sb.append(sb.toString()); break; case '?': i1 = str.indexOf('?', i + 1); if (i1 < 0) { i1 = str.length(); } String sc = str.substring(i + 1, i1); i = i1; idx = searchBackwards(sc); if (idx < 0) { throw new IllegalArgumentException("!?" + sc + ": event not found"); } else { rep = history.get(idx).toString(); } break; case '$': if (history.size() == 0) { throw new IllegalArgumentException("!$: event not found"); } String previous = history.get(history.index() - 1).toString().trim(); int lastSpace = previous.lastIndexOf(' '); if(lastSpace != -1) { rep = previous.substring(lastSpace+1); } else { rep = previous; } break; case ' ': case '\t': sb.append('!'); sb.append(c); break; case '-': neg = true; i++; // fall through case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': i1 = i; for (; i < str.length(); i++) { c = str.charAt(i); if (c < '0' || c > '9') { break; } } idx = 0; try { idx = Integer.parseInt(str.substring(i1, i)); } catch (NumberFormatException e) { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } if (neg) { if (idx > 0 && idx <= history.size()) { rep = (history.get(history.index() - idx)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } } else { if (idx > history.index() - history.size() && idx <= history.index()) { rep = (history.get(idx - 1)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } } break; default: String ss = str.substring(i); i = str.length(); idx = searchBackwards(ss, history.index(), true); if (idx < 0) { throw new IllegalArgumentException("!" + ss + ": event not found"); } else { rep = history.get(idx).toString(); } break; } if (rep != null) { sb.append(rep); } } else { sb.append(c); } break; case '^': if (i == 0) { int i1 = str.indexOf('^', i + 1); int i2 = str.indexOf('^', i1 + 1); if (i2 < 0) { i2 = str.length(); } if (i1 > 0 && i2 > 0) { String s1 = str.substring(i + 1, i1); String s2 = str.substring(i1 + 1, i2); String s = history.get(history.index() - 1).toString().replace(s1, s2); sb.append(s); i = i2 + 1; break; } } sb.append(c); break; default: sb.append(c); break; } } String result = sb.toString(); if (!str.equals(result)) { fmtPrint(result, getCursorPosition()); println(); flush(); } return result; } /** * Write out the specified string to the buffer and the output stream. */ public void putString(final CharSequence str) throws IOException { int pos = getCursorPosition(); buf.write(str); if (mask == null) { // no masking fmtPrint(str, pos); } else if (mask == NULL_MASK) { // don't print anything } else { rawPrint(mask, str.length()); } drawBuffer(); } /** * Redraw the rest of the buffer from the cursor onwards. This is necessary * for inserting text into the buffer. * * @param clear the number of characters to clear after the end of the buffer */ private void drawBuffer(final int clear) throws IOException { // debug ("drawBuffer: " + clear); int nbChars = buf.length() - buf.cursor; if (buf.cursor != buf.length() || clear != 0) { if (mask != null) { if (mask != NULL_MASK) { rawPrint(mask, nbChars); } else { nbChars = 0; } } else { fmtPrint(buf.buffer, buf.cursor, buf.length()); } } int cursorPos = promptLen + wcwidth(buf.buffer, 0, buf.length(), promptLen); if (terminal.hasWeirdWrap() && !cursorOk) { int width = terminal.getWidth(); // best guess on whether the cursor is in that weird location... // Need to do this without calling ansi cursor location methods // otherwise it breaks paste of wrapped lines in xterm. if (cursorPos > 0 && (cursorPos % width == 0)) { // the following workaround is reverse-engineered from looking // at what bash sent to the terminal in the same situation rawPrint(' '); // move cursor to next line by printing dummy space tputs("carriage_return"); // CR / not newline. } cursorOk = true; } clearAhead(clear, cursorPos); back(nbChars); } /** * Redraw the rest of the buffer from the cursor onwards. This is necessary * for inserting text into the buffer. */ private void drawBuffer() throws IOException { drawBuffer(0); } /** * Clear ahead the specified number of characters without moving the cursor. * * @param num the number of characters to clear * @param pos the current screen cursor position */ private void clearAhead(int num, final int pos) throws IOException { if (num == 0) return; int width = terminal.getWidth(); // Use kill line if (terminal.getStringCapability("clr_eol") != null) { int cur = pos; int c0 = cur % width; // Erase end of current line int nb = Math.min(num, width - c0); tputs("clr_eol"); num -= nb; // Loop while (num > 0) { // Move to beginning of next line int prev = cur; cur = cur - cur % width + width; moveCursorFromTo(prev, cur); // Erase nb = Math.min(num, width); tputs("clr_eol"); num -= nb; } moveCursorFromTo(cur, pos); } // Terminal does not wrap on the right margin else if (!terminal.getBooleanCapability("auto_right_margin")) { int cur = pos; int c0 = cur % width; // Erase end of current line int nb = Math.min(num, width - c0); rawPrint(' ', nb); num -= nb; cur += nb; // Loop while (num > 0) { // Move to beginning of next line moveCursorFromTo(cur, ++cur); // Erase nb = Math.min(num, width); rawPrint(' ', nb); num -= nb; cur += nb; } moveCursorFromTo(cur, pos); } // Simple erasure else { rawPrint(' ', num); moveCursorFromTo(pos + num, pos); } } /** * Move the visual cursor backward without modifying the buffer cursor. */ protected void back(final int num) throws IOException { if (num == 0) return; int i0 = promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen); int i1 = i0 + ((mask != null) ? num : wcwidth(buf.buffer, buf.cursor, buf.cursor + num, i0)); moveCursorFromTo(i1, i0); } /** * Flush the console output stream. This is important for printout out single characters (like a backspace or * keyboard) that we want the console to handle immediately. */ public void flush() throws IOException { out.flush(); } private int backspaceAll() throws IOException { return backspace(Integer.MAX_VALUE); } /** * Issue num backspaces. * * @return the number of characters backed up */ private int backspace(final int num) throws IOException { if (buf.cursor == 0) { return 0; } int count = - moveCursor(-num); int clear = wcwidth(buf.buffer, buf.cursor, buf.cursor + count, getCursorPosition()); buf.buffer.delete(buf.cursor, buf.cursor + count); drawBuffer(clear); return count; } /** * Issue a backspace. * * @return true if successful */ public boolean backspace() throws IOException { return backspace(1) == 1; } protected boolean moveToEnd() throws IOException { if (buf.cursor == buf.length()) { return true; } return moveCursor(buf.length() - buf.cursor) > 0; } /** * Delete the character at the current position and redraw the remainder of the buffer. */ private boolean deleteCurrentCharacter() throws IOException { if (buf.length() == 0 || buf.cursor == buf.length()) { return false; } buf.buffer.deleteCharAt(buf.cursor); drawBuffer(1); return true; } /** * This method is calling while doing a delete-to ("d"), change-to ("c"), * or yank-to ("y") and it filters out only those movement operations * that are allowable during those operations. Any operation that isn't * allow drops you back into movement mode. * * @param op The incoming operation to remap * @return The remaped operation */ private Operation viDeleteChangeYankToRemap (Operation op) { switch (op) { case VI_EOF_MAYBE: case ABORT: case BACKWARD_CHAR: case FORWARD_CHAR: case END_OF_LINE: case VI_MATCH: case VI_BEGINNING_OF_LINE_OR_ARG_DIGIT: case VI_ARG_DIGIT: case VI_PREV_WORD: case VI_END_WORD: case VI_CHAR_SEARCH: case VI_NEXT_WORD: case VI_FIRST_PRINT: case VI_GOTO_MARK: case VI_COLUMN: case VI_DELETE_TO: case VI_YANK_TO: case VI_CHANGE_TO: return op; default: return Operation.VI_MOVEMENT_MODE; } } /** * Deletes the previous character from the cursor position * @param count number of times to do it. * @return true if it was done. */ private boolean viRubout(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = backspace(); } return ok; } /** * Deletes the character you are sitting on and sucks the rest of * the line in from the right. * @param count Number of times to perform the operation. * @return true if its works, false if it didn't */ private boolean viDelete(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = deleteCurrentCharacter(); } return ok; } /** * Switches the case of the current character from upper to lower * or lower to upper as necessary and advances the cursor one * position to the right. * @param count The number of times to repeat * @return true if it completed successfully, false if not all * case changes could be completed. */ private boolean viChangeCase(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = buf.cursor < buf.buffer.length (); if (ok) { char ch = buf.buffer.charAt(buf.cursor); if (Character.isUpperCase(ch)) { ch = Character.toLowerCase(ch); } else if (Character.isLowerCase(ch)) { ch = Character.toUpperCase(ch); } buf.buffer.setCharAt(buf.cursor, ch); drawBuffer(1); moveCursor(1); } } return ok; } /** * Implements the vi change character command (in move-mode "r" * followed by the character to change to). * @param count Number of times to perform the action * @param c The character to change to * @return Whether or not there were problems encountered */ private boolean viChangeChar(int count, int c) throws IOException { // EOF, ESC, or CTRL-C aborts. if (c < 0 || c == '\033' || c == '\003') { return true; } boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = buf.cursor < buf.buffer.length (); if (ok) { buf.buffer.setCharAt(buf.cursor, (char) c); drawBuffer(1); if (i < (count-1)) { moveCursor(1); } } } return ok; } /** * This is a close facsimile of the actual vi previous word logic. In * actual vi words are determined by boundaries of identity characterse. * This logic is a bit more simple and simply looks at white space or * digits or characters. It should be revised at some point. * * @param count number of iterations * @return true if the move was successful, false otherwise */ private boolean viPreviousWord(int count) throws IOException { boolean ok = true; if (buf.cursor == 0) { return false; } int pos = buf.cursor - 1; for (int i = 0; pos > 0 && i < count; i++) { // If we are on white space, then move back. while (pos > 0 && isWhitespace(buf.buffer.charAt(pos))) { --pos; } while (pos > 0 && !isDelimiter(buf.buffer.charAt(pos-1))) { --pos; } if (pos > 0 && i < (count-1)) { --pos; } } setCursorPosition(pos); return ok; } /** * Performs the vi "delete-to" action, deleting characters between a given * span of the input line. * @param startPos The start position * @param endPos The end position. * @param isChange If true, then the delete is part of a change operationg * (e.g. "c$" is change-to-end-of line, so we first must delete to end * of line to start the change * @return true if it succeeded, false otherwise */ private boolean viDeleteTo(int startPos, int endPos, boolean isChange) throws IOException { if (startPos == endPos) { return true; } if (endPos < startPos) { int tmp = endPos; endPos = startPos; startPos = tmp; } setCursorPosition(startPos); buf.cursor = startPos; buf.buffer.delete(startPos, endPos); drawBuffer(endPos - startPos); // If we are doing a delete operation (e.g. "d$") then don't leave the // cursor dangling off the end. In reality the "isChange" flag is silly // what is really happening is that if we are in "move-mode" then the // cursor can't be moved off the end of the line, but in "edit-mode" it // is ok, but I have no easy way of knowing which mode we are in. if (! isChange && startPos > 0 && startPos == buf.length()) { moveCursor(-1); } return true; } /** * Implement the "vi" yank-to operation. This operation allows you * to yank the contents of the current line based upon a move operation, * for exaple "yw" yanks the current word, "3yw" yanks 3 words, etc. * * @param startPos The starting position from which to yank * @param endPos The ending position to which to yank * @return true if the yank succeeded */ private boolean viYankTo(int startPos, int endPos) throws IOException { int cursorPos = startPos; if (endPos < startPos) { int tmp = endPos; endPos = startPos; startPos = tmp; } if (startPos == endPos) { yankBuffer = ""; return true; } yankBuffer = buf.buffer.substring(startPos, endPos); /* * It was a movement command that moved the cursor to find the * end position, so put the cursor back where it started. */ setCursorPosition(cursorPos); return true; } /** * Pasts the yank buffer to the right of the current cursor position * and moves the cursor to the end of the pasted region. * * @param count Number of times to perform the operation. * @return true if it worked, false otherwise */ private boolean viPut(int count) throws IOException { if (yankBuffer.length () == 0) { return true; } if (buf.cursor < buf.buffer.length ()) { moveCursor(1); } for (int i = 0; i < count; i++) { putString(yankBuffer); } moveCursor(-1); return true; } /** * Searches forward of the current position for a character and moves * the cursor onto it. * @param count Number of times to repeat the process. * @param ch The character to search for * @return true if the char was found, false otherwise */ private boolean viCharSearch(int count, int invokeChar, int ch) throws IOException { if (ch < 0 || invokeChar < 0) { return false; } char searchChar = (char)ch; boolean isForward; boolean stopBefore; /* * The character stuff turns out to be hairy. Here is how it works: * f - search forward for ch * F - search backward for ch * t - search forward for ch, but stop just before the match * T - search backward for ch, but stop just after the match * ; - After [fFtT;], repeat the last search, after ',' reverse it * , - After [fFtT;], reverse the last search, after ',' repeat it */ if (invokeChar == ';' || invokeChar == ',') { // No recent search done? Then bail if (charSearchChar == 0) { return false; } // Reverse direction if switching between ',' and ';' if (charSearchLastInvokeChar == ';' || charSearchLastInvokeChar == ',') { if (charSearchLastInvokeChar != invokeChar) { charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); } } else { if (invokeChar == ',') { charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); } } searchChar = charSearchChar; } else { charSearchChar = searchChar; charSearchFirstInvokeChar = (char) invokeChar; } charSearchLastInvokeChar = (char)invokeChar; isForward = Character.isLowerCase(charSearchFirstInvokeChar); stopBefore = (Character.toLowerCase(charSearchFirstInvokeChar) == 't'); boolean ok = false; if (isForward) { while (count-- > 0) { int pos = buf.cursor + 1; while (pos < buf.buffer.length()) { if (buf.buffer.charAt(pos) == searchChar) { setCursorPosition(pos); ok = true; break; } ++pos; } } if (ok) { if (stopBefore) moveCursor(-1); /* * When in yank-to, move-to, del-to state we actually want to * go to the character after the one we landed on to make sure * that the character we ended up on is included in the * operation */ if (isInViMoveOperationState()) { moveCursor(1); } } } else { while (count-- > 0) { int pos = buf.cursor - 1; while (pos >= 0) { if (buf.buffer.charAt(pos) == searchChar) { setCursorPosition(pos); ok = true; break; } --pos; } } if (ok && stopBefore) moveCursor(1); } return ok; } private static char switchCase(char ch) { if (Character.isUpperCase(ch)) { return Character.toLowerCase(ch); } return Character.toUpperCase(ch); } /** * @return true if line reader is in the middle of doing a change-to * delete-to or yank-to. */ private final boolean isInViMoveOperationState() { return state == State.VI_CHANGE_TO || state == State.VI_DELETE_TO || state == State.VI_YANK_TO; } /** * This is a close facsimile of the actual vi next word logic. * As with viPreviousWord() this probably needs to be improved * at some point. * * @param count number of iterations * @return true if the move was successful, false otherwise */ private boolean viNextWord(int count) throws IOException { int pos = buf.cursor; int end = buf.buffer.length(); for (int i = 0; pos < end && i < count; i++) { // Skip over letter/digits while (pos < end && !isDelimiter(buf.buffer.charAt(pos))) { ++pos; } /* * Don't you love special cases? During delete-to and yank-to * operations the word movement is normal. However, during a * change-to, the trailing spaces behind the last word are * left in tact. */ if (i < (count-1) || !(state == State.VI_CHANGE_TO)) { while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { ++pos; } } } setCursorPosition(pos); return true; } /** * Implements a close facsimile of the vi end-of-word movement. * If the character is on white space, it takes you to the end * of the next word. If it is on the last character of a word * it takes you to the next of the next word. Any other character * of a word, takes you to the end of the current word. * * @param count Number of times to repeat the action * @return true if it worked. */ private boolean viEndWord(int count) throws IOException { int pos = buf.cursor; int end = buf.buffer.length(); for (int i = 0; pos < end && i < count; i++) { if (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos)) && isDelimiter(buf.buffer.charAt (pos+1))) { ++pos; } // If we are on white space, then move back. while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { ++pos; } while (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos+1))) { ++pos; } } setCursorPosition(pos); return true; } private boolean previousWord() throws IOException { while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { // nothing } while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { // nothing } return true; } private boolean nextWord() throws IOException { while (isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { // nothing } while (!isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { // nothing } return true; } /** * Deletes to the beginning of the word that the cursor is sitting on. * If the cursor is on white-space, it deletes that and to the beginning * of the word before it. If the user is not on a word or whitespace * it deletes up to the end of the previous word. * * @param count Number of times to perform the operation * @return true if it worked, false if you tried to delete too many words */ private boolean unixWordRubout(int count) throws IOException { boolean success = true; StringBuilder killed = new StringBuilder(); for (; count > 0; --count) { if (buf.cursor == 0) { success = false; break; } while (isWhitespace(buf.current())) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } while (!isWhitespace(buf.current())) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return success; } private String insertComment(boolean isViMode) throws IOException { String comment = this.getCommentBegin(); setCursorPosition(0); putString(comment); if (isViMode) { consoleKeys.setKeyMap(KeyMap.VI_INSERT); } return accept(); } /** * Implements vi search ("/" or "?"). */ @SuppressWarnings("fallthrough") private int viSearch(char searchChar) throws IOException { boolean isForward = (searchChar == '/'); /* * This is a little gross, I'm sure there is a more appropriate way * of saving and restoring state. */ CursorBuffer origBuffer = buf.copy(); // Clear the contents of the current line and setCursorPosition (0); killLine(); // Our new "prompt" is the character that got us into search mode. putString(Character.toString(searchChar)); flush(); boolean isAborted = false; boolean isComplete = false; /* * Readline doesn't seem to do any special character map handling * here, so I think we are safe. */ int ch = -1; while (!isAborted && !isComplete && (ch = readCharacter()) != -1) { switch (ch) { case '\033': // ESC /* * The ESC behavior doesn't appear to be readline behavior, * but it is a little tweak of my own. I like it. */ isAborted = true; break; case '\010': // Backspace case '\177': // Delete backspace(); /* * Backspacing through the "prompt" aborts the search. */ if (buf.cursor == 0) { isAborted = true; } break; case '\012': // NL case '\015': // CR isComplete = true; break; default: putString(Character.toString((char) ch)); } flush(); } // If we aborted, then put ourself at the end of the original buffer. if (ch == -1 || isAborted) { setCursorPosition(0); killLine(); putString(origBuffer.buffer); setCursorPosition(origBuffer.cursor); return -1; } /* * The first character of the buffer was the search character itself * so we discard it. */ String searchTerm = buf.buffer.substring(1); int idx = -1; /* * The semantics of the history thing is gross when you want to * explicitly iterate over entries (without an iterator) as size() * returns the actual number of entries in the list but get() * doesn't work the way you think. */ int end = history.index(); int start = (end <= history.size()) ? 0 : end - history.size(); if (isForward) { for (int i = start; i < end; i++) { if (history.get(i).toString().contains(searchTerm)) { idx = i; break; } } } else { for (int i = end-1; i >= start; i--) { if (history.get(i).toString().contains(searchTerm)) { idx = i; break; } } } /* * No match? Then restore what we were working on, but make sure * the cursor is at the beginning of the line. */ if (idx == -1) { setCursorPosition(0); killLine(); putString(origBuffer.buffer); setCursorPosition(0); return -1; } /* * Show the match. */ setCursorPosition(0); killLine(); putString(history.get(idx)); setCursorPosition(0); flush(); /* * While searching really only the "n" and "N" keys are interpreted * as movement, any other key is treated as if you are editing the * line with it, so we return it back up to the caller for interpretation. */ isComplete = false; while (!isComplete && (ch = readCharacter()) != -1) { boolean forward = isForward; switch (ch) { case 'p': case 'P': forward = !isForward; // Fallthru case 'n': case 'N': boolean isMatch = false; if (forward) { for (int i = idx+1; !isMatch && i < end; i++) { if (history.get(i).toString().contains(searchTerm)) { idx = i; isMatch = true; } } } else { for (int i = idx - 1; !isMatch && i >= start; i--) { if (history.get(i).toString().contains(searchTerm)) { idx = i; isMatch = true; } } } if (isMatch) { setCursorPosition(0); killLine(); putString(history.get(idx)); setCursorPosition(0); } break; default: isComplete = true; } flush(); } /* * Complete? */ return ch; } public void setParenBlinkTimeout(int timeout) { parenBlinkTimeout = timeout; } private void insertClose(String s) throws IOException { putString(s); int closePosition = buf.cursor; moveCursor(-1); viMatch(); if (in.isNonBlockingEnabled()) { in.peek(parenBlinkTimeout); } setCursorPosition(closePosition); flush(); } /** * Implements vi style bracket matching ("%" command). The matching * bracket for the current bracket type that you are sitting on is matched. * The logic works like so: * @return true if it worked, false if the cursor was not on a bracket * character or if there was no matching bracket. */ private boolean viMatch() throws IOException { int pos = buf.cursor; if (pos == buf.length()) { return false; } int type = getBracketType(buf.buffer.charAt (pos)); int move = (type < 0) ? -1 : 1; int count = 1; if (type == 0) return false; while (count > 0) { pos += move; // Fell off the start or end. if (pos < 0 || pos >= buf.buffer.length ()) { return false; } int curType = getBracketType(buf.buffer.charAt (pos)); if (curType == type) { ++count; } else if (curType == -type) { --count; } } /* * Slight adjustment for delete-to, yank-to, change-to to ensure * that the matching paren is consumed */ if (move > 0 && isInViMoveOperationState()) ++pos; setCursorPosition(pos); flush(); return true; } /** * Given a character determines what type of bracket it is (paren, * square, curly, or none). * @param ch The character to check * @return 1 is square, 2 curly, 3 parent, or zero for none. The value * will be negated if it is the closing form of the bracket. */ private static int getBracketType (char ch) { switch (ch) { case '[': return 1; case ']': return -1; case '{': return 2; case '}': return -2; case '(': return 3; case ')': return -3; default: return 0; } } private boolean deletePreviousWord() throws IOException { StringBuilder killed = new StringBuilder(); char c; while (isDelimiter((c = buf.current()))) { if (c == 0) { break; } killed.append(c); backspace(); } while (!isDelimiter((c = buf.current()))) { if (c == 0) { break; } killed.append(c); backspace(); } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return true; } private boolean deleteNextWord() throws IOException { StringBuilder killed = new StringBuilder(); char c; while (isDelimiter((c = buf.nextChar()))) { if (c == 0) { break; } killed.append(c); delete(); } while (!isDelimiter((c = buf.nextChar()))) { if (c == 0) { break; } killed.append(c); delete(); } String copy = killed.toString(); killRing.add(copy); return true; } private boolean capitalizeWord() throws IOException { boolean first = true; int i = 1; char c; while (buf.cursor + i - 1< buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, first ? Character.toUpperCase(c) : Character.toLowerCase(c)); first = false; i++; } drawBuffer(); moveCursor(i - 1); return true; } private boolean upCaseWord() throws IOException { int i = 1; char c; while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, Character.toUpperCase(c)); i++; } drawBuffer(); moveCursor(i - 1); return true; } private boolean downCaseWord() throws IOException { int i = 1; char c; while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, Character.toLowerCase(c)); i++; } drawBuffer(); moveCursor(i - 1); return true; } /** * Performs character transpose. The character prior to the cursor and the * character under the cursor are swapped and the cursor is advanced one * character unless you are already at the end of the line. * * @param count The number of times to perform the transpose * @return true if the operation succeeded, false otherwise (e.g. transpose * cannot happen at the beginning of the line). */ private boolean transposeChars(int count) throws IOException { for (; count > 0; --count) { if (buf.cursor == 0 || buf.cursor == buf.buffer.length()) { return false; } int first = buf.cursor-1; int second = buf.cursor; char tmp = buf.buffer.charAt (first); buf.buffer.setCharAt(first, buf.buffer.charAt(second)); buf.buffer.setCharAt(second, tmp); // This could be done more efficiently by only re-drawing at the end. moveInternal(-1); drawBuffer(); moveInternal(2); } return true; } public boolean isKeyMap(String name) { // Current keymap. KeyMap map = consoleKeys.getKeys(); KeyMap mapByName = consoleKeys.getKeyMaps().get(name); if (mapByName == null) return false; /* * This may not be safe to do, but there doesn't appear to be a * clean way to find this information out. */ return map == mapByName; } /** * The equivalent of hitting <RET>. The line is considered * complete and is returned. * * @return The completed line of text. */ public String accept() throws IOException { moveToEnd(); println(); // output newline flush(); return finishBuffer(); } private void abort() throws IOException { beep(); buf.clear(); println(); redrawLine(); } /** * Move the cursor where characters. * * @param num If less than 0, move abs(where) to the left, otherwise move where to the right. * @return The number of spaces we moved */ public int moveCursor(final int num) throws IOException { int where = num; if ((buf.cursor == 0) && (where <= 0)) { return 0; } if ((buf.cursor == buf.buffer.length()) && (where >= 0)) { return 0; } if ((buf.cursor + where) < 0) { where = -buf.cursor; } else if ((buf.cursor + where) > buf.buffer.length()) { where = buf.buffer.length() - buf.cursor; } moveInternal(where); return where; } /** * Move the cursor where characters, without checking the current buffer. * * @param where the number of characters to move to the right or left. */ private void moveInternal(final int where) throws IOException { // debug ("move cursor " + where + " (" // + buf.cursor + " => " + (buf.cursor + where) + ")"); buf.cursor += where; int i0; int i1; if (mask == null) { if (where < 0) { i1 = promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen); i0 = i1 + wcwidth(buf.buffer, buf.cursor, buf.cursor - where, i1); } else { i0 = promptLen + wcwidth(buf.buffer, 0, buf.cursor - where, promptLen); i1 = i0 + wcwidth(buf.buffer, buf.cursor - where, buf.cursor, i0); } } else if (mask != NULL_MASK) { i1 = promptLen + buf.cursor; i0 = i1 - where; } else { return; } moveCursorFromTo(i0, i1); } private void moveCursorFromTo(int i0, int i1) throws IOException { if (i0 == i1) return; int width = getTerminal().getWidth(); int l0 = i0 / width; int c0 = i0 % width; int l1 = i1 / width; int c1 = i1 % width; if (l0 == l1 + 1) { if (!tputs("cursor_up")) { tputs("parm_up_cursor", 1); } } else if (l0 > l1) { if (!tputs("parm_up_cursor", l0 - l1)) { for (int i = l1; i < l0; i++) { tputs("cursor_up"); } } } else if (l0 < l1) { tputs("carriage_return"); rawPrint('\n', l1 - l0); c0 = 0; } if (c0 == c1 - 1) { tputs("cursor_right"); } else if (c0 == c1 + 1) { tputs("cursor_left"); } else if (c0 < c1) { if (!tputs("parm_right_cursor", c1 - c0)) { for (int i = c0; i < c1; i++) { tputs("cursor_right"); } } } else if (c0 > c1) { if (!tputs("parm_left_cursor", c0 - c1)) { for (int i = c1; i < c0; i++) { tputs("cursor_left"); } } } cursorOk = true; } /** * Read a character from the console. * * @return the character, or -1 if an EOF is received. */ public int readCharacter() throws IOException { return readCharacter(false); } /** * Read a character from the console. If boolean parameter is "true", it will check whether the keystroke was an "alt-" key combination, and * if so add 1000 to the value returned. Better way...? * * @return the character, or -1 if an EOF is received. */ public int readCharacter(boolean checkForAltKeyCombo) throws IOException { int c = reader.read(); if (c >= 0) { Log.trace("Keystroke: ", c); // clear any echo characters if (terminal.isSupported()) { clearEcho(c); } if (c == ESCAPE && checkForAltKeyCombo && in.peek(escapeTimeout) >= 32) { /* When ESC is encountered and there is a pending * character in the pushback queue, then it seems to be * an Alt-[key] combination. Is this true, cross-platform? * It's working for me on Debian GNU/Linux at the moment anyway. * I removed the "isNonBlockingEnabled" check, though it was * in the similar code in "readLine(String prompt, final Character mask)" (way down), * as I am not sure / didn't look up what it's about, and things are working so far w/o it. */ int next = reader.read(); // with research, there's probably a much cleaner way to do this, but, this is now it flags an Alt key combination for now: next = next + 1000; return next; } } return c; } /** * Clear the echoed characters for the specified character code. */ private int clearEcho(final int c) throws IOException { // if the terminal is not echoing, then ignore if (!terminal.isEchoEnabled()) { return 0; } // otherwise, clear int pos = getCursorPosition(); int num = wcwidth(c, pos); moveCursorFromTo(pos + num, pos); drawBuffer(num); return num; } public int readCharacter(final char... allowed) throws IOException { return readCharacter(false, allowed); } public int readCharacter(boolean checkForAltKeyCombo, final char... allowed) throws IOException { // if we restrict to a limited set and the current character is not in the set, then try again. char c; Arrays.sort(allowed); // always need to sort before binarySearch while (Arrays.binarySearch(allowed, c = (char) readCharacter(checkForAltKeyCombo)) < 0) { // nothing } return c; } /** * Read from the input stream and decode an operation from the key map. * * The input stream will be read character by character until a matching * binding can be found. Characters that can't possibly be matched to * any binding will be discarded. * * @param keys the KeyMap to use for decoding the input stream * @return the decoded binding or null if the end of * stream has been reached */ public Object readBinding(KeyMap keys) throws IOException { Object o; opBuffer.setLength(0); do { int c = pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop(); if (c == -1) { return null; } opBuffer.appendCodePoint(c); if (recording) { macro += new String(Character.toChars(c)); } if (quotedInsert) { o = Operation.SELF_INSERT; quotedInsert = false; } else { o = keys.getBound(opBuffer); } /* * The kill ring keeps record of whether or not the * previous command was a yank or a kill. We reset * that state here if needed. */ if (!recording && !(o instanceof KeyMap)) { if (o != Operation.YANK_POP && o != Operation.YANK) { killRing.resetLastYank(); } if (o != Operation.KILL_LINE && o != Operation.KILL_WHOLE_LINE && o != Operation.BACKWARD_KILL_WORD && o != Operation.KILL_WORD && o != Operation.UNIX_LINE_DISCARD && o != Operation.UNIX_WORD_RUBOUT) { killRing.resetLastKill(); } } if (o == Operation.DO_LOWERCASE_VERSION) { opBuffer.setLength(opBuffer.length() - 1); opBuffer.append(Character.toLowerCase((char) c)); o = keys.getBound(opBuffer); } /* * A KeyMap indicates that the key that was struck has a * number of keys that can follow it as indicated in the * map. This is used primarily for Emacs style ESC-META-x * lookups. Since more keys must follow, go back to waiting * for the next key. */ if (o instanceof KeyMap) { /* * The ESC key (#27) is special in that it is ambiguous until * you know what is coming next. The ESC could be a literal * escape, like the user entering vi-move mode, or it could * be part of a terminal control sequence. The following * logic attempts to disambiguate things in the same * fashion as regular vi or readline. * * When ESC is encountered and there is no other pending * character in the pushback queue, then attempt to peek * into the input stream (if the feature is enabled) for * 150ms. If nothing else is coming, then assume it is * not a terminal control sequence, but a raw escape. */ if (c == ESCAPE && pushBackChar.isEmpty() && in.isNonBlockingEnabled() && in.peek(escapeTimeout) == READ_EXPIRED) { o = ((KeyMap) o).getAnotherKey(); if (o == null || o instanceof KeyMap) { continue; } opBuffer.setLength(0); } else { continue; } } /* * If we didn't find a binding for the key and there is * more than one character accumulated then start checking * the largest span of characters from the beginning to * see if there is a binding for them. * * For example if our buffer has ESC,CTRL-M,C the getBound() * called previously indicated that there is no binding for * this sequence, so this then checks ESC,CTRL-M, and failing * that, just ESC. Each keystroke that is pealed off the end * during these tests is stuffed onto the pushback buffer so * they won't be lost. * * If there is no binding found, then we go back to waiting for * input. */ while (o == null && opBuffer.length() > 0) { c = opBuffer.charAt(opBuffer.length() - 1); opBuffer.setLength(opBuffer.length() - 1); Object o2 = keys.getBound(opBuffer); if (o2 instanceof KeyMap) { o = ((KeyMap) o2).getAnotherKey(); if (o == null) { continue; } else { pushBackChar.push((char) c); } } } } while (o == null || o instanceof KeyMap); return o; } public String getLastBinding() { return opBuffer.toString(); } // // Key Bindings // public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold"; // // Line Reading // /** * Read the next line and return the contents of the buffer. */ public String readLine() throws IOException { return readLine((String) null); } /** * Read the next line with the specified character mask. If null, then * characters will be echoed. If 0, then no characters will be echoed. */ public String readLine(final Character mask) throws IOException { return readLine(null, mask); } public String readLine(final String prompt) throws IOException { return readLine(prompt, null); } /** * Read a line from the in {@link InputStream}, and return the line * (without any trailing newlines). * * @param prompt The prompt to issue to the console, may be null. * @return A line that is read from the terminal, or null if there was null input (e.g., CTRL-D * was pressed). */ public String readLine(String prompt, final Character mask) throws IOException { return readLine(prompt, mask, null); } /** * Sets the current keymap by name. Supported keymaps are "emacs", * "vi-insert", "vi-move". * @param name The name of the keymap to switch to * @return true if the keymap was set, or false if the keymap is * not recognized. */ public boolean setKeyMap(String name) { return consoleKeys.setKeyMap(name); } /** * Returns the name of the current key mapping. * @return the name of the key mapping. This will be the canonical name * of the current mode of the key map and may not reflect the name that * was used with {@link #setKeyMap(String)}. */ public String getKeyMap() { return consoleKeys.getKeys().getName(); } /** * Read a line from the in {@link InputStream}, and return the line * (without any trailing newlines). * * @param prompt The prompt to issue to the console, may be null. * @return A line that is read from the terminal, or null if there was null input (e.g., CTRL-D * was pressed). */ public String readLine(String prompt, final Character mask, String buffer) throws IOException { // prompt may be null // mask may be null // buffer may be null /* * This is the accumulator for VI-mode repeat count. That is, while in * move mode, if you type 30x it will delete 30 characters. This is * where the "30" is accumulated until the command is struck. */ int repeatCount = 0; // FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice. this.mask = mask != null ? mask : this.echoCharacter; if (prompt != null) { setPrompt(prompt); } else { prompt = getPrompt(); } try { if (buffer != null) { buf.write(buffer); } if (!terminal.isSupported()) { beforeReadLine(prompt, mask); } if (buffer != null && buffer.length() > 0 || prompt != null && prompt.length() > 0) { drawLine(); out.flush(); } // if the terminal is unsupported, just use plain-java reading if (!terminal.isSupported()) { return readLineSimple(); } if (handleUserInterrupt) { terminal.disableInterruptCharacter(); } if (handleLitteralNext && (terminal instanceof UnixTerminal)) { ((UnixTerminal) terminal).disableLitteralNextCharacter(); } String originalPrompt = this.prompt; state = State.NORMAL; boolean success = true; pushBackChar.clear(); while (true) { Object o = readBinding(getKeys()); if (o == null) { return null; } int c = 0; if (opBuffer.length() > 0) { c = opBuffer.codePointBefore(opBuffer.length()); } Log.trace("Binding: ", o); // Handle macros if (o instanceof String) { String macro = (String) o; for (int i = 0; i < macro.length(); i++) { pushBackChar.push(macro.charAt(macro.length() - 1 - i)); } opBuffer.setLength(0); continue; } // Handle custom callbacks if (o instanceof ActionListener) { ((ActionListener) o).actionPerformed(null); opBuffer.setLength(0); continue; } CursorBuffer oldBuf = new CursorBuffer(); oldBuf.buffer.append(buf.buffer); oldBuf.cursor = buf.cursor; // Search mode. // // Note that we have to do this first, because if there is a command // not linked to a search command, we leave the search mode and fall // through to the normal state. if (state == State.SEARCH || state == State.FORWARD_SEARCH) { int cursorDest = -1; // TODO: check the isearch-terminators variable terminating the search switch ( ((Operation) o )) { case ABORT: state = State.NORMAL; buf.clear(); buf.write(originalBuffer.buffer); buf.cursor = originalBuffer.cursor; break; case REVERSE_SEARCH_HISTORY: state = State.SEARCH; if (searchTerm.length() == 0) { searchTerm.append(previousSearchTerm); } if (searchIndex > 0) { searchIndex = searchBackwards(searchTerm.toString(), searchIndex); } break; case FORWARD_SEARCH_HISTORY: state = State.FORWARD_SEARCH; if (searchTerm.length() == 0) { searchTerm.append(previousSearchTerm); } if (searchIndex > -1 && searchIndex < history.size() - 1) { searchIndex = searchForwards(searchTerm.toString(), searchIndex); } break; case BACKWARD_DELETE_CHAR: if (searchTerm.length() > 0) { searchTerm.deleteCharAt(searchTerm.length() - 1); if (state == State.SEARCH) { searchIndex = searchBackwards(searchTerm.toString()); } else { searchIndex = searchForwards(searchTerm.toString()); } } break; case SELF_INSERT: searchTerm.appendCodePoint(c); if (state == State.SEARCH) { searchIndex = searchBackwards(searchTerm.toString()); } else { searchIndex = searchForwards(searchTerm.toString()); } break; default: // Set buffer and cursor position to the found string. if (searchIndex != -1) { history.moveTo(searchIndex); // set cursor position to the found string cursorDest = history.current().toString().indexOf(searchTerm.toString()); } if (o != Operation.ACCEPT_LINE) { o = null; } state = State.NORMAL; break; } // if we're still in search mode, print the search status if (state == State.SEARCH || state == State.FORWARD_SEARCH) { if (searchTerm.length() == 0) { if (state == State.SEARCH) { printSearchStatus("", ""); } else { printForwardSearchStatus("", ""); } searchIndex = -1; } else { if (searchIndex == -1) { beep(); printSearchStatus(searchTerm.toString(), ""); } else if (state == State.SEARCH) { printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); } else { printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); } } } // otherwise, restore the line else { restoreLine(originalPrompt, cursorDest); } } if (state != State.SEARCH && state != State.FORWARD_SEARCH) { /* * If this is still false at the end of the switch, then * we reset our repeatCount to 0. */ boolean isArgDigit = false; /* * Every command that can be repeated a specified number * of times, needs to know how many times to repeat, so * we figure that out here. */ int count = (repeatCount == 0) ? 1 : repeatCount; /* * Default success to true. You only need to explicitly * set it if something goes wrong. */ success = true; if (o instanceof Operation) { Operation op = (Operation)o; /* * Current location of the cursor (prior to the operation). * These are used by vi *-to operation (e.g. delete-to) * so we know where we came from. */ int cursorStart = buf.cursor; State origState = state; /* * If we are on a "vi" movement based operation, then we * need to restrict the sets of inputs pretty heavily. */ if (state == State.VI_CHANGE_TO || state == State.VI_YANK_TO || state == State.VI_DELETE_TO) { op = viDeleteChangeYankToRemap(op); } switch ( op ) { case COMPLETE: // tab // There is an annoyance with tab completion in that // sometimes the user is actually pasting input in that // has physical tabs in it. This attempts to look at how // quickly a character follows the tab, if the character // follows *immediately*, we assume it is a tab literal. boolean isTabLiteral = false; if (copyPasteDetection && c == 9 && (!pushBackChar.isEmpty() || (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) { isTabLiteral = true; } if (! isTabLiteral) { success = complete(); } else { putString(opBuffer); } break; case POSSIBLE_COMPLETIONS: printCompletionCandidates(); break; case BEGINNING_OF_LINE: success = setCursorPosition(0); break; case YANK: success = yank(); break; case YANK_POP: success = yankPop(); break; case KILL_LINE: // CTRL-K success = killLine(); break; case KILL_WHOLE_LINE: success = setCursorPosition(0) && killLine(); break; case CLEAR_SCREEN: // CTRL-L success = clearScreen(); redrawLine(); break; case OVERWRITE_MODE: buf.setOverTyping(!buf.isOverTyping()); break; case SELF_INSERT: putString(opBuffer); break; case ACCEPT_LINE: return accept(); case ABORT: if (searchTerm == null) { abort(); } break; case INTERRUPT: if (handleUserInterrupt) { println(); flush(); String partialLine = buf.buffer.toString(); buf.clear(); history.moveToEnd(); throw new UserInterruptException(partialLine); } break; /* * VI_MOVE_ACCEPT_LINE is the result of an ENTER * while in move mode. This is the same as a normal * ACCEPT_LINE, except that we need to enter * insert mode as well. */ case VI_MOVE_ACCEPT_LINE: consoleKeys.setKeyMap(KeyMap.VI_INSERT); return accept(); case BACKWARD_WORD: success = previousWord(); break; case FORWARD_WORD: success = nextWord(); break; case PREVIOUS_HISTORY: success = moveHistory(false); break; /* * According to bash/readline move through history * in "vi" mode will move the cursor to the * start of the line. If there is no previous * history, then the cursor doesn't move. */ case VI_PREVIOUS_HISTORY: success = moveHistory(false, count) && setCursorPosition(0); break; case NEXT_HISTORY: success = moveHistory(true); break; /* * According to bash/readline move through history * in "vi" mode will move the cursor to the * start of the line. If there is no next history, * then the cursor doesn't move. */ case VI_NEXT_HISTORY: success = moveHistory(true, count) && setCursorPosition(0); break; case BACKWARD_DELETE_CHAR: // backspace success = backspace(); break; case EXIT_OR_DELETE_CHAR: if (buf.buffer.length() == 0) { return null; } success = deleteCurrentCharacter(); break; case DELETE_CHAR: // delete success = deleteCurrentCharacter(); break; case BACKWARD_CHAR: success = moveCursor(-(count)) != 0; break; case FORWARD_CHAR: success = moveCursor(count) != 0; break; case UNIX_LINE_DISCARD: success = resetLine(); break; case UNIX_WORD_RUBOUT: success = unixWordRubout(count); break; case BACKWARD_KILL_WORD: success = deletePreviousWord(); break; case KILL_WORD: success = deleteNextWord(); break; case BEGINNING_OF_HISTORY: success = history.moveToFirst(); if (success) { setBuffer(history.current()); } break; case END_OF_HISTORY: success = history.moveToLast(); if (success) { setBuffer(history.current()); } break; case HISTORY_SEARCH_BACKWARD: searchTerm = new StringBuffer(buf.upToCursor()); searchIndex = searchBackwards(searchTerm.toString(), history.index(), true); if (searchIndex == -1) { beep(); } else { // Maintain cursor position while searching. success = history.moveTo(searchIndex); if (success) { setBufferKeepPos(history.current()); } } break; case HISTORY_SEARCH_FORWARD: searchTerm = new StringBuffer(buf.upToCursor()); int index = history.index() + 1; if (index == history.size()) { history.moveToEnd(); setBufferKeepPos(searchTerm.toString()); } else if (index < history.size()) { searchIndex = searchForwards(searchTerm.toString(), index, true); if (searchIndex == -1) { beep(); } else { // Maintain cursor position while searching. success = history.moveTo(searchIndex); if (success) { setBufferKeepPos(history.current()); } } } break; case REVERSE_SEARCH_HISTORY: originalBuffer = new CursorBuffer(); originalBuffer.write(buf.buffer); originalBuffer.cursor = buf.cursor; if (searchTerm != null) { previousSearchTerm = searchTerm.toString(); } searchTerm = new StringBuffer(buf.buffer); state = State.SEARCH; if (searchTerm.length() > 0) { searchIndex = searchBackwards(searchTerm.toString()); if (searchIndex == -1) { beep(); } printSearchStatus(searchTerm.toString(), searchIndex > -1 ? history.get(searchIndex).toString() : ""); } else { searchIndex = -1; printSearchStatus("", ""); } break; case FORWARD_SEARCH_HISTORY: originalBuffer = new CursorBuffer(); originalBuffer.write(buf.buffer); originalBuffer.cursor = buf.cursor; if (searchTerm != null) { previousSearchTerm = searchTerm.toString(); } searchTerm = new StringBuffer(buf.buffer); state = State.FORWARD_SEARCH; if (searchTerm.length() > 0) { searchIndex = searchForwards(searchTerm.toString()); if (searchIndex == -1) { beep(); } printForwardSearchStatus(searchTerm.toString(), searchIndex > -1 ? history.get(searchIndex).toString() : ""); } else { searchIndex = -1; printForwardSearchStatus("", ""); } break; case CAPITALIZE_WORD: success = capitalizeWord(); break; case UPCASE_WORD: success = upCaseWord(); break; case DOWNCASE_WORD: success = downCaseWord(); break; case END_OF_LINE: success = moveToEnd(); break; case TAB_INSERT: putString( "\t" ); break; case RE_READ_INIT_FILE: consoleKeys.loadKeys(appName, inputrcUrl); break; case START_KBD_MACRO: recording = true; break; case END_KBD_MACRO: recording = false; macro = macro.substring(0, macro.length() - opBuffer.length()); break; case CALL_LAST_KBD_MACRO: for (int i = 0; i < macro.length(); i++) { pushBackChar.push(macro.charAt(macro.length() - 1 - i)); } opBuffer.setLength(0); break; case VI_EDITING_MODE: consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case VI_MOVEMENT_MODE: /* * If we are re-entering move mode from an * aborted yank-to, delete-to, change-to then * don't move the cursor back. The cursor is * only move on an expclit entry to movement * mode. */ if (state == State.NORMAL) { moveCursor(-1); } consoleKeys.setKeyMap(KeyMap.VI_MOVE); break; case VI_INSERTION_MODE: consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case VI_APPEND_MODE: moveCursor(1); consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case VI_APPEND_EOL: success = moveToEnd(); consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; /* * Handler for CTRL-D. Attempts to follow readline * behavior. If the line is empty, then it is an EOF * otherwise it is as if the user hit enter. */ case VI_EOF_MAYBE: if (buf.buffer.length() == 0) { return null; } return accept(); case TRANSPOSE_CHARS: success = transposeChars(count); break; case INSERT_COMMENT: return insertComment (false); case INSERT_CLOSE_CURLY: insertClose("}"); break; case INSERT_CLOSE_PAREN: insertClose(")"); break; case INSERT_CLOSE_SQUARE: insertClose("]"); break; case VI_INSERT_COMMENT: return insertComment (true); case VI_MATCH: success = viMatch (); break; case VI_SEARCH: int lastChar = viSearch(opBuffer.charAt(0)); if (lastChar != -1) { pushBackChar.push((char)lastChar); } break; case VI_ARG_DIGIT: repeatCount = (repeatCount * 10) + opBuffer.charAt(0) - '0'; isArgDigit = true; break; case VI_BEGINNING_OF_LINE_OR_ARG_DIGIT: if (repeatCount > 0) { repeatCount = (repeatCount * 10) + opBuffer.charAt(0) - '0'; isArgDigit = true; } else { success = setCursorPosition(0); } break; case VI_FIRST_PRINT: success = setCursorPosition(0) && viNextWord(1); break; case VI_PREV_WORD: success = viPreviousWord(count); break; case VI_NEXT_WORD: success = viNextWord(count); break; case VI_END_WORD: success = viEndWord(count); break; case VI_INSERT_BEG: success = setCursorPosition(0); consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case VI_RUBOUT: success = viRubout(count); break; case VI_DELETE: success = viDelete(count); break; case VI_DELETE_TO: /* * This is a weird special case. In vi * "dd" deletes the current line. So if we * get a delete-to, followed by a delete-to, * we delete the line. */ if (state == State.VI_DELETE_TO) { success = setCursorPosition(0) && killLine(); state = origState = State.NORMAL; } else { state = State.VI_DELETE_TO; } break; case VI_YANK_TO: // Similar to delete-to, a "yy" yanks the whole line. if (state == State.VI_YANK_TO) { yankBuffer = buf.buffer.toString(); state = origState = State.NORMAL; } else { state = State.VI_YANK_TO; } break; case VI_CHANGE_TO: if (state == State.VI_CHANGE_TO) { success = setCursorPosition(0) && killLine(); state = origState = State.NORMAL; consoleKeys.setKeyMap(KeyMap.VI_INSERT); } else { state = State.VI_CHANGE_TO; } break; case VI_KILL_WHOLE_LINE: success = setCursorPosition(0) && killLine(); consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case VI_PUT: success = viPut(count); break; case VI_CHAR_SEARCH: { // ';' and ',' don't need another character. They indicate repeat next or repeat prev. int searchChar = (c != ';' && c != ',') ? (pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop ()) : 0; success = viCharSearch(count, c, searchChar); } break; case VI_CHANGE_CASE: success = viChangeCase(count); break; case VI_CHANGE_CHAR: success = viChangeChar(count, pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop()); break; case VI_DELETE_TO_EOL: success = viDeleteTo(buf.cursor, buf.buffer.length(), false); break; case VI_CHANGE_TO_EOL: success = viDeleteTo(buf.cursor, buf.buffer.length(), true); consoleKeys.setKeyMap(KeyMap.VI_INSERT); break; case EMACS_EDITING_MODE: consoleKeys.setKeyMap(KeyMap.EMACS); break; case QUIT: getCursorBuffer().clear(); return accept(); case QUOTED_INSERT: quotedInsert = true; break; case PASTE_FROM_CLIPBOARD: paste(); break; default: break; } /* * If we were in a yank-to, delete-to, move-to * when this operation started, then fall back to */ if (origState != State.NORMAL) { if (origState == State.VI_DELETE_TO) { success = viDeleteTo(cursorStart, buf.cursor, false); } else if (origState == State.VI_CHANGE_TO) { success = viDeleteTo(cursorStart, buf.cursor, true); consoleKeys.setKeyMap(KeyMap.VI_INSERT); } else if (origState == State.VI_YANK_TO) { success = viYankTo(cursorStart, buf.cursor); } state = State.NORMAL; } /* * Another subtly. The check for the NORMAL state is * to ensure that we do not clear out the repeat * count when in delete-to, yank-to, or move-to modes. */ if (state == State.NORMAL && !isArgDigit) { /* * If the operation performed wasn't a vi argument * digit, then clear out the current repeatCount; */ repeatCount = 0; } if (state != State.SEARCH && state != State.FORWARD_SEARCH) { originalBuffer = null; previousSearchTerm = ""; searchTerm = null; searchIndex = -1; } } } if (!success) { beep(); } opBuffer.setLength(0); flush(); } } finally { if (!terminal.isSupported()) { afterReadLine(); } if (handleUserInterrupt) { terminal.enableInterruptCharacter(); } } } /** * Read a line for unsupported terminals. */ private String readLineSimple() throws IOException { StringBuilder buff = new StringBuilder(); if (skipLF) { skipLF = false; int i = readCharacter(); if (i == -1 || i == '\r') { return buff.toString(); } else if (i == '\n') { // ignore } else { buff.append((char) i); } } while (true) { int i = readCharacter(); if (i == -1 && buff.length() == 0) { return null; } if (i == -1 || i == '\n') { return buff.toString(); } else if (i == '\r') { skipLF = true; return buff.toString(); } else { buff.append((char) i); } } } // // Completion // private final List completers = new LinkedList(); private CompletionHandler completionHandler = new CandidateListCompletionHandler(); /** * Add the specified {@link jline.console.completer.Completer} to the list of handlers for tab-completion. * * @param completer the {@link jline.console.completer.Completer} to add * @return true if it was successfully added */ public boolean addCompleter(final Completer completer) { return completers.add(completer); } /** * Remove the specified {@link jline.console.completer.Completer} from the list of handlers for tab-completion. * * @param completer The {@link Completer} to remove * @return True if it was successfully removed */ public boolean removeCompleter(final Completer completer) { return completers.remove(completer); } /** * Returns an unmodifiable list of all the completers. */ public Collection getCompleters() { return Collections.unmodifiableList(completers); } public void setCompletionHandler(final CompletionHandler handler) { this.completionHandler = checkNotNull(handler); } public CompletionHandler getCompletionHandler() { return this.completionHandler; } /** * Use the completers to modify the buffer with the appropriate completions. * * @return true if successful */ protected boolean complete() throws IOException { // debug ("tab for (" + buf + ")"); if (completers.size() == 0) { return false; } List candidates = new LinkedList(); String bufstr = buf.buffer.toString(); int cursor = buf.cursor; int position = -1; for (Completer comp : completers) { if ((position = comp.complete(bufstr, cursor, candidates)) != -1) { break; } } return candidates.size() != 0 && getCompletionHandler().complete(this, candidates, position); } protected void printCompletionCandidates() throws IOException { // debug ("tab for (" + buf + ")"); if (completers.size() == 0) { return; } List candidates = new LinkedList(); String bufstr = buf.buffer.toString(); int cursor = buf.cursor; for (Completer comp : completers) { if (comp.complete(bufstr, cursor, candidates) != -1) { break; } } CandidateListCompletionHandler.printCandidates(this, candidates); drawLine(); } /** * The number of tab-completion candidates above which a warning will be * prompted before showing all the candidates. */ private int autoprintThreshold = Configuration.getInteger(JLINE_COMPLETION_THRESHOLD, 100); // same default as bash /** * @param threshold the number of candidates to print without issuing a warning. */ public void setAutoprintThreshold(final int threshold) { this.autoprintThreshold = threshold; } /** * @return the number of candidates to print without issuing a warning. */ public int getAutoprintThreshold() { return autoprintThreshold; } private boolean paginationEnabled; /** * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal. */ public void setPaginationEnabled(final boolean enabled) { this.paginationEnabled = enabled; } /** * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal. */ public boolean isPaginationEnabled() { return paginationEnabled; } // // History // private History history = new MemoryHistory(); public void setHistory(final History history) { this.history = history; } public History getHistory() { return history; } private boolean historyEnabled = true; /** * Whether or not to add new commands to the history buffer. */ public void setHistoryEnabled(final boolean enabled) { this.historyEnabled = enabled; } /** * Whether or not to add new commands to the history buffer. */ public boolean isHistoryEnabled() { return historyEnabled; } /** * Used in "vi" mode for argumented history move, to move a specific * number of history entries forward or back. * * @param next If true, move forward * @param count The number of entries to move * @return true if the move was successful */ private boolean moveHistory(final boolean next, int count) throws IOException { boolean ok = true; for (int i = 0; i < count && (ok = moveHistory(next)); i++) { /* empty */ } return ok; } /** * Move up or down the history tree. */ private boolean moveHistory(final boolean next) throws IOException { if (next && !history.next()) { return false; } else if (!next && !history.previous()) { return false; } setBuffer(history.current()); return true; } // // Printing // /** * Output the specified characters to the output stream without manipulating the current buffer. */ private int fmtPrint(final CharSequence buff, int cursorPos) throws IOException { return fmtPrint(buff, 0, buff.length(), cursorPos); } private int fmtPrint(final CharSequence buff, int start, int end) throws IOException { return fmtPrint(buff, start, end, getCursorPosition()); } private int fmtPrint(final CharSequence buff, int start, int end, int cursorPos) throws IOException { checkNotNull(buff); for (int i = start; i < end; i++) { char c = buff.charAt(i); if (c == '\t') { int nb = nextTabStop(cursorPos); cursorPos += nb; while (nb-- > 0) { out.write(' '); } } else if (c < 32) { out.write('^'); out.write((char) (c + '@')); cursorPos += 2; } else { int w = WCWidth.wcwidth(c); if (w > 0) { out.write(c); cursorPos += w; } } } cursorOk = false; return cursorPos; } /** * Output the specified string to the output stream (but not the buffer). */ public void print(final CharSequence s) throws IOException { rawPrint(s.toString()); } public void println(final CharSequence s) throws IOException { print(s); println(); } /** * Output a platform-dependant newline. */ public void println() throws IOException { tputs("carriage_return"); rawPrint('\n'); } /** * Raw output printing */ final void rawPrint(final int c) throws IOException { out.write(c); cursorOk = false; } final void rawPrint(final String str) throws IOException { out.write(str); cursorOk = false; } private void rawPrint(final char c, final int num) throws IOException { for (int i = 0; i < num; i++) { rawPrint(c); } } private void rawPrintln(final String s) throws IOException { rawPrint(s); println(); } // // Actions // /** * Issue a delete. * * @return true if successful */ public boolean delete() throws IOException { if (buf.cursor == buf.buffer.length()) { return false; } buf.buffer.delete(buf.cursor, buf.cursor + 1); drawBuffer(1); return true; } /** * Kill the buffer ahead of the current cursor position. * * @return true if successful */ public boolean killLine() throws IOException { int cp = buf.cursor; int len = buf.buffer.length(); if (cp >= len) { return false; } int num = len - cp; int pos = getCursorPosition(); int width = wcwidth(buf.buffer, cp, len, pos); clearAhead(width, pos); char[] killed = new char[num]; buf.buffer.getChars(cp, (cp + num), killed, 0); buf.buffer.delete(cp, (cp + num)); String copy = new String(killed); killRing.add(copy); return true; } public boolean yank() throws IOException { String yanked = killRing.yank(); if (yanked == null) { return false; } putString(yanked); return true; } public boolean yankPop() throws IOException { if (!killRing.lastYank()) { return false; } String current = killRing.yank(); if (current == null) { // This shouldn't happen. return false; } backspace(current.length()); String yanked = killRing.yankPop(); if (yanked == null) { // This shouldn't happen. return false; } putString(yanked); return true; } /** * Clear the screen by issuing the ANSI "clear screen" code. */ public boolean clearScreen() throws IOException { if (!tputs("clear_screen")) { println(); } return true; } /** * Issue an audible keyboard bell. */ public void beep() throws IOException { if (bellEnabled) { if (tputs("bell")) { // need to flush so the console actually beeps flush(); } } } /** * Paste the contents of the clipboard into the console buffer * * @return true if clipboard contents pasted */ public boolean paste() throws IOException { Clipboard clipboard; try { // May throw ugly exception on system without X clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); } catch (Exception e) { return false; } if (clipboard == null) { return false; } Transferable transferable = clipboard.getContents(null); if (transferable == null) { return false; } try { @SuppressWarnings("deprecation") Object content = transferable.getTransferData(DataFlavor.plainTextFlavor); // This fix was suggested in bug #1060649 at // http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056 // to get around the deprecated DataFlavor.plainTextFlavor, but it // raises a UnsupportedFlavorException on Mac OS X if (content == null) { try { content = new DataFlavor().getReaderForText(transferable); } catch (Exception e) { // ignore } } if (content == null) { return false; } String value; if (content instanceof Reader) { // TODO: we might want instead connect to the input stream // so we can interpret individual lines value = ""; String line; BufferedReader read = new BufferedReader((Reader) content); while ((line = read.readLine()) != null) { if (value.length() > 0) { value += "\n"; } value += line; } } else { value = content.toString(); } if (value == null) { return true; } putString(value); return true; } catch (UnsupportedFlavorException e) { Log.error("Paste failed: ", e); return false; } } /** * Adding a triggered Action allows to give another curse of action if a character passed the pre-processing. *

* Say you want to close the application if the user enter q. * addTriggerAction('q', new ActionListener(){ System.exit(0); }); would do the trick. */ public void addTriggeredAction(final char c, final ActionListener listener) { getKeys().bind(Character.toString(c), listener); } // // Formatted Output // /** * Output the specified {@link Collection} in proper columns. */ public void printColumns(final Collection items) throws IOException { if (items == null || items.isEmpty()) { return; } int width = getTerminal().getWidth(); int height = getTerminal().getHeight(); int maxWidth = 0; for (CharSequence item : items) { // we use 0 here, as we don't really support tabulations inside candidates int len = wcwidth(Ansi.stripAnsi(item.toString()), 0); maxWidth = Math.max(maxWidth, len); } maxWidth = maxWidth + 3; Log.debug("Max width: ", maxWidth); int showLines; if (isPaginationEnabled()) { showLines = height - 1; // page limit } else { showLines = Integer.MAX_VALUE; } StringBuilder buff = new StringBuilder(); int realLength = 0; for (CharSequence item : items) { if ((realLength + maxWidth) > width) { rawPrintln(buff.toString()); buff.setLength(0); realLength = 0; if (--showLines == 0) { // Overflow print(resources.getString("DISPLAY_MORE")); flush(); int c = readCharacter(); if (c == '\r' || c == '\n') { // one step forward showLines = 1; } else if (c != 'q') { // page forward showLines = height - 1; } tputs("carriage_return"); if (c == 'q') { // cancel break; } } } // NOTE: toString() is important here due to AnsiString being retarded buff.append(item.toString()); int strippedItemLength = wcwidth(Ansi.stripAnsi(item.toString()), 0); for (int i = 0; i < (maxWidth - strippedItemLength); i++) { buff.append(' '); } realLength += maxWidth; } if (buff.length() > 0) { rawPrintln(buff.toString()); } } // // Non-supported Terminal Support // private Thread maskThread; private void beforeReadLine(final String prompt, final Character mask) { if (mask != null && maskThread == null) { final String fullPrompt = "\r" + prompt + " " + " " + " " + "\r" + prompt; maskThread = new Thread() { public void run() { while (!interrupted()) { try { Writer out = getOutput(); out.write(fullPrompt); out.flush(); sleep(3); } catch (IOException e) { return; } catch (InterruptedException e) { return; } } } }; maskThread.setPriority(Thread.MAX_PRIORITY); maskThread.setDaemon(true); maskThread.start(); } } private void afterReadLine() { if (maskThread != null && maskThread.isAlive()) { maskThread.interrupt(); } maskThread = null; } /** * Erases the current line with the existing prompt, then redraws the line * with the provided prompt and buffer * @param prompt * the new prompt * @param buffer * the buffer to be drawn * @param cursorDest * where you want the cursor set when the line has been drawn. * -1 for end of line. * */ public void resetPromptLine(String prompt, String buffer, int cursorDest) throws IOException { // move cursor to end of line moveToEnd(); // backspace all text, including prompt buf.buffer.append(this.prompt); int promptLength = 0; if (this.prompt != null) { promptLength = this.prompt.length(); } buf.cursor += promptLength; setPrompt(""); backspaceAll(); setPrompt(prompt); redrawLine(); setBuffer(buffer); // move cursor to destination (-1 will move to end of line) if (cursorDest < 0) cursorDest = buffer.length(); setCursorPosition(cursorDest); flush(); } public void printSearchStatus(String searchTerm, String match) throws IOException { printSearchStatus(searchTerm, match, "(reverse-i-search)`"); } public void printForwardSearchStatus(String searchTerm, String match) throws IOException { printSearchStatus(searchTerm, match, "(i-search)`"); } private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException { String prompt = searchLabel + searchTerm + "': "; int cursorDest = match.indexOf(searchTerm); resetPromptLine(prompt, match, cursorDest); } public void restoreLine(String originalPrompt, int cursorDest) throws IOException { // TODO move cursor to matched string String prompt = lastLine(originalPrompt); String buffer = buf.buffer.toString(); resetPromptLine(prompt, buffer, cursorDest); } // // History search // /** * Search backward in history from a given position. * * @param searchTerm substring to search for. * @param startIndex the index from which on to search * @return index where this substring has been found, or -1 else. */ public int searchBackwards(String searchTerm, int startIndex) { return searchBackwards(searchTerm, startIndex, false); } /** * Search backwards in history from the current position. * * @param searchTerm substring to search for. * @return index where the substring has been found, or -1 else. */ public int searchBackwards(String searchTerm) { return searchBackwards(searchTerm, history.index()); } public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) { ListIterator it = history.entries(startIndex); while (it.hasPrevious()) { History.Entry e = it.previous(); if (startsWith) { if (e.value().toString().startsWith(searchTerm)) { return e.index(); } } else { if (e.value().toString().contains(searchTerm)) { return e.index(); } } } return -1; } /** * Search forward in history from a given position. * * @param searchTerm substring to search for. * @param startIndex the index from which on to search * @return index where this substring has been found, or -1 else. */ public int searchForwards(String searchTerm, int startIndex) { return searchForwards(searchTerm, startIndex, false); } /** * Search forwards in history from the current position. * * @param searchTerm substring to search for. * @return index where the substring has been found, or -1 else. */ public int searchForwards(String searchTerm) { return searchForwards(searchTerm, history.index()); } public int searchForwards(String searchTerm, int startIndex, boolean startsWith) { if (startIndex >= history.size()) { startIndex = history.size() - 1; } ListIterator it = history.entries(startIndex); if (searchIndex != -1 && it.hasNext()) { it.next(); } while (it.hasNext()) { History.Entry e = it.next(); if (startsWith) { if (e.value().toString().startsWith(searchTerm)) { return e.index(); } } else { if (e.value().toString().contains(searchTerm)) { return e.index(); } } } return -1; } // // Helpers // /** * Checks to see if the specified character is a delimiter. We consider a * character a delimiter if it is anything but a letter or digit. * * @param c The character to test * @return True if it is a delimiter */ private static boolean isDelimiter(final char c) { return !Character.isLetterOrDigit(c); } /** * Checks to see if a character is a whitespace character. Currently * this delegates to {@link Character#isWhitespace(char)}, however * eventually it should be hooked up so that the definition of whitespace * can be configured, as readline does. * * @param c The character to check * @return true if the character is a whitespace */ private static boolean isWhitespace(final char c) { return Character.isWhitespace (c); } private boolean tputs(String cap, Object... params) throws IOException { String str = terminal.getStringCapability(cap); if (str == null) { return false; } Curses.tputs(out, str, params); return true; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy