diff options
Diffstat (limited to 'src/src/main/java/jline/ConsoleReader.java')
-rw-r--r-- | src/src/main/java/jline/ConsoleReader.java | 1823 |
1 files changed, 1823 insertions, 0 deletions
diff --git a/src/src/main/java/jline/ConsoleReader.java b/src/src/main/java/jline/ConsoleReader.java new file mode 100644 index 0000000..18339d4 --- /dev/null +++ b/src/src/main/java/jline/ConsoleReader.java @@ -0,0 +1,1823 @@ +/* + * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + */ +package jline; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.ActionListener; + +import java.io.*; +import java.util.*; +import java.util.List; + +/** + * 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 Terminal#initializeTerminal} for convenience + * methods for issuing platform-specific setup commands. + * + * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> + */ +public class ConsoleReader implements ConsoleOperations { + + final static int TAB_WIDTH = 4; + String prompt; + private boolean useHistory = true; + private boolean usePagination = false; + public static final String CR = System.getProperty("line.separator"); + private static ResourceBundle loc = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName()); + /** + * Map that contains the operation name to keymay operation mapping. + */ + public static SortedMap KEYMAP_NAMES; + + + static { + Map names = new TreeMap(); + + names.put("MOVE_TO_BEG", new Short(MOVE_TO_BEG)); + names.put("MOVE_TO_END", new Short(MOVE_TO_END)); + names.put("PREV_CHAR", new Short(PREV_CHAR)); + names.put("NEWLINE", new Short(NEWLINE)); + names.put("KILL_LINE", new Short(KILL_LINE)); + names.put("PASTE", new Short(PASTE)); + names.put("CLEAR_SCREEN", new Short(CLEAR_SCREEN)); + names.put("NEXT_HISTORY", new Short(NEXT_HISTORY)); + names.put("PREV_HISTORY", new Short(PREV_HISTORY)); + names.put("START_OF_HISTORY", new Short(START_OF_HISTORY)); + names.put("END_OF_HISTORY", new Short(END_OF_HISTORY)); + names.put("REDISPLAY", new Short(REDISPLAY)); + names.put("KILL_LINE_PREV", new Short(KILL_LINE_PREV)); + names.put("DELETE_PREV_WORD", new Short(DELETE_PREV_WORD)); + names.put("NEXT_CHAR", new Short(NEXT_CHAR)); + names.put("REPEAT_PREV_CHAR", new Short(REPEAT_PREV_CHAR)); + names.put("SEARCH_PREV", new Short(SEARCH_PREV)); + names.put("REPEAT_NEXT_CHAR", new Short(REPEAT_NEXT_CHAR)); + names.put("SEARCH_NEXT", new Short(SEARCH_NEXT)); + names.put("PREV_SPACE_WORD", new Short(PREV_SPACE_WORD)); + names.put("TO_END_WORD", new Short(TO_END_WORD)); + names.put("REPEAT_SEARCH_PREV", new Short(REPEAT_SEARCH_PREV)); + names.put("PASTE_PREV", new Short(PASTE_PREV)); + names.put("REPLACE_MODE", new Short(REPLACE_MODE)); + names.put("SUBSTITUTE_LINE", new Short(SUBSTITUTE_LINE)); + names.put("TO_PREV_CHAR", new Short(TO_PREV_CHAR)); + names.put("NEXT_SPACE_WORD", new Short(NEXT_SPACE_WORD)); + names.put("DELETE_PREV_CHAR", new Short(DELETE_PREV_CHAR)); + names.put("ADD", new Short(ADD)); + names.put("PREV_WORD", new Short(PREV_WORD)); + names.put("CHANGE_META", new Short(CHANGE_META)); + names.put("DELETE_META", new Short(DELETE_META)); + names.put("END_WORD", new Short(END_WORD)); + names.put("NEXT_CHAR", new Short(NEXT_CHAR)); + names.put("INSERT", new Short(INSERT)); + names.put("REPEAT_SEARCH_NEXT", new Short(REPEAT_SEARCH_NEXT)); + names.put("PASTE_NEXT", new Short(PASTE_NEXT)); + names.put("REPLACE_CHAR", new Short(REPLACE_CHAR)); + names.put("SUBSTITUTE_CHAR", new Short(SUBSTITUTE_CHAR)); + names.put("TO_NEXT_CHAR", new Short(TO_NEXT_CHAR)); + names.put("UNDO", new Short(UNDO)); + names.put("NEXT_WORD", new Short(NEXT_WORD)); + names.put("DELETE_NEXT_CHAR", new Short(DELETE_NEXT_CHAR)); + names.put("CHANGE_CASE", new Short(CHANGE_CASE)); + names.put("COMPLETE", new Short(COMPLETE)); + names.put("EXIT", new Short(EXIT)); + names.put("CLEAR_LINE", new Short(CLEAR_LINE)); + names.put("ABORT", new Short(ABORT)); + + KEYMAP_NAMES = new TreeMap(Collections.unmodifiableMap(names)); + } + /** + * The map for logical operations. + */ + private final short[] keybindings; + /** + * If true, issue an audible keyboard bell when appropriate. + */ + private boolean bellEnabled = true; + /** + * The current character mask. + */ + private Character mask = null; + /** + * The null mask. + */ + private static final Character NULL_MASK = new Character((char) 0); + /** + * The number of tab-completion candidates above which a warning will be + * prompted before showing all the candidates. + */ + private int autoprintThreshhold = Integer.getInteger( + "jline.completion.threshold", 100).intValue(); // same default as + + // bash + /** + * The Terminal to use. + */ + private final Terminal terminal; + private CompletionHandler completionHandler = new CandidateListCompletionHandler(); + InputStream in; + final Writer out; + final CursorBuffer buf = new CursorBuffer(); + static PrintWriter debugger; + History history = new History(); + final List completors = new LinkedList(); + private Character echoCharacter = null; + private Map triggeredActions = new HashMap(); + + private StringBuffer searchTerm = null; + private String previousSearchTerm = ""; + private int searchIndex = -1; + + /** + * Adding a triggered Action allows to give another course of action + * if a character passed the preprocessing. + * + * Say you want to close the application if the user enter q. + * addTriggerAction('q', new ActionListener(){ System.exit(0); }); + * would do the trick. + * + * @param c + * @param listener + */ + public void addTriggeredAction(char c, ActionListener listener) { + triggeredActions.put(new Character(c), listener); + } + + /** + * Create a new reader using {@link FileDescriptor#in} for input and + * {@link System#out} for output. {@link FileDescriptor#in} is used because + * it has a better chance of being unbuffered. + */ + public ConsoleReader() throws IOException { + this(new FileInputStream(FileDescriptor.in), + new PrintWriter( + new OutputStreamWriter(System.out, + System.getProperty("jline.WindowsTerminal.output.encoding", System.getProperty("file.encoding"))))); + } + + /** + * Create a new reader using the specified {@link InputStream} for input and + * the specific writer for output, using the default keybindings resource. + */ + public ConsoleReader(final InputStream in, final Writer out) + throws IOException { + this(in, out, null); + } + + public ConsoleReader(final InputStream in, final Writer out, + final InputStream bindings) throws IOException { + this(in, out, bindings, Terminal.getTerminal()); + } + + /** + * Create a new reader. + * + * @param in + * the input + * @param out + * the output + * @param bindings + * the key bindings to use + * @param term + * the terminal to use + */ + public ConsoleReader(InputStream in, Writer out, InputStream bindings, + Terminal term) throws IOException { + this.terminal = term; + setInput(in); + this.out = out; + if (bindings == null) { + try { + String bindingFile = System.getProperty("jline.keybindings", + new File(System.getProperty("user.home"), + ".jlinebindings.properties").getAbsolutePath()); + + if (new File(bindingFile).isFile()) { + bindings = new FileInputStream(new File(bindingFile)); + } + } catch (Exception e) { + // swallow exceptions with option debugging + if (debugger != null) { + e.printStackTrace(debugger); + } + } + } + + if (bindings == null) { + bindings = terminal.getDefaultBindings(); + } + + this.keybindings = new short[Character.MAX_VALUE * 2]; + + Arrays.fill(this.keybindings, UNKNOWN); + + /** + * Loads the key bindings. Bindings file is in the format: + * + * keycode: operation name + */ + if (bindings != null) { + Properties p = new Properties(); + p.load(bindings); + bindings.close(); + + for (Iterator i = p.keySet().iterator(); i.hasNext();) { + String val = (String) i.next(); + + try { + Short code = new Short(val); + String op = (String) p.getProperty(val); + + Short opval = (Short) KEYMAP_NAMES.get(op); + + if (opval != null) { + keybindings[code.shortValue()] = opval.shortValue(); + } + } catch (NumberFormatException nfe) { + consumeException(nfe); + } + } + + // hardwired arrow key bindings + // keybindings[VK_UP] = PREV_HISTORY; + // keybindings[VK_DOWN] = NEXT_HISTORY; + // keybindings[VK_LEFT] = PREV_CHAR; + // keybindings[VK_RIGHT] = NEXT_CHAR; + } + } + + public Terminal getTerminal() { + return this.terminal; + } + + /** + * Set the stream for debugging. Development use only. + */ + public void setDebug(final PrintWriter debugger) { + ConsoleReader.debugger = debugger; + } + + /** + * Set the stream to be used for console input. + */ + public void setInput(final InputStream in) { + this.in = in; + } + + /** + * Returns the stream used for console input. + */ + public InputStream getInput() { + return this.in; + } + + /** + * 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); + } + + /** + * @param bellEnabled + * if true, enable audible keyboard bells if an alert is + * required. + */ + public void setBellEnabled(final boolean bellEnabled) { + this.bellEnabled = bellEnabled; + } + + /** + * @return true is audible keyboard bell is enabled. + */ + public boolean getBellEnabled() { + return this.bellEnabled; + } + + /** + * Query the terminal to find the current width; + * + * @see Terminal#getTerminalWidth + * @return the width of the current terminal. + */ + public int getTermwidth() { + return getTerminal().getTerminalWidth(); + } + + /** + * Query the terminal to find the current width; + * + * @see Terminal#getTerminalHeight + * + * @return the height of the current terminal. + */ + public int getTermheight() { + return getTerminal().getTerminalHeight(); + } + + /** + * @param autoprintThreshhold + * the number of candidates to print without issuing a warning. + */ + public void setAutoprintThreshhold(final int autoprintThreshhold) { + this.autoprintThreshhold = autoprintThreshhold; + } + + /** + * @return the number of candidates to print without issing a warning. + */ + public int getAutoprintThreshhold() { + return this.autoprintThreshhold; + } + + int getKeyForAction(short logicalAction) { + for (int i = 0; i < keybindings.length; i++) { + if (keybindings[i] == logicalAction) { + return i; + } + } + + return -1; + } + + /** + * Clear the echoed characters for the specified character code. + */ + int clearEcho(int c) throws IOException { + // if the terminal is not echoing, then just return... + if (!terminal.getEcho()) { + return 0; + } + + // otherwise, clear + int num = countEchoCharacters((char) c); + back(num); + drawBuffer(num); + + return num; + } + + int countEchoCharacters(char c) { + // tabs as special: we need to determine the number of spaces + // to cancel based on what out current cursor position is + if (c == 9) { + int tabstop = 8; // will this ever be different? + int position = getCursorPosition(); + + return tabstop - (position % tabstop); + } + + return getPrintableCharacters(c).length(); + } + + /** + * Return the number of characters that will be printed when the specified + * character is echoed to the screen. Adapted from cat by Torbjorn Granlund, + * as repeated in stty by David MacKenzie. + */ + StringBuffer getPrintableCharacters(char ch) { + StringBuffer sbuff = new StringBuffer(); + + if (ch >= 32) { + if (ch < 127) { + sbuff.append(ch); + } else if (ch == 127) { + sbuff.append('^'); + sbuff.append('?'); + } else { + sbuff.append('M'); + sbuff.append('-'); + + if (ch >= (128 + 32)) { + if (ch < (128 + 127)) { + sbuff.append((char) (ch - 128)); + } else { + sbuff.append('^'); + sbuff.append('?'); + } + } else { + sbuff.append('^'); + sbuff.append((char) (ch - 128 + 64)); + } + } + } else { + sbuff.append('^'); + sbuff.append((char) (ch + 64)); + } + + return sbuff; + } + + int getCursorPosition() { + // FIXME: does not handle anything but a line with a prompt + // absolute position + return getStrippedAnsiLength(prompt) + buf.cursor; + } + + /** + * Strips ANSI escape sequences starting with CSI and ending with char in range 64-126 + * @param ansiString String possibly containing ANSI codes, may be null + * @return length after stripping ANSI codes + */ + int getStrippedAnsiLength(String ansiString) { + if (ansiString == null) return 0; + boolean inAnsi = false; + int strippedLength = 0; + char[] chars = ansiString.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (!inAnsi && c == 27 && i < chars.length - 1 && chars[i+1] == '[') { + i++; // skip '[' + inAnsi = true; + } else if (inAnsi) { + if (64 <= c && c <= 126) { + inAnsi = false; + } + } else { + strippedLength++; + } + } + return strippedLength; + } + + public String readLine(final String prompt) throws IOException { + return readLine(prompt, null); + } + + /** + * The default prompt that will be issued. + */ + public void setDefaultPrompt(String prompt) { + this.prompt = prompt; + } + + /** + * The default prompt that will be issued. + */ + public String getDefaultPrompt() { + return prompt; + } + + /** + * Read a line from the <i>in</i> {@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., <i>CTRL-D</i> was pressed). + */ + public String readLine(final String prompt, final Character mask) + throws IOException { + this.mask = mask; + if (prompt != null) { + this.prompt = prompt; + } + + try { + terminal.beforeReadLine(this, this.prompt, mask); + + if ((this.prompt != null) && (this.prompt.length() > 0)) { + out.write(this.prompt); + out.flush(); + } + + // if the terminal is unsupported, just use plain-java reading + if (!terminal.isSupported()) { + return readLine(in); + } + + final int NORMAL = 1; + final int SEARCH = 2; + int state = NORMAL; + + boolean success = true; + + while (true) { + // Read next key and look up the command binding. + int[] next = readBinding(); + + if (next == null) { + return null; + } + + int c = next[0]; + int code = next[1]; + + if (c == -1) { + return null; + } + + // 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 == SEARCH) { + switch (code) { + // This doesn't work right now, it seems CTRL-G is not passed + // down correctly. :( + case ABORT: + state = NORMAL; + break; + + case SEARCH_PREV: + if (searchTerm.length() == 0) { + searchTerm.append(previousSearchTerm); + } + + if (searchIndex == -1) { + searchIndex = history.searchBackwards(searchTerm.toString()); + } else { + searchIndex = history.searchBackwards(searchTerm.toString(), searchIndex); + } + break; + + case DELETE_PREV_CHAR: + if (searchTerm.length() > 0) { + searchTerm.deleteCharAt(searchTerm.length() - 1); + searchIndex = history.searchBackwards(searchTerm.toString()); + } + break; + + case UNKNOWN: + searchTerm.appendCodePoint(c); + searchIndex = history.searchBackwards(searchTerm.toString()); + break; + + default: + // Set buffer and cursor position to the found string. + if (searchIndex != -1) { + history.setCurrentIndex(searchIndex); + setBuffer(history.current()); + buf.cursor = history.current().indexOf(searchTerm.toString()); + } + state = NORMAL; + break; + } + + // if we're still in search mode, print the search status + if (state == SEARCH) { + if (searchTerm.length() == 0) { + printSearchStatus("", ""); + } else { + if (searchIndex == -1) { + beep(); + } else { + printSearchStatus(searchTerm.toString(), history.getHistory(searchIndex)); + } + } + } + // otherwise, restore the line + else { + restoreLine(); + } + } + + if (state == NORMAL) { + switch (code) { + case EXIT: // ctrl-d + + if (buf.buffer.length() == 0) { + return null; + } + else { + success = deleteCurrentCharacter(); + } + break; + + case COMPLETE: // tab + success = complete(); + break; + + case MOVE_TO_BEG: + success = setCursorPosition(0); + break; + + case KILL_LINE: // CTRL-K + success = killLine(); + break; + + case CLEAR_SCREEN: // CTRL-L + success = clearScreen(); + break; + + case KILL_LINE_PREV: // CTRL-U + success = resetLine(); + break; + + case NEWLINE: // enter + moveToEnd(); + printNewline(); // output newline + return finishBuffer(); + + case DELETE_PREV_CHAR: // backspace + success = backspace(); + break; + + case DELETE_NEXT_CHAR: // delete + success = deleteCurrentCharacter(); + break; + + case MOVE_TO_END: + success = moveToEnd(); + break; + + case PREV_CHAR: + success = moveCursor(-1) != 0; + break; + + case NEXT_CHAR: + success = moveCursor(1) != 0; + break; + + case NEXT_HISTORY: + success = moveHistory(true); + break; + + case PREV_HISTORY: + success = moveHistory(false); + break; + + case ABORT: + case REDISPLAY: + break; + + case PASTE: + success = paste(); + break; + + case DELETE_PREV_WORD: + success = deletePreviousWord(); + break; + + case PREV_WORD: + success = previousWord(); + break; + + case NEXT_WORD: + success = nextWord(); + break; + + case START_OF_HISTORY: + success = history.moveToFirstEntry(); + if (success) { + setBuffer(history.current()); + } + break; + + case END_OF_HISTORY: + success = history.moveToLastEntry(); + if (success) { + setBuffer(history.current()); + } + break; + + case CLEAR_LINE: + moveInternal(-(buf.buffer.length())); + killLine(); + break; + + case INSERT: + buf.setOvertyping(!buf.isOvertyping()); + break; + + case SEARCH_PREV: // CTRL-R + if (searchTerm != null) { + previousSearchTerm = searchTerm.toString(); + } + searchTerm = new StringBuffer(buf.buffer); + state = SEARCH; + if (searchTerm.length() > 0) { + searchIndex = history.searchBackwards(searchTerm.toString()); + if (searchIndex == -1) { + beep(); + } + printSearchStatus(searchTerm.toString(), + searchIndex > -1 ? history.getHistory(searchIndex) : ""); + } else { + searchIndex = -1; + printSearchStatus("", ""); + } + break; + + case UNKNOWN: + default: + if (c != 0) { // ignore null chars + ActionListener action = (ActionListener) triggeredActions.get(new Character((char) c)); + if (action != null) { + action.actionPerformed(null); + } else { + putChar(c, true); + } + } else { + success = false; + } + } + + if (!(success)) { + beep(); + } + + flushConsole(); + } + } + } finally { + terminal.afterReadLine(this, this.prompt, mask); + } + } + + private String readLine(InputStream in) throws IOException { + StringBuffer buf = new StringBuffer(); + + while (true) { + int i = in.read(); + + if ((i == -1) || (i == '\n') || (i == '\r')) { + return buf.toString(); + } + + buf.append((char) i); + } + + // return new BufferedReader (new InputStreamReader (in)).readLine (); + } + + /** + * Reads the console input and returns an array of the form [raw, key + * binding]. + */ + private int[] readBinding() throws IOException { + int c = readVirtualKey(); + + if (c == -1) { + return null; + } + + // extract the appropriate key binding + short code = keybindings[c]; + + if (debugger != null) { + // debug(" translated: " + (int) c + ": " + code); + } + + return new int[]{c, code}; + } + + /** + * Move up or down the history tree. + */ + private final 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; + } + + /** + * 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 { + 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) { + } + } + + 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 = null; + + for (BufferedReader read = new BufferedReader((Reader) content); (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 ufe) { + if (debugger != null) { + debug(ufe + ""); + } + + return false; + } + } + + /** + * 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 = buf.buffer.length() - cp; + clearAhead(num); + + for (int i = 0; i < num; i++) { + buf.buffer.deleteCharAt(len - i - 1); + } + + return true; + } + + /** + * Clear the screen by issuing the ANSI "clear screen" code. + */ + public boolean clearScreen() throws IOException { + if (!terminal.isANSISupported()) { + return false; + } + + // send the ANSI code to clear the screen + printANSISequence("2J"); + + // then send the ANSI code to go to position 1,1 + printANSISequence("1;1H"); + + redrawLine(); + + return true; + } + + /** + * Use the completors to modify the buffer with the appropriate completions. + * + * @return true if successful + */ + private final boolean complete() throws IOException { + // debug ("tab for (" + buf + ")"); + if (completors.size() == 0) { + return false; + } + + List candidates = new LinkedList(); + String bufstr = buf.buffer.toString(); + int cursor = buf.cursor; + + int position = -1; + + for (Iterator i = completors.iterator(); i.hasNext();) { + Completor comp = (Completor) i.next(); + + if ((position = comp.complete(bufstr, cursor, candidates)) != -1) { + break; + } + } + + // no candidates? Fail. + if (candidates.size() == 0) { + return false; + } + + return completionHandler.complete(this, candidates, position); + } + + public CursorBuffer getCursorBuffer() { + return buf; + } + + /** + * Output the specified {@link Collection} in proper columns. + * + * @param stuff + * the stuff to print + */ + public void printColumns(final Collection stuff) throws IOException { + if ((stuff == null) || (stuff.size() == 0)) { + return; + } + + int width = getTermwidth(); + int maxwidth = 0; + + for (Iterator i = stuff.iterator(); i.hasNext(); maxwidth = Math.max( + maxwidth, i.next().toString().length())) { + ; + } + + StringBuffer line = new StringBuffer(); + + int showLines; + + if (usePagination) { + showLines = getTermheight() - 1; // page limit + } else { + showLines = Integer.MAX_VALUE; + } + + for (Iterator i = stuff.iterator(); i.hasNext();) { + String cur = (String) i.next(); + + if ((line.length() + maxwidth) > width) { + printString(line.toString().trim()); + printNewline(); + line.setLength(0); + if (--showLines == 0) { // Overflow + printString(loc.getString("display-more")); + flushConsole(); + int c = readVirtualKey(); + if (c == '\r' || c == '\n') { + showLines = 1; // one step forward + } else if (c != 'q') { + showLines = getTermheight() - 1; // page forward + } + back(loc.getString("display-more").length()); + if (c == 'q') { + break; // cancel + } + } + } + + pad(cur, maxwidth + 3, line); + } + + if (line.length() > 0) { + printString(line.toString().trim()); + printNewline(); + line.setLength(0); + } + } + + /** + * Append <i>toPad</i> to the specified <i>appendTo</i>, as well as (<i>toPad.length () - + * len</i>) spaces. + * + * @param toPad + * the {@link String} to pad + * @param len + * the target length + * @param appendTo + * the {@link StringBuffer} to which to append the padded + * {@link String}. + */ + private final void pad(final String toPad, final int len, + final StringBuffer appendTo) { + appendTo.append(toPad); + + for (int i = 0; i < (len - toPad.length()); i++, appendTo.append(' ')) { + ; + } + } + + /** + * Add the specified {@link Completor} to the list of handlers for + * tab-completion. + * + * @param completor + * the {@link Completor} to add + * @return true if it was successfully added + */ + public boolean addCompletor(final Completor completor) { + return completors.add(completor); + } + + /** + * Remove the specified {@link Completor} from the list of handlers for + * tab-completion. + * + * @param completor + * the {@link Completor} to remove + * @return true if it was successfully removed + */ + public boolean removeCompletor(final Completor completor) { + return completors.remove(completor); + } + + /** + * Returns an unmodifiable list of all the completors. + */ + public Collection getCompletors() { + return Collections.unmodifiableList(completors); + } + + /** + * Erase the current line. + * + * @return false if we failed (e.g., the buffer was empty) + */ + final boolean resetLine() throws IOException { + if (buf.cursor == 0) { + return false; + } + + backspaceAll(); + + return true; + } + + /** + * Move the cursor position to the specified absolute index. + */ + public final boolean setCursorPosition(final int position) + throws IOException { + 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 final 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 + } + + /** + * Clear the line and redraw it. + */ + public final void redrawLine() throws IOException { + printCharacter(RESET_LINE); + flushConsole(); + drawLine(); + } + + /** + * Output put the prompt + the current buffer + */ + public final void drawLine() throws IOException { + if (prompt != null) { + printString(prompt); + } + + printString(buf.buffer.toString()); + + if (buf.length() != buf.cursor) // not at end of line + { + back(buf.length() - buf.cursor - 1); // sync + } + } + + /** + * Output a platform-dependant newline. + */ + public final void printNewline() throws IOException { + printString(CR); + flushConsole(); + } + + /** + * Clear the buffer and add its contents to the history. + * + * @return the former contents of the buffer. + */ + final String finishBuffer() { + String str = buf.buffer.toString(); + + // 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 && useHistory) { + history.addToHistory(str); + } else { + mask = null; + } + } + + history.moveToEnd(); + + buf.buffer.setLength(0); + buf.cursor = 0; + + return str; + } + + /** + * Write out the specified string to the buffer and the output stream. + */ + public final void putString(final String str) throws IOException { + buf.write(str); + printString(str); + drawBuffer(); + } + + /** + * Output the specified string to the output stream (but not the buffer). + */ + public final void printString(final String str) throws IOException { + printCharacters(str.toCharArray()); + } + + /** + * Output the specified character, both to the buffer and the output stream. + */ + private final void putChar(final int c, final boolean print) + throws IOException { + buf.write((char) c); + + if (print) { + // no masking... + if (mask == null) { + printCharacter(c); + } // null mask: don't print anything... + else if (mask.charValue() == 0) { + ; + } // otherwise print the mask... + else { + printCharacter(mask.charValue()); + } + + 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 final void drawBuffer(final int clear) throws IOException { + // debug ("drawBuffer: " + clear); + if (buf.cursor == buf.length() && clear == 0) { + return; + } + char[] chars = buf.buffer.substring(buf.cursor).toCharArray(); + if (mask != null) { + Arrays.fill(chars, mask.charValue()); + } + + printCharacters(chars); + clearAhead(clear); + if (terminal.isANSISupported()) { + if (chars.length > 0) { + // don't ask, it seems to work + back(Math.max(chars.length - 1, 1)); + } + } else { + back(chars.length); + } + flushConsole(); + } + + /** + * Redraw the rest of the buffer from the cursor onwards. This is necessary + * for inserting text into the buffer. + */ + private final void drawBuffer() throws IOException { + drawBuffer(0); + } + + /** + * Clear ahead the specified number of characters without moving the cursor. + */ + private final void clearAhead(final int num) throws IOException { + if (num == 0) { + return; + } + + if (terminal.isANSISupported()) { + printANSISequence("J"); + return; + } + + // debug ("clearAhead: " + num); + + // print blank extra characters + printCharacters(' ', num); + + // we need to flush here so a "clever" console + // doesn't just ignore the redundancy of a space followed by + // a backspace. + flushConsole(); + + // reset the visual cursor + back(num); + + flushConsole(); + } + + /** + * Move the visual cursor backwards without modifying the buffer cursor. + */ + private final void back(final int num) throws IOException { + if (num == 0) return; + if (terminal.isANSISupported()) { + int width = getTermwidth(); + int cursor = getCursorPosition(); + // debug("back: " + cursor + " + " + num + " on " + width); + int currRow = (cursor + num) / width; + int newRow = cursor / width; + int newCol = cursor % width + 1; + // debug(" old row: " + currRow + " new row: " + newRow); + if (newRow < currRow) { + printANSISequence((currRow - newRow) + "A"); + } + printANSISequence(newCol + "G"); + flushConsole(); + return; + } + printCharacters(BACKSPACE, num); + flushConsole(); + } + + /** + * Issue an audible keyboard bell, if {@link #getBellEnabled} return true. + */ + public final void beep() throws IOException { + if (!(getBellEnabled())) { + return; + } + + printCharacter(KEYBOARD_BELL); + // need to flush so the console actually beeps + flushConsole(); + } + + /** + * Output the specified character to the output stream without manipulating + * the current buffer. + */ + private final void printCharacter(final int c) throws IOException { + if (c == '\t') { + char cbuf[] = new char[TAB_WIDTH]; + Arrays.fill(cbuf, ' '); + out.write(cbuf); + return; + } + + out.write(c); + } + + /** + * Output the specified characters to the output stream without manipulating + * the current buffer. + */ + private final void printCharacters(final char[] c) throws IOException { + int len = 0; + for (int i = 0; i < c.length; i++) { + if (c[i] == '\t') { + len += TAB_WIDTH; + } else { + len++; + } + } + + char cbuf[]; + if (len == c.length) { + cbuf = c; + } else { + cbuf = new char[len]; + int pos = 0; + for (int i = 0; i < c.length; i++) { + if (c[i] == '\t') { + Arrays.fill(cbuf, pos, pos + TAB_WIDTH, ' '); + pos += TAB_WIDTH; + } else { + cbuf[pos] = c[i]; + pos++; + } + } + } + + out.write(cbuf); + } + + private final void printCharacters(final char c, final int num) + throws IOException { + if (num == 1) { + printCharacter(c); + } else { + char[] chars = new char[num]; + Arrays.fill(chars, c); + printCharacters(chars); + } + } + + /** + * 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 immedately. + */ + public final void flushConsole() throws IOException { + out.flush(); + } + + private final int backspaceAll() throws IOException { + return backspace(Integer.MAX_VALUE); + } + + /** + * Issue <em>num</em> backspaces. + * + * @return the number of characters backed up + */ + private final int backspace(final int num) throws IOException { + if (buf.cursor == 0) { + return 0; + } + + int count = 0; + int termwidth = getTermwidth(); + int lines = getCursorPosition() / termwidth; + count = moveCursor(-1 * num) * -1; + // debug ("Deleting from " + buf.cursor + " for " + count); + buf.buffer.delete(buf.cursor, buf.cursor + count); + if (getCursorPosition() / termwidth != lines) { + if (terminal.isANSISupported()) { + // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines); + printANSISequence("J"); + flushConsole(); + } + } + drawBuffer(count); + + return count; + } + + /** + * Issue a backspace. + * + * @return true if successful + */ + public final boolean backspace() throws IOException { + return backspace(1) == 1; + } + + private final boolean moveToEnd() throws IOException { + return moveCursor(buf.length() - buf.cursor) > 0; + } + + /** + * Delete the character at the current position and redraw the remainder of + * the buffer. + */ + private final boolean deleteCurrentCharacter() throws IOException { + if (buf.length() == 0 || buf.cursor == buf.length()) { + return false; + } + + buf.buffer.deleteCharAt(buf.cursor); + drawBuffer(1); + return true; + } + + private final boolean previousWord() throws IOException { + while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { + ; + } + + while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { + ; + } + + return true; + } + + private final boolean nextWord() throws IOException { + while (isDelimiter(buf.current()) && (moveCursor(1) != 0)) { + ; + } + + while (!isDelimiter(buf.current()) && (moveCursor(1) != 0)) { + ; + } + + return true; + } + + private final boolean deletePreviousWord() throws IOException { + while (isDelimiter(buf.current()) && backspace()) { + ; + } + + while (!isDelimiter(buf.current()) && backspace()) { + ; + } + + return true; + } + + /** + * Move the cursor <i>where</i> characters. + * + * @param num + * if less than 0, move abs(<i>num</i>) to the left, + * otherwise move <i>num</i> to the right. + * + * @return the number of spaces we moved + */ + public final 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; + } + + /** + * debug. + * + * @param str + * the message to issue. + */ + public static void debug(final String str) { + if (debugger != null) { + debugger.println(str); + debugger.flush(); + } + } + + /** + * Move the cursor <i>where</i> characters, withough checking the current + * buffer. + * + * @param where + * the number of characters to move to the right or left. + */ + private final void moveInternal(final int where) throws IOException { + // debug ("move cursor " + where + " (" + // + buf.cursor + " => " + (buf.cursor + where) + ")"); + buf.cursor += where; + + if (terminal.isANSISupported()) { + if (where < 0) { + back(Math.abs(where)); + } else { + int width = getTermwidth(); + int cursor = getCursorPosition(); + int oldLine = (cursor - where) / width; + int newLine = cursor / width; + if (newLine > oldLine) { + printANSISequence((newLine - oldLine) + "B"); + } + printANSISequence(1 +(cursor % width) + "G"); + } + flushConsole(); + return; + } + + char c; + + if (where < 0) { + int len = 0; + for (int i = buf.cursor; i < buf.cursor - where; i++) { + if (buf.getBuffer().charAt(i) == '\t') { + len += TAB_WIDTH; + } else { + len++; + } + } + + char cbuf[] = new char[len]; + Arrays.fill(cbuf, BACKSPACE); + out.write(cbuf); + + return; + } else if (buf.cursor == 0) { + return; + } else if (mask != null) { + c = mask.charValue(); + } else { + printCharacters(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray()); + return; + } + + // null character mask: don't output anything + if (NULL_MASK.equals(mask)) { + return; + } + + printCharacters(c, Math.abs(where)); + } + + /** + * Read a character from the console. + * + * @return the character, or -1 if an EOF is received. + */ + public final int readVirtualKey() throws IOException { + int c = terminal.readVirtualKey(in); + + if (debugger != null) { + // debug("keystroke: " + c + ""); + } + + // clear any echo characters + clearEcho(c); + + return c; + } + + public final int readCharacter(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) readVirtualKey()) < 0); + + return c; + } + + /** + * Issue <em>num</em> deletes. + * + * @return the number of characters backed up + */ + private final int delete(final int num) + throws IOException { + /* Commented out beacuse of DWA-2949: + if (buf.cursor == 0) + return 0;*/ + + buf.buffer.delete(buf.cursor, buf.cursor + 1); + drawBuffer(1); + + return 1; + } + + public final boolean replace(int num, String replacement) { + buf.buffer.replace(buf.cursor - num, buf.cursor, replacement); + try { + moveCursor(-num); + drawBuffer(Math.max(0, num - replacement.length())); + moveCursor(replacement.length()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + /** + * Issue a delete. + * + * @return true if successful + */ + public final boolean delete() + throws IOException { + return delete(1) == 1; + } + + public void setHistory(final History history) { + this.history = history; + } + + public History getHistory() { + return this.history; + } + + public void setCompletionHandler(final CompletionHandler completionHandler) { + this.completionHandler = completionHandler; + } + + public CompletionHandler getCompletionHandler() { + return this.completionHandler; + } + + /** + * <p> + * Set the echo character. For example, to have "*" entered when a password + * is typed: + * </p> + * + * <pre> + * myConsoleReader.setEchoCharacter(new Character('*')); + * </pre> + * + * <p> + * Setting the character to + * + * <pre> + * null + * </pre> + * + * will restore normal character echoing. Setting the character to + * + * <pre> + * new Character(0) + * </pre> + * + * will cause nothing to be echoed. + * </p> + * + * @param echoCharacter + * the character to echo to the console in place of the typed + * character. + */ + public void setEchoCharacter(final Character echoCharacter) { + this.echoCharacter = echoCharacter; + } + + /** + * Returns the echo character. + */ + public Character getEchoCharacter() { + return this.echoCharacter; + } + + /** + * No-op for exceptions we want to silently consume. + */ + private void consumeException(final Throwable e) { + } + + /** + * 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 boolean isDelimiter(char c) { + return !Character.isLetterOrDigit(c); + } + + private void printANSISequence(String sequence) throws IOException { + printCharacter(27); + printCharacter('['); + printString(sequence); + flushConsole(); + } + + /* + private int currentCol, currentRow; + + private void getCurrentPosition() { + // check for ByteArrayInputStream to disable for unit tests + if (terminal.isANSISupported() && !(in instanceof ByteArrayInputStream)) { + try { + printANSISequence("[6n"); + flushConsole(); + StringBuffer b = new StringBuffer(8); + // position is sent as <ESC>[{ROW};{COLUMN}R + int r; + while((r = in.read()) > -1 && r != 'R') { + if (r != 27 && r != '[') { + b.append((char) r); + } + } + String[] pos = b.toString().split(";"); + currentRow = Integer.parseInt(pos[0]); + currentCol = Integer.parseInt(pos[1]); + } catch (Exception x) { + // no luck + currentRow = currentCol = -1; + } + } + } + */ + + /** + * Whether or not to add new commands to the history buffer. + */ + public void setUseHistory(boolean useHistory) { + this.useHistory = useHistory; + } + + /** + * Whether or not to add new commands to the history buffer. + */ + public boolean getUseHistory() { + return useHistory; + } + + /** + * Whether to use pagination when the number of rows of candidates exceeds + * the height of the temrinal. + */ + public void setUsePagination(boolean usePagination) { + this.usePagination = usePagination; + } + + /** + * Whether to use pagination when the number of rows of candidates exceeds + * the height of the temrinal. + */ + public boolean getUsePagination() { + return this.usePagination; + } + + public void printSearchStatus(String searchTerm, String match) throws IOException { + int i = match.indexOf(searchTerm); + printString("\r(reverse-i-search) `" + searchTerm + "': " + match + "\u001b[K"); + // FIXME: our ANSI using back() does not work here + printCharacters(BACKSPACE, match.length() - i); + flushConsole(); + } + + public void restoreLine() throws IOException { + printString("\u001b[2K"); // ansi/vt100 for clear whole line + redrawLine(); + flushConsole(); + } +} |