0001: /*
0002: * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
0003: *
0004: * This software is distributable under the BSD license. See the terms of the
0005: * BSD license in the documentation provided with this software.
0006: */
0007: package jline;
0008:
0009: import java.awt.*;
0010: import java.awt.datatransfer.*;
0011: import java.awt.event.ActionListener;
0012:
0013: import java.io.*;
0014: import java.util.*;
0015: import java.util.List;
0016:
0017: /**
0018: * A reader for console applications. It supports custom tab-completion,
0019: * saveable command history, and command line editing. On some platforms,
0020: * platform-specific commands will need to be issued before the reader will
0021: * function properly. See {@link Terminal#initializeTerminal} for convenience
0022: * methods for issuing platform-specific setup commands.
0023: *
0024: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
0025: */
0026: public class ConsoleReader implements ConsoleOperations {
0027:
0028: final static int TAB_WIDTH = 4;
0029:
0030: String prompt;
0031:
0032: private boolean useHistory = true;
0033:
0034: private boolean usePagination = false;
0035:
0036: public static final String CR = System
0037: .getProperty("line.separator");
0038:
0039: private static ResourceBundle loc = ResourceBundle
0040: .getBundle(CandidateListCompletionHandler.class.getName());
0041:
0042: /**
0043: * Map that contains the operation name to keymay operation mapping.
0044: */
0045: public static SortedMap KEYMAP_NAMES;
0046:
0047: static {
0048: Map names = new TreeMap();
0049:
0050: names.put("MOVE_TO_BEG", new Short(MOVE_TO_BEG));
0051: names.put("MOVE_TO_END", new Short(MOVE_TO_END));
0052: names.put("PREV_CHAR", new Short(PREV_CHAR));
0053: names.put("NEWLINE", new Short(NEWLINE));
0054: names.put("KILL_LINE", new Short(KILL_LINE));
0055: names.put("PASTE", new Short(PASTE));
0056: names.put("CLEAR_SCREEN", new Short(CLEAR_SCREEN));
0057: names.put("NEXT_HISTORY", new Short(NEXT_HISTORY));
0058: names.put("PREV_HISTORY", new Short(PREV_HISTORY));
0059: names.put("START_OF_HISTORY", new Short(START_OF_HISTORY));
0060: names.put("END_OF_HISTORY", new Short(END_OF_HISTORY));
0061: names.put("REDISPLAY", new Short(REDISPLAY));
0062: names.put("KILL_LINE_PREV", new Short(KILL_LINE_PREV));
0063: names.put("DELETE_PREV_WORD", new Short(DELETE_PREV_WORD));
0064: names.put("NEXT_CHAR", new Short(NEXT_CHAR));
0065: names.put("REPEAT_PREV_CHAR", new Short(REPEAT_PREV_CHAR));
0066: names.put("SEARCH_PREV", new Short(SEARCH_PREV));
0067: names.put("REPEAT_NEXT_CHAR", new Short(REPEAT_NEXT_CHAR));
0068: names.put("SEARCH_NEXT", new Short(SEARCH_NEXT));
0069: names.put("PREV_SPACE_WORD", new Short(PREV_SPACE_WORD));
0070: names.put("TO_END_WORD", new Short(TO_END_WORD));
0071: names.put("REPEAT_SEARCH_PREV", new Short(REPEAT_SEARCH_PREV));
0072: names.put("PASTE_PREV", new Short(PASTE_PREV));
0073: names.put("REPLACE_MODE", new Short(REPLACE_MODE));
0074: names.put("SUBSTITUTE_LINE", new Short(SUBSTITUTE_LINE));
0075: names.put("TO_PREV_CHAR", new Short(TO_PREV_CHAR));
0076: names.put("NEXT_SPACE_WORD", new Short(NEXT_SPACE_WORD));
0077: names.put("DELETE_PREV_CHAR", new Short(DELETE_PREV_CHAR));
0078: names.put("ADD", new Short(ADD));
0079: names.put("PREV_WORD", new Short(PREV_WORD));
0080: names.put("CHANGE_META", new Short(CHANGE_META));
0081: names.put("DELETE_META", new Short(DELETE_META));
0082: names.put("END_WORD", new Short(END_WORD));
0083: names.put("NEXT_CHAR", new Short(NEXT_CHAR));
0084: names.put("INSERT", new Short(INSERT));
0085: names.put("REPEAT_SEARCH_NEXT", new Short(REPEAT_SEARCH_NEXT));
0086: names.put("PASTE_NEXT", new Short(PASTE_NEXT));
0087: names.put("REPLACE_CHAR", new Short(REPLACE_CHAR));
0088: names.put("SUBSTITUTE_CHAR", new Short(SUBSTITUTE_CHAR));
0089: names.put("TO_NEXT_CHAR", new Short(TO_NEXT_CHAR));
0090: names.put("UNDO", new Short(UNDO));
0091: names.put("NEXT_WORD", new Short(NEXT_WORD));
0092: names.put("DELETE_NEXT_CHAR", new Short(DELETE_NEXT_CHAR));
0093: names.put("CHANGE_CASE", new Short(CHANGE_CASE));
0094: names.put("COMPLETE", new Short(COMPLETE));
0095: names.put("EXIT", new Short(EXIT));
0096: names.put("CLEAR_LINE", new Short(CLEAR_LINE));
0097:
0098: KEYMAP_NAMES = new TreeMap(Collections.unmodifiableMap(names));
0099: }
0100:
0101: /**
0102: * The map for logical operations.
0103: */
0104: private final short[] keybindings;
0105:
0106: /**
0107: * If true, issue an audible keyboard bell when appropriate.
0108: */
0109: private boolean bellEnabled = true;
0110:
0111: /**
0112: * The current character mask.
0113: */
0114: private Character mask = null;
0115:
0116: /**
0117: * The null mask.
0118: */
0119: private static final Character NULL_MASK = new Character((char) 0);
0120:
0121: /**
0122: * The number of tab-completion candidates above which a warning will be
0123: * prompted before showing all the candidates.
0124: */
0125: private int autoprintThreshhold = Integer.getInteger(
0126: "jline.completion.threshold", 100).intValue(); // same default as
0127:
0128: // bash
0129:
0130: /**
0131: * The Terminal to use.
0132: */
0133: private final Terminal terminal;
0134:
0135: private CompletionHandler completionHandler = new CandidateListCompletionHandler();
0136:
0137: InputStream in;
0138:
0139: final Writer out;
0140:
0141: final CursorBuffer buf = new CursorBuffer();
0142:
0143: static PrintWriter debugger;
0144:
0145: History history = new History();
0146:
0147: final List completors = new LinkedList();
0148:
0149: private Character echoCharacter = null;
0150:
0151: private Map triggeredActions = new HashMap();
0152:
0153: /**
0154: * Adding a triggered Action allows to give another curse of action
0155: * if a character passed the preprocessing.
0156: *
0157: * Say you want to close the application if the user enter q.
0158: * addTriggerAction('q', new ActionListener(){ System.exit(0); });
0159: * would do the trick.
0160: *
0161: * @param c
0162: * @param listener
0163: */
0164: public void addTriggeredAction(char c, ActionListener listener) {
0165: triggeredActions.put(new Character(c), listener);
0166: }
0167:
0168: /**
0169: * Create a new reader using {@link FileDescriptor#in} for input and
0170: * {@link System#out} for output. {@link FileDescriptor#in} is used because
0171: * it has a better chance of being unbuffered.
0172: */
0173: public ConsoleReader() throws IOException {
0174: this (new FileInputStream(FileDescriptor.in), new PrintWriter(
0175: new OutputStreamWriter(System.out, System.getProperty(
0176: "jline.WindowsTerminal.output.encoding", System
0177: .getProperty("file.encoding")))));
0178: }
0179:
0180: /**
0181: * Create a new reader using the specified {@link InputStream} for input and
0182: * the specific writer for output, using the default keybindings resource.
0183: */
0184: public ConsoleReader(final InputStream in, final Writer out)
0185: throws IOException {
0186: this (in, out, null);
0187: }
0188:
0189: public ConsoleReader(final InputStream in, final Writer out,
0190: final InputStream bindings) throws IOException {
0191: this (in, out, bindings, Terminal.getTerminal());
0192: }
0193:
0194: /**
0195: * Create a new reader.
0196: *
0197: * @param in
0198: * the input
0199: * @param out
0200: * the output
0201: * @param bindings
0202: * the key bindings to use
0203: * @param term
0204: * the terminal to use
0205: */
0206: public ConsoleReader(InputStream in, Writer out,
0207: InputStream bindings, Terminal term) throws IOException {
0208: this .terminal = term;
0209: setInput(in);
0210: this .out = out;
0211: if (bindings == null) {
0212: try {
0213: String bindingFile = System.getProperty(
0214: "jline.keybindings", new File(System
0215: .getProperty("user.home",
0216: ".jlinebindings.properties"))
0217: .getAbsolutePath());
0218:
0219: if (new File(bindingFile).isFile()) {
0220: bindings = new FileInputStream(
0221: new File(bindingFile));
0222: }
0223: } catch (Exception e) {
0224: // swallow exceptions with option debugging
0225: if (debugger != null) {
0226: e.printStackTrace(debugger);
0227: }
0228: }
0229: }
0230:
0231: if (bindings == null) {
0232: bindings = terminal.getDefaultBindings();
0233: }
0234:
0235: this .keybindings = new short[Character.MAX_VALUE * 2];
0236:
0237: Arrays.fill(this .keybindings, UNKNOWN);
0238:
0239: /**
0240: * Loads the key bindings. Bindings file is in the format:
0241: *
0242: * keycode: operation name
0243: */
0244: if (bindings != null) {
0245: Properties p = new Properties();
0246: p.load(bindings);
0247: bindings.close();
0248:
0249: for (Iterator i = p.keySet().iterator(); i.hasNext();) {
0250: String val = (String) i.next();
0251:
0252: try {
0253: Short code = new Short(val);
0254: String op = (String) p.getProperty(val);
0255:
0256: Short opval = (Short) KEYMAP_NAMES.get(op);
0257:
0258: if (opval != null) {
0259: keybindings[code.shortValue()] = opval
0260: .shortValue();
0261: }
0262: } catch (NumberFormatException nfe) {
0263: consumeException(nfe);
0264: }
0265: }
0266:
0267: // hardwired arrow key bindings
0268: // keybindings[VK_UP] = PREV_HISTORY;
0269: // keybindings[VK_DOWN] = NEXT_HISTORY;
0270: // keybindings[VK_LEFT] = PREV_CHAR;
0271: // keybindings[VK_RIGHT] = NEXT_CHAR;
0272: }
0273: }
0274:
0275: public Terminal getTerminal() {
0276: return this .terminal;
0277: }
0278:
0279: /**
0280: * Set the stream for debugging. Development use only.
0281: */
0282: public void setDebug(final PrintWriter debugger) {
0283: ConsoleReader.debugger = debugger;
0284: }
0285:
0286: /**
0287: * Set the stream to be used for console input.
0288: */
0289: public void setInput(final InputStream in) {
0290: this .in = in;
0291: }
0292:
0293: /**
0294: * Returns the stream used for console input.
0295: */
0296: public InputStream getInput() {
0297: return this .in;
0298: }
0299:
0300: /**
0301: * Read the next line and return the contents of the buffer.
0302: */
0303: public String readLine() throws IOException {
0304: return readLine((String) null);
0305: }
0306:
0307: /**
0308: * Read the next line with the specified character mask. If null, then
0309: * characters will be echoed. If 0, then no characters will be echoed.
0310: */
0311: public String readLine(final Character mask) throws IOException {
0312: return readLine(null, mask);
0313: }
0314:
0315: /**
0316: * @param bellEnabled
0317: * if true, enable audible keyboard bells if an alert is
0318: * required.
0319: */
0320: public void setBellEnabled(final boolean bellEnabled) {
0321: this .bellEnabled = bellEnabled;
0322: }
0323:
0324: /**
0325: * @return true is audible keyboard bell is enabled.
0326: */
0327: public boolean getBellEnabled() {
0328: return this .bellEnabled;
0329: }
0330:
0331: /**
0332: * Query the terminal to find the current width;
0333: *
0334: * @see Terminal#getTerminalWidth
0335: * @return the width of the current terminal.
0336: */
0337: public int getTermwidth() {
0338: return Terminal.setupTerminal().getTerminalWidth();
0339: }
0340:
0341: /**
0342: * Query the terminal to find the current width;
0343: *
0344: * @see Terminal#getTerminalHeight
0345: *
0346: * @return the height of the current terminal.
0347: */
0348: public int getTermheight() {
0349: return Terminal.setupTerminal().getTerminalHeight();
0350: }
0351:
0352: /**
0353: * @param autoprintThreshhold
0354: * the number of candidates to print without issuing a warning.
0355: */
0356: public void setAutoprintThreshhold(final int autoprintThreshhold) {
0357: this .autoprintThreshhold = autoprintThreshhold;
0358: }
0359:
0360: /**
0361: * @return the number of candidates to print without issing a warning.
0362: */
0363: public int getAutoprintThreshhold() {
0364: return this .autoprintThreshhold;
0365: }
0366:
0367: int getKeyForAction(short logicalAction) {
0368: for (int i = 0; i < keybindings.length; i++) {
0369: if (keybindings[i] == logicalAction) {
0370: return i;
0371: }
0372: }
0373:
0374: return -1;
0375: }
0376:
0377: /**
0378: * Clear the echoed characters for the specified character code.
0379: */
0380: int clearEcho(int c) throws IOException {
0381: // if the terminal is not echoing, then just return...
0382: if (!terminal.getEcho()) {
0383: return 0;
0384: }
0385:
0386: // otherwise, clear
0387: int num = countEchoCharacters((char) c);
0388: back(num);
0389: drawBuffer(num);
0390:
0391: return num;
0392: }
0393:
0394: int countEchoCharacters(char c) {
0395: // tabs as special: we need to determine the number of spaces
0396: // to cancel based on what out current cursor position is
0397: if (c == 9) {
0398: int tabstop = 8; // will this ever be different?
0399: int position = getCursorPosition();
0400:
0401: return tabstop - (position % tabstop);
0402: }
0403:
0404: return getPrintableCharacters(c).length();
0405: }
0406:
0407: /**
0408: * Return the number of characters that will be printed when the specified
0409: * character is echoed to the screen. Adapted from cat by Torbjorn Granlund,
0410: * as repeated in stty by David MacKenzie.
0411: */
0412: StringBuffer getPrintableCharacters(char ch) {
0413: StringBuffer sbuff = new StringBuffer();
0414:
0415: if (ch >= 32) {
0416: if (ch < 127) {
0417: sbuff.append(ch);
0418: } else if (ch == 127) {
0419: sbuff.append('^');
0420: sbuff.append('?');
0421: } else {
0422: sbuff.append('M');
0423: sbuff.append('-');
0424:
0425: if (ch >= (128 + 32)) {
0426: if (ch < (128 + 127)) {
0427: sbuff.append((char) (ch - 128));
0428: } else {
0429: sbuff.append('^');
0430: sbuff.append('?');
0431: }
0432: } else {
0433: sbuff.append('^');
0434: sbuff.append((char) (ch - 128 + 64));
0435: }
0436: }
0437: } else {
0438: sbuff.append('^');
0439: sbuff.append((char) (ch + 64));
0440: }
0441:
0442: return sbuff;
0443: }
0444:
0445: int getCursorPosition() {
0446: // FIXME: does not handle anything but a line with a prompt
0447: // absolute position
0448: return ((prompt == null) ? 0 : prompt.length()) + buf.cursor;
0449: }
0450:
0451: public String readLine(final String prompt) throws IOException {
0452: return readLine(prompt, null);
0453: }
0454:
0455: /**
0456: * The default prompt that will be issued.
0457: */
0458: public void setDefaultPrompt(String prompt) {
0459: this .prompt = prompt;
0460: }
0461:
0462: /**
0463: * The default prompt that will be issued.
0464: */
0465: public String getDefaultPrompt() {
0466: return prompt;
0467: }
0468:
0469: /**
0470: * Read a line from the <i>in</i> {@link InputStream}, and return the line
0471: * (without any trailing newlines).
0472: *
0473: * @param prompt
0474: * the prompt to issue to the console, may be null.
0475: * @return a line that is read from the terminal, or null if there was null
0476: * input (e.g., <i>CTRL-D</i> was pressed).
0477: */
0478: public String readLine(final String prompt, final Character mask)
0479: throws IOException {
0480: this .mask = mask;
0481: if (prompt != null)
0482: this .prompt = prompt;
0483:
0484: try {
0485: terminal.beforeReadLine(this , this .prompt, mask);
0486:
0487: if ((this .prompt != null) && (this .prompt.length() > 0)) {
0488: out.write(this .prompt);
0489: out.flush();
0490: }
0491:
0492: // if the terminal is unsupported, just use plain-java reading
0493: if (!terminal.isSupported()) {
0494: return readLine(in);
0495: }
0496:
0497: while (true) {
0498: int[] next = readBinding();
0499:
0500: if (next == null) {
0501: return null;
0502: }
0503:
0504: int c = next[0];
0505: int code = next[1];
0506:
0507: if (c == -1) {
0508: return null;
0509: }
0510:
0511: boolean success = true;
0512:
0513: switch (code) {
0514: case EXIT: // ctrl-d
0515:
0516: if (buf.buffer.length() == 0) {
0517: return null;
0518: }
0519: break;
0520:
0521: case COMPLETE: // tab
0522: success = complete();
0523: break;
0524:
0525: case MOVE_TO_BEG:
0526: success = setCursorPosition(0);
0527: break;
0528:
0529: case KILL_LINE: // CTRL-K
0530: success = killLine();
0531: break;
0532:
0533: case CLEAR_SCREEN: // CTRL-L
0534: success = clearScreen();
0535: break;
0536:
0537: case KILL_LINE_PREV: // CTRL-U
0538: success = resetLine();
0539: break;
0540:
0541: case NEWLINE: // enter
0542: moveToEnd();
0543: printNewline(); // output newline
0544: return finishBuffer();
0545:
0546: case DELETE_PREV_CHAR: // backspace
0547: success = backspace();
0548: break;
0549:
0550: case DELETE_NEXT_CHAR: // delete
0551: success = deleteCurrentCharacter();
0552: break;
0553:
0554: case MOVE_TO_END:
0555: success = moveToEnd();
0556: break;
0557:
0558: case PREV_CHAR:
0559: success = moveCursor(-1) != 0;
0560: break;
0561:
0562: case NEXT_CHAR:
0563: success = moveCursor(1) != 0;
0564: break;
0565:
0566: case NEXT_HISTORY:
0567: success = moveHistory(true);
0568: break;
0569:
0570: case PREV_HISTORY:
0571: success = moveHistory(false);
0572: break;
0573:
0574: case REDISPLAY:
0575: break;
0576:
0577: case PASTE:
0578: success = paste();
0579: break;
0580:
0581: case DELETE_PREV_WORD:
0582: success = deletePreviousWord();
0583: break;
0584:
0585: case PREV_WORD:
0586: success = previousWord();
0587: break;
0588:
0589: case NEXT_WORD:
0590: success = nextWord();
0591: break;
0592:
0593: case START_OF_HISTORY:
0594: success = history.moveToFirstEntry();
0595: if (success)
0596: setBuffer(history.current());
0597: break;
0598:
0599: case END_OF_HISTORY:
0600: success = history.moveToLastEntry();
0601: if (success)
0602: setBuffer(history.current());
0603: break;
0604:
0605: case CLEAR_LINE:
0606: moveInternal(-(buf.buffer.length()));
0607: killLine();
0608: break;
0609:
0610: case INSERT:
0611: buf.setOvertyping(!buf.isOvertyping());
0612: break;
0613:
0614: case UNKNOWN:
0615: default:
0616: if (c != 0) { // ignore null chars
0617: ActionListener action = (ActionListener) triggeredActions
0618: .get(new Character((char) c));
0619: if (action != null)
0620: action.actionPerformed(null);
0621: else
0622: putChar(c, true);
0623: } else
0624: success = false;
0625: }
0626:
0627: if (!(success)) {
0628: beep();
0629: }
0630:
0631: flushConsole();
0632: }
0633: } finally {
0634: terminal.afterReadLine(this , this .prompt, mask);
0635: }
0636: }
0637:
0638: private String readLine(InputStream in) throws IOException {
0639: StringBuffer buf = new StringBuffer();
0640:
0641: while (true) {
0642: int i = in.read();
0643:
0644: if ((i == -1) || (i == '\n') || (i == '\r')) {
0645: return buf.toString();
0646: }
0647:
0648: buf.append((char) i);
0649: }
0650:
0651: // return new BufferedReader (new InputStreamReader (in)).readLine ();
0652: }
0653:
0654: /**
0655: * Reads the console input and returns an array of the form [raw, key
0656: * binding].
0657: */
0658: private int[] readBinding() throws IOException {
0659: int c = readVirtualKey();
0660:
0661: if (c == -1) {
0662: return null;
0663: }
0664:
0665: // extract the appropriate key binding
0666: short code = keybindings[c];
0667:
0668: if (debugger != null) {
0669: debug(" translated: " + (int) c + ": " + code);
0670: }
0671:
0672: return new int[] { c, code };
0673: }
0674:
0675: /**
0676: * Move up or down the history tree.
0677: *
0678: * @param direction
0679: * less than 0 to move up the tree, down otherwise
0680: */
0681: private final boolean moveHistory(final boolean next)
0682: throws IOException {
0683: if (next && !history.next()) {
0684: return false;
0685: } else if (!next && !history.previous()) {
0686: return false;
0687: }
0688:
0689: setBuffer(history.current());
0690:
0691: return true;
0692: }
0693:
0694: /**
0695: * Paste the contents of the clipboard into the console buffer
0696: *
0697: * @return true if clipboard contents pasted
0698: */
0699: public boolean paste() throws IOException {
0700: Clipboard clipboard;
0701: try { // May throw ugly exception on system without X
0702: clipboard = Toolkit.getDefaultToolkit()
0703: .getSystemClipboard();
0704: } catch (Exception e) {
0705: return false;
0706: }
0707:
0708: if (clipboard == null) {
0709: return false;
0710: }
0711:
0712: Transferable transferable = clipboard.getContents(null);
0713:
0714: if (transferable == null) {
0715: return false;
0716: }
0717:
0718: try {
0719: Object content = transferable
0720: .getTransferData(DataFlavor.plainTextFlavor);
0721:
0722: /*
0723: * This fix was suggested in bug #1060649 at
0724: * http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056
0725: * to get around the deprecated DataFlavor.plainTextFlavor, but it
0726: * raises a UnsupportedFlavorException on Mac OS X
0727: */
0728: if (content == null) {
0729: try {
0730: content = new DataFlavor()
0731: .getReaderForText(transferable);
0732: } catch (Exception e) {
0733: }
0734: }
0735:
0736: if (content == null) {
0737: return false;
0738: }
0739:
0740: String value;
0741:
0742: if (content instanceof Reader) {
0743: // TODO: we might want instead connect to the input stream
0744: // so we can interpret individual lines
0745: value = "";
0746:
0747: String line = null;
0748:
0749: for (BufferedReader read = new BufferedReader(
0750: (Reader) content); (line = read.readLine()) != null;) {
0751: if (value.length() > 0) {
0752: value += "\n";
0753: }
0754:
0755: value += line;
0756: }
0757: } else {
0758: value = content.toString();
0759: }
0760:
0761: if (value == null) {
0762: return true;
0763: }
0764:
0765: putString(value);
0766:
0767: return true;
0768: } catch (UnsupportedFlavorException ufe) {
0769: if (debugger != null)
0770: debug(ufe + "");
0771:
0772: return false;
0773: }
0774: }
0775:
0776: /**
0777: * Kill the buffer ahead of the current cursor position.
0778: *
0779: * @return true if successful
0780: */
0781: public boolean killLine() throws IOException {
0782: int cp = buf.cursor;
0783: int len = buf.buffer.length();
0784:
0785: if (cp >= len) {
0786: return false;
0787: }
0788:
0789: int num = buf.buffer.length() - cp;
0790: clearAhead(num);
0791:
0792: for (int i = 0; i < num; i++) {
0793: buf.buffer.deleteCharAt(len - i - 1);
0794: }
0795:
0796: return true;
0797: }
0798:
0799: /**
0800: * Clear the screen by issuing the ANSI "clear screen" code.
0801: */
0802: public boolean clearScreen() throws IOException {
0803: if (!terminal.isANSISupported()) {
0804: return false;
0805: }
0806:
0807: // send the ANSI code to clear the screen
0808: printString(((char) 27) + "[2J");
0809: flushConsole();
0810:
0811: // then send the ANSI code to go to position 1,1
0812: printString(((char) 27) + "[1;1H");
0813: flushConsole();
0814:
0815: redrawLine();
0816:
0817: return true;
0818: }
0819:
0820: /**
0821: * Use the completors to modify the buffer with the appropriate completions.
0822: *
0823: * @return true if successful
0824: */
0825: private final boolean complete() throws IOException {
0826: // debug ("tab for (" + buf + ")");
0827: if (completors.size() == 0) {
0828: return false;
0829: }
0830:
0831: List candidates = new LinkedList();
0832: String bufstr = buf.buffer.toString();
0833: int cursor = buf.cursor;
0834:
0835: int position = -1;
0836:
0837: for (Iterator i = completors.iterator(); i.hasNext();) {
0838: Completor comp = (Completor) i.next();
0839:
0840: if ((position = comp.complete(bufstr, cursor, candidates)) != -1) {
0841: break;
0842: }
0843: }
0844:
0845: // no candidates? Fail.
0846: if (candidates.size() == 0) {
0847: return false;
0848: }
0849:
0850: return completionHandler.complete(this , candidates, position);
0851: }
0852:
0853: public CursorBuffer getCursorBuffer() {
0854: return buf;
0855: }
0856:
0857: /**
0858: * Output the specified {@link Collection} in proper columns.
0859: *
0860: * @param stuff
0861: * the stuff to print
0862: */
0863: public void printColumns(final Collection stuff) throws IOException {
0864: if ((stuff == null) || (stuff.size() == 0)) {
0865: return;
0866: }
0867:
0868: int width = getTermwidth();
0869: int maxwidth = 0;
0870:
0871: for (Iterator i = stuff.iterator(); i.hasNext(); maxwidth = Math
0872: .max(maxwidth, i.next().toString().length())) {
0873: ;
0874: }
0875:
0876: StringBuffer line = new StringBuffer();
0877:
0878: int showLines;
0879:
0880: if (usePagination)
0881: showLines = getTermheight() - 1; // page limit
0882: else
0883: showLines = Integer.MAX_VALUE;
0884:
0885: for (Iterator i = stuff.iterator(); i.hasNext();) {
0886: String cur = (String) i.next();
0887:
0888: if ((line.length() + maxwidth) > width) {
0889: printString(line.toString().trim());
0890: printNewline();
0891: line.setLength(0);
0892: if (--showLines == 0) { // Overflow
0893: printString(loc.getString("display-more"));
0894: flushConsole();
0895: int c = readVirtualKey();
0896: if (c == '\r' || c == '\n')
0897: showLines = 1; // one step forward
0898: else if (c != 'q')
0899: showLines = getTermheight() - 1; // page forward
0900:
0901: back(loc.getString("display-more").length());
0902: if (c == 'q')
0903: break; // cancel
0904: }
0905: }
0906:
0907: pad(cur, maxwidth + 3, line);
0908: }
0909:
0910: if (line.length() > 0) {
0911: printString(line.toString().trim());
0912: printNewline();
0913: line.setLength(0);
0914: }
0915: }
0916:
0917: /**
0918: * Append <i>toPad</i> to the specified <i>appendTo</i>, as well as (<i>toPad.length () -
0919: * len</i>) spaces.
0920: *
0921: * @param toPad
0922: * the {@link String} to pad
0923: * @param len
0924: * the target length
0925: * @param appendTo
0926: * the {@link StringBuffer} to which to append the padded
0927: * {@link String}.
0928: */
0929: private final void pad(final String toPad, final int len,
0930: final StringBuffer appendTo) {
0931: appendTo.append(toPad);
0932:
0933: for (int i = 0; i < (len - toPad.length()); i++, appendTo
0934: .append(' ')) {
0935: ;
0936: }
0937: }
0938:
0939: /**
0940: * Add the specified {@link Completor} to the list of handlers for
0941: * tab-completion.
0942: *
0943: * @param completor
0944: * the {@link Completor} to add
0945: * @return true if it was successfully added
0946: */
0947: public boolean addCompletor(final Completor completor) {
0948: return completors.add(completor);
0949: }
0950:
0951: /**
0952: * Remove the specified {@link Completor} from the list of handlers for
0953: * tab-completion.
0954: *
0955: * @param completor
0956: * the {@link Completor} to remove
0957: * @return true if it was successfully removed
0958: */
0959: public boolean removeCompletor(final Completor completor) {
0960: return completors.remove(completor);
0961: }
0962:
0963: /**
0964: * Returns an unmodifiable list of all the completors.
0965: */
0966: public Collection getCompletors() {
0967: return Collections.unmodifiableList(completors);
0968: }
0969:
0970: /**
0971: * Erase the current line.
0972: *
0973: * @return false if we failed (e.g., the buffer was empty)
0974: */
0975: final boolean resetLine() throws IOException {
0976: if (buf.cursor == 0) {
0977: return false;
0978: }
0979:
0980: backspaceAll();
0981:
0982: return true;
0983: }
0984:
0985: /**
0986: * Move the cursor position to the specified absolute index.
0987: */
0988: public final boolean setCursorPosition(final int position)
0989: throws IOException {
0990: return moveCursor(position - buf.cursor) != 0;
0991: }
0992:
0993: /**
0994: * Set the current buffer's content to the specified {@link String}. The
0995: * visual console will be modified to show the current buffer.
0996: *
0997: * @param buffer
0998: * the new contents of the buffer.
0999: */
1000: private final void setBuffer(final String buffer)
1001: throws IOException {
1002: // don't bother modifying it if it is unchanged
1003: if (buffer.equals(buf.buffer.toString())) {
1004: return;
1005: }
1006:
1007: // obtain the difference between the current buffer and the new one
1008: int sameIndex = 0;
1009:
1010: for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1)
1011: && (i < l2); i++) {
1012: if (buffer.charAt(i) == buf.buffer.charAt(i)) {
1013: sameIndex++;
1014: } else {
1015: break;
1016: }
1017: }
1018:
1019: int diff = buf.buffer.length() - sameIndex;
1020:
1021: backspace(diff); // go back for the differences
1022: killLine(); // clear to the end of the line
1023: buf.buffer.setLength(sameIndex); // the new length
1024: putString(buffer.substring(sameIndex)); // append the differences
1025: }
1026:
1027: /**
1028: * Clear the line and redraw it.
1029: */
1030: public final void redrawLine() throws IOException {
1031: printCharacter(RESET_LINE);
1032: flushConsole();
1033: drawLine();
1034: }
1035:
1036: /**
1037: * Output put the prompt + the current buffer
1038: */
1039: public final void drawLine() throws IOException {
1040: if (prompt != null) {
1041: printString(prompt);
1042: }
1043:
1044: printString(buf.buffer.toString());
1045:
1046: if (buf.length() != buf.cursor) // not at end of line
1047: back(buf.length() - buf.cursor); // sync
1048: }
1049:
1050: /**
1051: * Output a platform-dependant newline.
1052: */
1053: public final void printNewline() throws IOException {
1054: printString(CR);
1055: flushConsole();
1056: }
1057:
1058: /**
1059: * Clear the buffer and add its contents to the history.
1060: *
1061: * @return the former contents of the buffer.
1062: */
1063: final String finishBuffer() {
1064: String str = buf.buffer.toString();
1065:
1066: // we only add it to the history if the buffer is not empty
1067: // and if mask is null, since having a mask typically means
1068: // the string was a password. We clear the mask after this call
1069: if (str.length() > 0) {
1070: if (mask == null && useHistory) {
1071: history.addToHistory(str);
1072: } else {
1073: mask = null;
1074: }
1075: }
1076:
1077: history.moveToEnd();
1078:
1079: buf.buffer.setLength(0);
1080: buf.cursor = 0;
1081:
1082: return str;
1083: }
1084:
1085: /**
1086: * Write out the specified string to the buffer and the output stream.
1087: */
1088: public final void putString(final String str) throws IOException {
1089: buf.write(str);
1090: printString(str);
1091: drawBuffer();
1092: }
1093:
1094: /**
1095: * Output the specified string to the output stream (but not the buffer).
1096: */
1097: public final void printString(final String str) throws IOException {
1098: printCharacters(str.toCharArray());
1099: }
1100:
1101: /**
1102: * Output the specified character, both to the buffer and the output stream.
1103: */
1104: private final void putChar(final int c, final boolean print)
1105: throws IOException {
1106: buf.write((char) c);
1107:
1108: if (print) {
1109: // no masking...
1110: if (mask == null) {
1111: printCharacter(c);
1112: }
1113: // null mask: don't print anything...
1114: else if (mask.charValue() == 0) {
1115: ;
1116: }
1117: // otherwise print the mask...
1118: else {
1119: printCharacter(mask.charValue());
1120: }
1121:
1122: drawBuffer();
1123: }
1124: }
1125:
1126: /**
1127: * Redraw the rest of the buffer from the cursor onwards. This is necessary
1128: * for inserting text into the buffer.
1129: *
1130: * @param clear
1131: * the number of characters to clear after the end of the buffer
1132: */
1133: private final void drawBuffer(final int clear) throws IOException {
1134: // debug ("drawBuffer: " + clear);
1135: char[] chars = buf.buffer.substring(buf.cursor).toCharArray();
1136: if (mask != null)
1137: Arrays.fill(chars, mask.charValue());
1138:
1139: printCharacters(chars);
1140:
1141: clearAhead(clear);
1142: back(chars.length);
1143: flushConsole();
1144: }
1145:
1146: /**
1147: * Redraw the rest of the buffer from the cursor onwards. This is necessary
1148: * for inserting text into the buffer.
1149: */
1150: private final void drawBuffer() throws IOException {
1151: drawBuffer(0);
1152: }
1153:
1154: /**
1155: * Clear ahead the specified number of characters without moving the cursor.
1156: */
1157: private final void clearAhead(final int num) throws IOException {
1158: if (num == 0) {
1159: return;
1160: }
1161:
1162: // debug ("clearAhead: " + num);
1163:
1164: // print blank extra characters
1165: printCharacters(' ', num);
1166:
1167: // we need to flush here so a "clever" console
1168: // doesn't just ignore the redundancy of a space followed by
1169: // a backspace.
1170: flushConsole();
1171:
1172: // reset the visual cursor
1173: back(num);
1174:
1175: flushConsole();
1176: }
1177:
1178: /**
1179: * Move the visual cursor backwards without modifying the buffer cursor.
1180: */
1181: private final void back(final int num) throws IOException {
1182: printCharacters(BACKSPACE, num);
1183: flushConsole();
1184: }
1185:
1186: /**
1187: * Issue an audible keyboard bell, if {@link #getBellEnabled} return true.
1188: */
1189: public final void beep() throws IOException {
1190: if (!(getBellEnabled())) {
1191: return;
1192: }
1193:
1194: printCharacter(KEYBOARD_BELL);
1195: // need to flush so the console actually beeps
1196: flushConsole();
1197: }
1198:
1199: /**
1200: * Output the specified character to the output stream without manipulating
1201: * the current buffer.
1202: */
1203: private final void printCharacter(final int c) throws IOException {
1204: if (c == '\t') {
1205: char cbuf[] = new char[TAB_WIDTH];
1206: Arrays.fill(cbuf, ' ');
1207: out.write(cbuf);
1208: return;
1209: }
1210:
1211: out.write(c);
1212: }
1213:
1214: /**
1215: * Output the specified characters to the output stream without manipulating
1216: * the current buffer.
1217: */
1218: private final void printCharacters(final char[] c)
1219: throws IOException {
1220: int len = 0;
1221: for (int i = 0; i < c.length; i++)
1222: if (c[i] == '\t')
1223: len += TAB_WIDTH;
1224: else
1225: len++;
1226:
1227: char cbuf[];
1228: if (len == c.length)
1229: cbuf = c;
1230: else {
1231: cbuf = new char[len];
1232: int pos = 0;
1233: for (int i = 0; i < c.length; i++) {
1234: if (c[i] == '\t') {
1235: Arrays.fill(cbuf, pos, pos + TAB_WIDTH, ' ');
1236: pos += TAB_WIDTH;
1237: } else {
1238: cbuf[pos] = c[i];
1239: pos++;
1240: }
1241: }
1242: }
1243:
1244: out.write(cbuf);
1245: }
1246:
1247: private final void printCharacters(final char c, final int num)
1248: throws IOException {
1249: if (num == 1) {
1250: printCharacter(c);
1251: } else {
1252: char[] chars = new char[num];
1253: Arrays.fill(chars, c);
1254: printCharacters(chars);
1255: }
1256: }
1257:
1258: /**
1259: * Flush the console output stream. This is important for printout out
1260: * single characters (like a backspace or keyboard) that we want the console
1261: * to handle immedately.
1262: */
1263: public final void flushConsole() throws IOException {
1264: out.flush();
1265: }
1266:
1267: private final int backspaceAll() throws IOException {
1268: return backspace(Integer.MAX_VALUE);
1269: }
1270:
1271: /**
1272: * Issue <em>num</em> backspaces.
1273: *
1274: * @return the number of characters backed up
1275: */
1276: private final int backspace(final int num) throws IOException {
1277: if (buf.cursor == 0) {
1278: return 0;
1279: }
1280:
1281: int count = 0;
1282:
1283: count = moveCursor(-1 * num) * -1;
1284: // debug ("Deleting from " + buf.cursor + " for " + count);
1285: buf.buffer.delete(buf.cursor, buf.cursor + count);
1286: drawBuffer(count);
1287:
1288: return count;
1289: }
1290:
1291: /**
1292: * Issue a backspace.
1293: *
1294: * @return true if successful
1295: */
1296: public final boolean backspace() throws IOException {
1297: return backspace(1) == 1;
1298: }
1299:
1300: private final boolean moveToEnd() throws IOException {
1301: if (moveCursor(1) == 0) {
1302: return false;
1303: }
1304:
1305: while (moveCursor(1) != 0) {
1306: ;
1307: }
1308:
1309: return true;
1310: }
1311:
1312: /**
1313: * Delete the character at the current position and redraw the remainder of
1314: * the buffer.
1315: */
1316: private final boolean deleteCurrentCharacter() throws IOException {
1317: boolean success = buf.buffer.length() > 0;
1318: if (!success) {
1319: return false;
1320: }
1321:
1322: if (buf.cursor == buf.buffer.length()) {
1323: return false;
1324: }
1325:
1326: buf.buffer.deleteCharAt(buf.cursor);
1327: drawBuffer(1);
1328: return true;
1329: }
1330:
1331: private final boolean previousWord() throws IOException {
1332: while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
1333: ;
1334: }
1335:
1336: while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
1337: ;
1338: }
1339:
1340: return true;
1341: }
1342:
1343: private final boolean nextWord() throws IOException {
1344: while (isDelimiter(buf.current()) && (moveCursor(1) != 0)) {
1345: ;
1346: }
1347:
1348: while (!isDelimiter(buf.current()) && (moveCursor(1) != 0)) {
1349: ;
1350: }
1351:
1352: return true;
1353: }
1354:
1355: private final boolean deletePreviousWord() throws IOException {
1356: while (isDelimiter(buf.current()) && backspace()) {
1357: ;
1358: }
1359:
1360: while (!isDelimiter(buf.current()) && backspace()) {
1361: ;
1362: }
1363:
1364: return true;
1365: }
1366:
1367: /**
1368: * Move the cursor <i>where</i> characters.
1369: *
1370: * @param where
1371: * if less than 0, move abs(<i>where</i>) to the left,
1372: * otherwise move <i>where</i> to the right.
1373: *
1374: * @return the number of spaces we moved
1375: */
1376: public final int moveCursor(final int num) throws IOException {
1377: int where = num;
1378:
1379: if ((buf.cursor == 0) && (where < 0)) {
1380: return 0;
1381: }
1382:
1383: if ((buf.cursor == buf.buffer.length()) && (where > 0)) {
1384: return 0;
1385: }
1386:
1387: if ((buf.cursor + where) < 0) {
1388: where = -buf.cursor;
1389: } else if ((buf.cursor + where) > buf.buffer.length()) {
1390: where = buf.buffer.length() - buf.cursor;
1391: }
1392:
1393: moveInternal(where);
1394:
1395: return where;
1396: }
1397:
1398: /**
1399: * debug.
1400: *
1401: * @param str
1402: * the message to issue.
1403: */
1404: public static void debug(final String str) {
1405: if (debugger != null) {
1406: debugger.println(str);
1407: debugger.flush();
1408: }
1409: }
1410:
1411: /**
1412: * Move the cursor <i>where</i> characters, withough checking the current
1413: * buffer.
1414: *
1415: * @see #where
1416: *
1417: * @param where
1418: * the number of characters to move to the right or left.
1419: */
1420: private final void moveInternal(final int where) throws IOException {
1421: // debug ("move cursor " + where + " ("
1422: // + buf.cursor + " => " + (buf.cursor + where) + ")");
1423: buf.cursor += where;
1424:
1425: char c;
1426:
1427: if (where < 0) {
1428: int len = 0;
1429: for (int i = buf.cursor; i < buf.cursor - where; i++) {
1430: if (buf.getBuffer().charAt(i) == '\t')
1431: len += TAB_WIDTH;
1432: else
1433: len++;
1434: }
1435:
1436: char cbuf[] = new char[len];
1437: Arrays.fill(cbuf, BACKSPACE);
1438: out.write(cbuf);
1439:
1440: return;
1441: } else if (buf.cursor == 0) {
1442: return;
1443: } else if (mask != null) {
1444: c = mask.charValue();
1445: } else {
1446: printCharacters(buf.buffer.substring(buf.cursor - where,
1447: buf.cursor).toCharArray());
1448: return;
1449: }
1450:
1451: // null character mask: don't output anything
1452: if (NULL_MASK.equals(mask)) {
1453: return;
1454: }
1455:
1456: printCharacters(c, Math.abs(where));
1457: }
1458:
1459: /**
1460: * Read a character from the console.
1461: *
1462: * @return the character, or -1 if an EOF is received.
1463: */
1464: public final int readVirtualKey() throws IOException {
1465: int c = terminal.readVirtualKey(in);
1466:
1467: if (debugger != null) {
1468: debug("keystroke: " + c + "");
1469: }
1470:
1471: // clear any echo characters
1472: clearEcho(c);
1473:
1474: return c;
1475: }
1476:
1477: public final int readCharacter(final char[] allowed)
1478: throws IOException {
1479: // if we restrict to a limited set and the current character
1480: // is not in the set, then try again.
1481: char c;
1482:
1483: Arrays.sort(allowed); // always need to sort before binarySearch
1484:
1485: while (Arrays
1486: .binarySearch(allowed, c = (char) readVirtualKey()) < 0)
1487: ;
1488:
1489: return c;
1490: }
1491:
1492: /**
1493: * Issue <em>num</em> deletes.
1494: *
1495: * @return the number of characters backed up
1496: */
1497: private final int delete(final int num) throws IOException {
1498: /* Commented out beacuse of DWA-2949:
1499: if (buf.cursor == 0)
1500: return 0;*/
1501:
1502: buf.buffer.delete(buf.cursor, buf.cursor + 1);
1503: drawBuffer(1);
1504:
1505: return 1;
1506: }
1507:
1508: public final boolean replace(int num, String replacement) {
1509: buf.buffer.replace(buf.cursor - num, buf.cursor, replacement);
1510: try {
1511: moveCursor(-num);
1512: drawBuffer(Math.max(0, num - replacement.length()));
1513: moveCursor(replacement.length());
1514: } catch (IOException e) {
1515: e.printStackTrace();
1516: return false;
1517: }
1518: return true;
1519: }
1520:
1521: /**
1522: * Issue a delete.
1523: *
1524: * @return true if successful
1525: */
1526: public final boolean delete() throws IOException {
1527: return delete(1) == 1;
1528: }
1529:
1530: public void setHistory(final History history) {
1531: this .history = history;
1532: }
1533:
1534: public History getHistory() {
1535: return this .history;
1536: }
1537:
1538: public void setCompletionHandler(
1539: final CompletionHandler completionHandler) {
1540: this .completionHandler = completionHandler;
1541: }
1542:
1543: public CompletionHandler getCompletionHandler() {
1544: return this .completionHandler;
1545: }
1546:
1547: /**
1548: * <p>
1549: * Set the echo character. For example, to have "*" entered when a password
1550: * is typed:
1551: * </p>
1552: *
1553: * <pre>
1554: * myConsoleReader.setEchoCharacter(new Character('*'));
1555: * </pre>
1556: *
1557: * <p>
1558: * Setting the character to
1559: *
1560: * <pre>
1561: * null
1562: * </pre>
1563: *
1564: * will restore normal character echoing. Setting the character to
1565: *
1566: * <pre>
1567: * new Character(0)
1568: * </pre>
1569: *
1570: * will cause nothing to be echoed.
1571: * </p>
1572: *
1573: * @param echoCharacter
1574: * the character to echo to the console in place of the typed
1575: * character.
1576: */
1577: public void setEchoCharacter(final Character echoCharacter) {
1578: this .echoCharacter = echoCharacter;
1579: }
1580:
1581: /**
1582: * Returns the echo character.
1583: */
1584: public Character getEchoCharacter() {
1585: return this .echoCharacter;
1586: }
1587:
1588: /**
1589: * No-op for exceptions we want to silently consume.
1590: */
1591: private void consumeException(final Throwable e) {
1592: }
1593:
1594: /**
1595: * Checks to see if the specified character is a delimiter. We consider a
1596: * character a delimiter if it is anything but a letter or digit.
1597: *
1598: * @param c
1599: * the character to test
1600: * @return true if it is a delimiter
1601: */
1602: private boolean isDelimiter(char c) {
1603: return !Character.isLetterOrDigit(c);
1604: }
1605:
1606: /**
1607: * Whether or not to add new commands to the history buffer.
1608: */
1609: public void setUseHistory(boolean useHistory) {
1610: this .useHistory = useHistory;
1611: }
1612:
1613: /**
1614: * Whether or not to add new commands to the history buffer.
1615: */
1616: public boolean getUseHistory() {
1617: return useHistory;
1618: }
1619:
1620: /**
1621: * Whether to use pagination when the number of rows of candidates exceeds
1622: * the height of the temrinal.
1623: */
1624: public void setUsePagination(boolean usePagination) {
1625: this .usePagination = usePagination;
1626: }
1627:
1628: /**
1629: * Whether to use pagination when the number of rows of candidates exceeds
1630: * the height of the temrinal.
1631: */
1632: public boolean getUsePagination() {
1633: return this.usePagination;
1634: }
1635:
1636: }
|