001 /* 002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved. 003 * 004 * This software is distributable under the BSD license. See the terms of the 005 * BSD license in the documentation provided with this software. 006 */ 007 package jline; 008 009 import java.awt.*; 010 import java.awt.datatransfer.*; 011 012 import java.io.*; 013 import java.util.*; 014 import java.util.List; 015 016 /** 017 * A reader for console applications. It supports custom tab-completion, 018 * saveable command history, and command line editing. On some platforms, 019 * platform-specific commands will need to be issued before the reader will 020 * function properly. See {@link Terminal#initializeTerminal} for convenience 021 * methods for issuing platform-specific setup commands. 022 * 023 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 024 */ 025 public class ConsoleReader implements ConsoleOperations { 026 String prompt; 027 028 private boolean useHistory = true; 029 030 private boolean usePagination = false; 031 032 public static final String CR = System.getProperty("line.separator"); 033 034 private static ResourceBundle loc = ResourceBundle 035 .getBundle(CandidateListCompletionHandler.class.getName()); 036 037 /** 038 * Map that contains the operation name to keymay operation mapping. 039 */ 040 public static SortedMap KEYMAP_NAMES; 041 042 static { 043 Map names = new TreeMap(); 044 045 names.put("MOVE_TO_BEG", new Short(MOVE_TO_BEG)); 046 names.put("MOVE_TO_END", new Short(MOVE_TO_END)); 047 names.put("PREV_CHAR", new Short(PREV_CHAR)); 048 names.put("NEWLINE", new Short(NEWLINE)); 049 names.put("KILL_LINE", new Short(KILL_LINE)); 050 names.put("PASTE", new Short(PASTE)); 051 names.put("CLEAR_SCREEN", new Short(CLEAR_SCREEN)); 052 names.put("NEXT_HISTORY", new Short(NEXT_HISTORY)); 053 names.put("PREV_HISTORY", new Short(PREV_HISTORY)); 054 names.put("START_OF_HISTORY", new Short(START_OF_HISTORY)); 055 names.put("END_OF_HISTORY", new Short(END_OF_HISTORY)); 056 names.put("REDISPLAY", new Short(REDISPLAY)); 057 names.put("KILL_LINE_PREV", new Short(KILL_LINE_PREV)); 058 names.put("DELETE_PREV_WORD", new Short(DELETE_PREV_WORD)); 059 names.put("NEXT_CHAR", new Short(NEXT_CHAR)); 060 names.put("REPEAT_PREV_CHAR", new Short(REPEAT_PREV_CHAR)); 061 names.put("SEARCH_PREV", new Short(SEARCH_PREV)); 062 names.put("REPEAT_NEXT_CHAR", new Short(REPEAT_NEXT_CHAR)); 063 names.put("SEARCH_NEXT", new Short(SEARCH_NEXT)); 064 names.put("PREV_SPACE_WORD", new Short(PREV_SPACE_WORD)); 065 names.put("TO_END_WORD", new Short(TO_END_WORD)); 066 names.put("REPEAT_SEARCH_PREV", new Short(REPEAT_SEARCH_PREV)); 067 names.put("PASTE_PREV", new Short(PASTE_PREV)); 068 names.put("REPLACE_MODE", new Short(REPLACE_MODE)); 069 names.put("SUBSTITUTE_LINE", new Short(SUBSTITUTE_LINE)); 070 names.put("TO_PREV_CHAR", new Short(TO_PREV_CHAR)); 071 names.put("NEXT_SPACE_WORD", new Short(NEXT_SPACE_WORD)); 072 names.put("DELETE_PREV_CHAR", new Short(DELETE_PREV_CHAR)); 073 names.put("ADD", new Short(ADD)); 074 names.put("PREV_WORD", new Short(PREV_WORD)); 075 names.put("CHANGE_META", new Short(CHANGE_META)); 076 names.put("DELETE_META", new Short(DELETE_META)); 077 names.put("END_WORD", new Short(END_WORD)); 078 names.put("NEXT_CHAR", new Short(NEXT_CHAR)); 079 names.put("INSERT", new Short(INSERT)); 080 names.put("REPEAT_SEARCH_NEXT", new Short(REPEAT_SEARCH_NEXT)); 081 names.put("PASTE_NEXT", new Short(PASTE_NEXT)); 082 names.put("REPLACE_CHAR", new Short(REPLACE_CHAR)); 083 names.put("SUBSTITUTE_CHAR", new Short(SUBSTITUTE_CHAR)); 084 names.put("TO_NEXT_CHAR", new Short(TO_NEXT_CHAR)); 085 names.put("UNDO", new Short(UNDO)); 086 names.put("NEXT_WORD", new Short(NEXT_WORD)); 087 names.put("DELETE_NEXT_CHAR", new Short(DELETE_NEXT_CHAR)); 088 names.put("CHANGE_CASE", new Short(CHANGE_CASE)); 089 names.put("COMPLETE", new Short(COMPLETE)); 090 names.put("EXIT", new Short(EXIT)); 091 names.put("CLEAR_LINE", new Short(CLEAR_LINE)); 092 093 KEYMAP_NAMES = new TreeMap(Collections.unmodifiableMap(names)); 094 } 095 096 /** 097 * The map for logical operations. 098 */ 099 private final short[] keybindings; 100 101 /** 102 * If true, issue an audible keyboard bell when appropriate. 103 */ 104 private boolean bellEnabled = true; 105 106 /** 107 * The current character mask. 108 */ 109 private Character mask = null; 110 111 /** 112 * The null mask. 113 */ 114 private static final Character NULL_MASK = new Character((char) 0); 115 116 /** 117 * The number of tab-completion candidates above which a warning will be 118 * prompted before showing all the candidates. 119 */ 120 private int autoprintThreshhold = Integer.getInteger( 121 "jline.completion.threshold", 100).intValue(); // same default as 122 123 // bash 124 125 /** 126 * The Terminal to use. 127 */ 128 private final Terminal terminal; 129 130 private CompletionHandler completionHandler = new CandidateListCompletionHandler(); 131 132 InputStream in; 133 134 final Writer out; 135 136 final CursorBuffer buf = new CursorBuffer(); 137 138 static PrintWriter debugger; 139 140 History history = new History(); 141 142 final List completors = new LinkedList(); 143 144 private Character echoCharacter = null; 145 146 /** 147 * Create a new reader using {@link FileDescriptor#in} for input and 148 * {@link System#out} for output. {@link FileDescriptor#in} is used because 149 * it has a better chance of being unbuffered. 150 */ 151 public ConsoleReader() throws IOException { 152 this(new FileInputStream(FileDescriptor.in), 153 new PrintWriter(System.out)); 154 } 155 156 /** 157 * Create a new reader using the specified {@link InputStream} for input and 158 * the specific writer for output, using the default keybindings resource. 159 */ 160 public ConsoleReader(final InputStream in, final Writer out) 161 throws IOException { 162 this(in, out, null); 163 } 164 165 public ConsoleReader(final InputStream in, final Writer out, 166 final InputStream bindings) throws IOException { 167 this(in, out, bindings, Terminal.getTerminal()); 168 } 169 170 /** 171 * Create a new reader. 172 * 173 * @param in 174 * the input 175 * @param out 176 * the output 177 * @param bindings 178 * the key bindings to use 179 * @param term 180 * the terminal to use 181 */ 182 public ConsoleReader(InputStream in, Writer out, InputStream bindings, 183 Terminal term) throws IOException { 184 this.terminal = term; 185 setInput(in); 186 this.out = out; 187 188 if (bindings == null) { 189 String bindingFile = System.getProperty("jline.keybindings", 190 new File(System.getProperty("user.home", 191 ".jlinebindings.properties")).getAbsolutePath()); 192 193 if (!(new File(bindingFile).isFile())) { 194 bindings = terminal.getDefaultBindings(); 195 } else { 196 bindings = new FileInputStream(new File(bindingFile)); 197 } 198 } 199 200 this.keybindings = new short[Character.MAX_VALUE * 2]; 201 202 Arrays.fill(this.keybindings, UNKNOWN); 203 204 /** 205 * Loads the key bindings. Bindings file is in the format: 206 * 207 * keycode: operation name 208 */ 209 if (bindings != null) { 210 Properties p = new Properties(); 211 p.load(bindings); 212 bindings.close(); 213 214 for (Iterator i = p.keySet().iterator(); i.hasNext();) { 215 String val = (String) i.next(); 216 217 try { 218 Short code = new Short(val); 219 String op = (String) p.getProperty(val); 220 221 Short opval = (Short) KEYMAP_NAMES.get(op); 222 223 if (opval != null) { 224 keybindings[code.shortValue()] = opval.shortValue(); 225 } 226 } catch (NumberFormatException nfe) { 227 consumeException(nfe); 228 } 229 } 230 231 // hardwired arrow key bindings 232 // keybindings[VK_UP] = PREV_HISTORY; 233 // keybindings[VK_DOWN] = NEXT_HISTORY; 234 // keybindings[VK_LEFT] = PREV_CHAR; 235 // keybindings[VK_RIGHT] = NEXT_CHAR; 236 } 237 } 238 239 public Terminal getTerminal() { 240 return this.terminal; 241 } 242 243 /** 244 * Set the stream for debugging. Development use only. 245 */ 246 public void setDebug(final PrintWriter debugger) { 247 ConsoleReader.debugger = debugger; 248 } 249 250 /** 251 * Set the stream to be used for console input. 252 */ 253 public void setInput(final InputStream in) { 254 this.in = in; 255 } 256 257 /** 258 * Returns the stream used for console input. 259 */ 260 public InputStream getInput() { 261 return this.in; 262 } 263 264 /** 265 * Read the next line and return the contents of the buffer. 266 */ 267 public String readLine() throws IOException { 268 return readLine((String) null); 269 } 270 271 /** 272 * Read the next line with the specified character mask. If null, then 273 * characters will be echoed. If 0, then no characters will be echoed. 274 */ 275 public String readLine(final Character mask) throws IOException { 276 return readLine(null, mask); 277 } 278 279 /** 280 * @param bellEnabled 281 * if true, enable audible keyboard bells if an alert is 282 * required. 283 */ 284 public void setBellEnabled(final boolean bellEnabled) { 285 this.bellEnabled = bellEnabled; 286 } 287 288 /** 289 * @return true is audible keyboard bell is enabled. 290 */ 291 public boolean getBellEnabled() { 292 return this.bellEnabled; 293 } 294 295 /** 296 * Query the terminal to find the current width; 297 * 298 * @see Terminal#getTerminalWidth 299 * @return the width of the current terminal. 300 */ 301 public int getTermwidth() { 302 return Terminal.setupTerminal().getTerminalWidth(); 303 } 304 305 /** 306 * Query the terminal to find the current width; 307 * 308 * @see Terminal#getTerminalHeight 309 * 310 * @return the height of the current terminal. 311 */ 312 public int getTermheight() { 313 return Terminal.setupTerminal().getTerminalHeight(); 314 } 315 316 /** 317 * @param autoprintThreshhold 318 * the number of candidates to print without issuing a warning. 319 */ 320 public void setAutoprintThreshhold(final int autoprintThreshhold) { 321 this.autoprintThreshhold = autoprintThreshhold; 322 } 323 324 /** 325 * @return the number of candidates to print without issing a warning. 326 */ 327 public int getAutoprintThreshhold() { 328 return this.autoprintThreshhold; 329 } 330 331 int getKeyForAction(short logicalAction) { 332 for (int i = 0; i < keybindings.length; i++) { 333 if (keybindings[i] == logicalAction) { 334 return i; 335 } 336 } 337 338 return -1; 339 } 340 341 /** 342 * Clear the echoed characters for the specified character code. 343 */ 344 int clearEcho(int c) throws IOException { 345 // if the terminal is not echoing, then just return... 346 if (!terminal.getEcho()) { 347 return 0; 348 } 349 350 // otherwise, clear 351 int num = countEchoCharacters((char) c); 352 back(num); 353 drawBuffer(num); 354 355 return num; 356 } 357 358 int countEchoCharacters(char c) { 359 // tabs as special: we need to determine the number of spaces 360 // to cancel based on what out current cursor position is 361 if (c == 9) { 362 int tabstop = 8; // will this ever be different? 363 int position = getCursorPosition(); 364 365 return tabstop - (position % tabstop); 366 } 367 368 return getPrintableCharacters(c).length(); 369 } 370 371 /** 372 * Return the number of characters that will be printed when the specified 373 * character is echoed to the screen. Adapted from cat by Torbjorn Granlund, 374 * as repeated in stty by David MacKenzie. 375 */ 376 StringBuffer getPrintableCharacters(char ch) { 377 StringBuffer sbuff = new StringBuffer(); 378 379 if (ch >= 32) { 380 if (ch < 127) { 381 sbuff.append(ch); 382 } else if (ch == 127) { 383 sbuff.append('^'); 384 sbuff.append('?'); 385 } else { 386 sbuff.append('M'); 387 sbuff.append('-'); 388 389 if (ch >= (128 + 32)) { 390 if (ch < (128 + 127)) { 391 sbuff.append((char) (ch - 128)); 392 } else { 393 sbuff.append('^'); 394 sbuff.append('?'); 395 } 396 } else { 397 sbuff.append('^'); 398 sbuff.append((char) (ch - 128 + 64)); 399 } 400 } 401 } else { 402 sbuff.append('^'); 403 sbuff.append((char) (ch + 64)); 404 } 405 406 return sbuff; 407 } 408 409 int getCursorPosition() { 410 // FIXME: does not handle anything but a line with a prompt 411 // absolute position 412 return ((prompt == null) ? 0 : prompt.length()) + buf.cursor; 413 } 414 415 public String readLine(final String prompt) throws IOException { 416 return readLine(prompt, null); 417 } 418 419 /** 420 * The default prompt that will be issued. 421 */ 422 public void setDefaultPrompt(String prompt) { 423 this.prompt = prompt; 424 } 425 426 /** 427 * The default prompt that will be issued. 428 */ 429 public String getDefaultPrompt() { 430 return prompt; 431 } 432 433 /** 434 * Read a line from the <i>in</i> {@link InputStream}, and return the line 435 * (without any trailing newlines). 436 * 437 * @param prompt 438 * the prompt to issue to the console, may be null. 439 * @return a line that is read from the terminal, or null if there was null 440 * input (e.g., <i>CTRL-D</i> was pressed). 441 */ 442 public String readLine(final String prompt, final Character mask) 443 throws IOException { 444 this.mask = mask; 445 if (prompt != null) 446 this.prompt = prompt; 447 448 try { 449 terminal.beforeReadLine(this, this.prompt, mask); 450 451 if ((this.prompt != null) && (this.prompt.length() > 0)) { 452 out.write(this.prompt); 453 out.flush(); 454 } 455 456 // if the terminal is unsupported, just use plain-java reading 457 if (!terminal.isSupported()) { 458 return readLine(in); 459 } 460 461 while (true) { 462 int[] next = readBinding(); 463 464 if (next == null) { 465 return null; 466 } 467 468 int c = next[0]; 469 int code = next[1]; 470 471 if (c == -1) { 472 return null; 473 } 474 475 boolean success = true; 476 477 switch (code) { 478 case EXIT: // ctrl-d 479 480 if (buf.buffer.length() == 0) { 481 return null; 482 } 483 484 case COMPLETE: // tab 485 success = complete(); 486 break; 487 488 case MOVE_TO_BEG: 489 success = setCursorPosition(0); 490 break; 491 492 case KILL_LINE: // CTRL-K 493 success = killLine(); 494 break; 495 496 case CLEAR_SCREEN: // CTRL-L 497 success = clearScreen(); 498 break; 499 500 case KILL_LINE_PREV: // CTRL-U 501 success = resetLine(); 502 break; 503 504 case NEWLINE: // enter 505 printNewline(); // output newline 506 return finishBuffer(); 507 508 case DELETE_PREV_CHAR: // backspace 509 success = backspace(); 510 break; 511 512 case DELETE_NEXT_CHAR: // delete 513 success = deleteCurrentCharacter(); 514 break; 515 516 case MOVE_TO_END: 517 success = moveToEnd(); 518 break; 519 520 case PREV_CHAR: 521 success = moveCursor(-1) != 0; 522 break; 523 524 case NEXT_CHAR: 525 success = moveCursor(1) != 0; 526 break; 527 528 case NEXT_HISTORY: 529 success = moveHistory(true); 530 break; 531 532 case PREV_HISTORY: 533 success = moveHistory(false); 534 break; 535 536 case REDISPLAY: 537 break; 538 539 case PASTE: 540 success = paste(); 541 break; 542 543 case DELETE_PREV_WORD: 544 success = deletePreviousWord(); 545 break; 546 547 case PREV_WORD: 548 success = previousWord(); 549 break; 550 551 case NEXT_WORD: 552 success = nextWord(); 553 break; 554 555 case START_OF_HISTORY: 556 success = history.moveToFirstEntry(); 557 if (success) 558 setBuffer(history.current()); 559 break; 560 561 case END_OF_HISTORY: 562 success = history.moveToLastEntry(); 563 if (success) 564 setBuffer(history.current()); 565 break; 566 567 case CLEAR_LINE: 568 moveInternal(-(buf.buffer.length())); 569 killLine(); 570 break; 571 572 case INSERT: 573 buf.setOvertyping(!buf.isOvertyping()); 574 break; 575 576 case UNKNOWN: 577 default: 578 if (c != 0) // ignore null chars 579 putChar(c, true); 580 else 581 success = false; 582 } 583 584 if (!(success)) { 585 beep(); 586 } 587 588 flushConsole(); 589 } 590 } finally { 591 terminal.afterReadLine(this, this.prompt, mask); 592 } 593 } 594 595 private String readLine(InputStream in) throws IOException { 596 StringBuffer buf = new StringBuffer(); 597 598 while (true) { 599 int i = in.read(); 600 601 if ((i == -1) || (i == '\n') || (i == '\r')) { 602 return buf.toString(); 603 } 604 605 buf.append((char) i); 606 } 607 608 // return new BufferedReader (new InputStreamReader (in)).readLine (); 609 } 610 611 /** 612 * Reads the console input and returns an array of the form [raw, key 613 * binding]. 614 */ 615 private int[] readBinding() throws IOException { 616 int c = readVirtualKey(); 617 618 if (c == -1) { 619 return null; 620 } 621 622 // extract the appropriate key binding 623 short code = keybindings[c]; 624 625 if (debugger != null) { 626 debug(" translated: " + (int) c + ": " + code); 627 } 628 629 return new int[] { c, code }; 630 } 631 632 /** 633 * Move up or down the history tree. 634 * 635 * @param direction 636 * less than 0 to move up the tree, down otherwise 637 */ 638 private final boolean moveHistory(final boolean next) throws IOException { 639 if (next && !history.next()) { 640 return false; 641 } else if (!next && !history.previous()) { 642 return false; 643 } 644 645 setBuffer(history.current()); 646 647 return true; 648 } 649 650 /** 651 * Paste the contents of the clipboard into the console buffer 652 * 653 * @return true if clipboard contents pasted 654 */ 655 public boolean paste() throws IOException { 656 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 657 658 if (clipboard == null) { 659 return false; 660 } 661 662 Transferable transferable = clipboard.getContents(null); 663 664 if (transferable == null) { 665 return false; 666 } 667 668 try { 669 Object content = transferable 670 .getTransferData(DataFlavor.plainTextFlavor); 671 672 /* 673 * This fix was suggested in bug #1060649 at 674 * http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056 675 * to get around the deprecated DataFlavor.plainTextFlavor, but it 676 * raises a UnsupportedFlavorException on Mac OS X 677 */ 678 if (content == null) { 679 try { 680 content = new DataFlavor().getReaderForText(transferable); 681 } catch (Exception e) { 682 } 683 } 684 685 if (content == null) { 686 return false; 687 } 688 689 String value; 690 691 if (content instanceof Reader) { 692 // TODO: we might want instead connect to the input stream 693 // so we can interpret individual lines 694 value = ""; 695 696 String line = null; 697 698 for (BufferedReader read = new BufferedReader((Reader) content); (line = read 699 .readLine()) != null;) { 700 if (value.length() > 0) { 701 value += "\n"; 702 } 703 704 value += line; 705 } 706 } else { 707 value = content.toString(); 708 } 709 710 if (value == null) { 711 return true; 712 } 713 714 putString(value); 715 716 return true; 717 } catch (UnsupportedFlavorException ufe) { 718 if (debugger != null) 719 debug(ufe + ""); 720 721 return false; 722 } 723 } 724 725 /** 726 * Kill the buffer ahead of the current cursor position. 727 * 728 * @return true if successful 729 */ 730 public boolean killLine() throws IOException { 731 int cp = buf.cursor; 732 int len = buf.buffer.length(); 733 734 if (cp >= len) { 735 return false; 736 } 737 738 int num = buf.buffer.length() - cp; 739 clearAhead(num); 740 741 for (int i = 0; i < num; i++) { 742 buf.buffer.deleteCharAt(len - i - 1); 743 } 744 745 return true; 746 } 747 748 /** 749 * Clear the screen by issuing the ANSI "clear screen" code. 750 */ 751 public boolean clearScreen() throws IOException { 752 if (!terminal.isANSISupported()) { 753 return false; 754 } 755 756 // send the ANSI code to clear the screen 757 printString(((char) 27) + "[2J"); 758 flushConsole(); 759 760 // then send the ANSI code to go to position 1,1 761 printString(((char) 27) + "[1;1H"); 762 flushConsole(); 763 764 redrawLine(); 765 766 return true; 767 } 768 769 /** 770 * Use the completors to modify the buffer with the appropriate completions. 771 * 772 * @return true if successful 773 */ 774 private final boolean complete() throws IOException { 775 // debug ("tab for (" + buf + ")"); 776 if (completors.size() == 0) { 777 return false; 778 } 779 780 List candidates = new LinkedList(); 781 String bufstr = buf.buffer.toString(); 782 int cursor = buf.cursor; 783 784 int position = -1; 785 786 for (Iterator i = completors.iterator(); i.hasNext();) { 787 Completor comp = (Completor) i.next(); 788 789 if ((position = comp.complete(bufstr, cursor, candidates)) != -1) { 790 break; 791 } 792 } 793 794 // no candidates? Fail. 795 if (candidates.size() == 0) { 796 return false; 797 } 798 799 return completionHandler.complete(this, candidates, position); 800 } 801 802 public CursorBuffer getCursorBuffer() { 803 return buf; 804 } 805 806 /** 807 * Output the specified {@link Collection} in proper columns. 808 * 809 * @param stuff 810 * the stuff to print 811 */ 812 public void printColumns(final Collection stuff) throws IOException { 813 if ((stuff == null) || (stuff.size() == 0)) { 814 return; 815 } 816 817 int width = getTermwidth(); 818 int maxwidth = 0; 819 820 for (Iterator i = stuff.iterator(); i.hasNext(); maxwidth = Math.max( 821 maxwidth, i.next().toString().length())) { 822 ; 823 } 824 825 StringBuffer line = new StringBuffer(); 826 827 int showLines; 828 829 if (usePagination) 830 showLines = getTermheight() - 1; // page limit 831 else 832 showLines = Integer.MAX_VALUE; 833 834 for (Iterator i = stuff.iterator(); i.hasNext();) { 835 String cur = (String) i.next(); 836 837 if ((line.length() + maxwidth) > width) { 838 printString(line.toString().trim()); 839 printNewline(); 840 line.setLength(0); 841 if (--showLines == 0) { // Overflow 842 printString(loc.getString("display-more")); 843 flushConsole(); 844 int c = readVirtualKey(); 845 if (c == '\r' || c == '\n') 846 showLines = 1; // one step forward 847 else if (c != 'q') 848 showLines = getTermheight() - 1; // page forward 849 850 back(loc.getString("display-more").length()); 851 if (c == 'q') 852 break; // cancel 853 } 854 } 855 856 pad(cur, maxwidth + 3, line); 857 } 858 859 if (line.length() > 0) { 860 printString(line.toString().trim()); 861 printNewline(); 862 line.setLength(0); 863 } 864 } 865 866 /** 867 * Append <i>toPad</i> to the specified <i>appendTo</i>, as well as (<i>toPad.length () - 868 * len</i>) spaces. 869 * 870 * @param toPad 871 * the {@link String} to pad 872 * @param len 873 * the target length 874 * @param appendTo 875 * the {@link StringBuffer} to which to append the padded 876 * {@link String}. 877 */ 878 private final void pad(final String toPad, final int len, 879 final StringBuffer appendTo) { 880 appendTo.append(toPad); 881 882 for (int i = 0; i < (len - toPad.length()); i++, appendTo.append(' ')) { 883 ; 884 } 885 } 886 887 /** 888 * Add the specified {@link Completor} to the list of handlers for 889 * tab-completion. 890 * 891 * @param completor 892 * the {@link Completor} to add 893 * @return true if it was successfully added 894 */ 895 public boolean addCompletor(final Completor completor) { 896 return completors.add(completor); 897 } 898 899 /** 900 * Remove the specified {@link Completor} from the list of handlers for 901 * tab-completion. 902 * 903 * @param completor 904 * the {@link Completor} to remove 905 * @return true if it was successfully removed 906 */ 907 public boolean removeCompletor(final Completor completor) { 908 return completors.remove(completor); 909 } 910 911 /** 912 * Returns an unmodifiable list of all the completors. 913 */ 914 public Collection getCompletors() { 915 return Collections.unmodifiableList(completors); 916 } 917 918 /** 919 * Erase the current line. 920 * 921 * @return false if we failed (e.g., the buffer was empty) 922 */ 923 final boolean resetLine() throws IOException { 924 if (buf.cursor == 0) { 925 return false; 926 } 927 928 backspaceAll(); 929 930 return true; 931 } 932 933 /** 934 * Move the cursor position to the specified absolute index. 935 */ 936 public final boolean setCursorPosition(final int position) 937 throws IOException { 938 return moveCursor(position - buf.cursor) != 0; 939 } 940 941 /** 942 * Set the current buffer's content to the specified {@link String}. The 943 * visual console will be modified to show the current buffer. 944 * 945 * @param buffer 946 * the new contents of the buffer. 947 */ 948 private final void setBuffer(final String buffer) throws IOException { 949 // don't bother modifying it if it is unchanged 950 if (buffer.equals(buf.buffer.toString())) { 951 return; 952 } 953 954 // obtain the difference between the current buffer and the new one 955 int sameIndex = 0; 956 957 for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1) 958 && (i < l2); i++) { 959 if (buffer.charAt(i) == buf.buffer.charAt(i)) { 960 sameIndex++; 961 } else { 962 break; 963 } 964 } 965 966 int diff = buf.buffer.length() - sameIndex; 967 968 backspace(diff); // go back for the differences 969 killLine(); // clear to the end of the line 970 buf.buffer.setLength(sameIndex); // the new length 971 putString(buffer.substring(sameIndex)); // append the differences 972 } 973 974 /** 975 * Clear the line and redraw it. 976 */ 977 public final void redrawLine() throws IOException { 978 printCharacter(RESET_LINE); 979 flushConsole(); 980 drawLine(); 981 } 982 983 /** 984 * Output put the prompt + the current buffer 985 */ 986 public final void drawLine() throws IOException { 987 if (prompt != null) { 988 printString(prompt); 989 } 990 991 printString(buf.buffer.toString()); 992 993 if (buf.length() != buf.cursor) // not at end of line 994 back(buf.length() - buf.cursor); // sync 995 } 996 997 /** 998 * Output a platform-dependant newline. 999 */ 1000 public final void printNewline() throws IOException { 1001 printString(CR); 1002 flushConsole(); 1003 } 1004 1005 /** 1006 * Clear the buffer and add its contents to the history. 1007 * 1008 * @return the former contents of the buffer. 1009 */ 1010 final String finishBuffer() { 1011 String str = buf.buffer.toString(); 1012 1013 // we only add it to the history if the buffer is not empty 1014 // and if mask is null, since having a mask typically means 1015 // the string was a password. We clear the mask after this call 1016 if (str.length() > 0) { 1017 if (mask == null && useHistory) { 1018 history.addToHistory(str); 1019 } else { 1020 mask = null; 1021 } 1022 } 1023 1024 history.moveToEnd(); 1025 1026 buf.buffer.setLength(0); 1027 buf.cursor = 0; 1028 1029 return str; 1030 } 1031 1032 /** 1033 * Write out the specified string to the buffer and the output stream. 1034 */ 1035 public final void putString(final String str) throws IOException { 1036 buf.write(str); 1037 printString(str); 1038 drawBuffer(); 1039 } 1040 1041 /** 1042 * Output the specified string to the output stream (but not the buffer). 1043 */ 1044 public final void printString(final String str) throws IOException { 1045 printCharacters(str.toCharArray()); 1046 } 1047 1048 /** 1049 * Output the specified character, both to the buffer and the output stream. 1050 */ 1051 private final void putChar(final int c, final boolean print) 1052 throws IOException { 1053 buf.write((char) c); 1054 1055 if (print) { 1056 // no masking... 1057 if (mask == null) { 1058 printCharacter(c); 1059 } 1060 // null mask: don't print anything... 1061 else if (mask.charValue() == 0) { 1062 ; 1063 } 1064 // otherwise print the mask... 1065 else { 1066 printCharacter(mask.charValue()); 1067 } 1068 1069 drawBuffer(); 1070 } 1071 } 1072 1073 /** 1074 * Redraw the rest of the buffer from the cursor onwards. This is necessary 1075 * for inserting text into the buffer. 1076 * 1077 * @param clear 1078 * the number of characters to clear after the end of the buffer 1079 */ 1080 private final void drawBuffer(final int clear) throws IOException { 1081 // debug ("drawBuffer: " + clear); 1082 char[] chars = buf.buffer.substring(buf.cursor).toCharArray(); 1083 if (mask != null) 1084 Arrays.fill(chars, mask.charValue()); 1085 1086 printCharacters(chars); 1087 1088 clearAhead(clear); 1089 back(chars.length); 1090 flushConsole(); 1091 } 1092 1093 /** 1094 * Redraw the rest of the buffer from the cursor onwards. This is necessary 1095 * for inserting text into the buffer. 1096 */ 1097 private final void drawBuffer() throws IOException { 1098 drawBuffer(0); 1099 } 1100 1101 /** 1102 * Clear ahead the specified number of characters without moving the cursor. 1103 */ 1104 private final void clearAhead(final int num) throws IOException { 1105 if (num == 0) { 1106 return; 1107 } 1108 1109 // debug ("clearAhead: " + num); 1110 1111 // print blank extra characters 1112 printCharacters(' ', num); 1113 1114 // we need to flush here so a "clever" console 1115 // doesn't just ignore the redundancy of a space followed by 1116 // a backspace. 1117 flushConsole(); 1118 1119 // reset the visual cursor 1120 back(num); 1121 1122 flushConsole(); 1123 } 1124 1125 /** 1126 * Move the visual cursor backwards without modifying the buffer cursor. 1127 */ 1128 private final void back(final int num) throws IOException { 1129 printCharacters(BACKSPACE, num); 1130 flushConsole(); 1131 } 1132 1133 /** 1134 * Issue an audible keyboard bell, if {@link #getBellEnabled} return true. 1135 */ 1136 public final void beep() throws IOException { 1137 if (!(getBellEnabled())) { 1138 return; 1139 } 1140 1141 printCharacter(KEYBOARD_BELL); 1142 // need to flush so the console actually beeps 1143 flushConsole(); 1144 } 1145 1146 /** 1147 * Output the specified character to the output stream without manipulating 1148 * the current buffer. 1149 */ 1150 private final void printCharacter(final int c) throws IOException { 1151 out.write(c); 1152 } 1153 1154 /** 1155 * Output the specified characters to the output stream without manipulating 1156 * the current buffer. 1157 */ 1158 private final void printCharacters(final char[] c) throws IOException { 1159 out.write(c); 1160 } 1161 1162 private final void printCharacters(final char c, final int num) 1163 throws IOException { 1164 if (num == 1) { 1165 printCharacter(c); 1166 } else { 1167 char[] chars = new char[num]; 1168 Arrays.fill(chars, c); 1169 printCharacters(chars); 1170 } 1171 } 1172 1173 /** 1174 * Flush the console output stream. This is important for printout out 1175 * single characters (like a backspace or keyboard) that we want the console 1176 * to handle immedately. 1177 */ 1178 public final void flushConsole() throws IOException { 1179 out.flush(); 1180 } 1181 1182 private final int backspaceAll() throws IOException { 1183 return backspace(Integer.MAX_VALUE); 1184 } 1185 1186 /** 1187 * Issue <em>num</em> backspaces. 1188 * 1189 * @return the number of characters backed up 1190 */ 1191 private final int backspace(final int num) throws IOException { 1192 if (buf.cursor == 0) { 1193 return 0; 1194 } 1195 1196 int count = 0; 1197 1198 count = moveCursor(-1 * num) * -1; 1199 // debug ("Deleting from " + buf.cursor + " for " + count); 1200 buf.buffer.delete(buf.cursor, buf.cursor + count); 1201 drawBuffer(count); 1202 1203 return count; 1204 } 1205 1206 /** 1207 * Issue a backspace. 1208 * 1209 * @return true if successful 1210 */ 1211 public final boolean backspace() throws IOException { 1212 return backspace(1) == 1; 1213 } 1214 1215 private final boolean moveToEnd() throws IOException { 1216 if (moveCursor(1) == 0) { 1217 return false; 1218 } 1219 1220 while (moveCursor(1) != 0) { 1221 ; 1222 } 1223 1224 return true; 1225 } 1226 1227 /** 1228 * Delete the character at the current position and redraw the remainder of 1229 * the buffer. 1230 */ 1231 private final boolean deleteCurrentCharacter() throws IOException { 1232 boolean success = buf.buffer.length() > 0; 1233 if (!success) { 1234 return false; 1235 } 1236 1237 if (buf.cursor == buf.buffer.length()) { 1238 return false; 1239 } 1240 1241 buf.buffer.deleteCharAt(buf.cursor); 1242 drawBuffer(1); 1243 return true; 1244 } 1245 1246 private final boolean previousWord() throws IOException { 1247 while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { 1248 ; 1249 } 1250 1251 while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { 1252 ; 1253 } 1254 1255 return true; 1256 } 1257 1258 private final boolean nextWord() throws IOException { 1259 while (isDelimiter(buf.current()) && (moveCursor(1) != 0)) { 1260 ; 1261 } 1262 1263 while (!isDelimiter(buf.current()) && (moveCursor(1) != 0)) { 1264 ; 1265 } 1266 1267 return true; 1268 } 1269 1270 private final boolean deletePreviousWord() throws IOException { 1271 while (isDelimiter(buf.current()) && backspace()) { 1272 ; 1273 } 1274 1275 while (!isDelimiter(buf.current()) && backspace()) { 1276 ; 1277 } 1278 1279 return true; 1280 } 1281 1282 /** 1283 * Move the cursor <i>where</i> characters. 1284 * 1285 * @param where 1286 * if less than 0, move abs(<i>where</i>) to the left, 1287 * otherwise move <i>where</i> to the right. 1288 * 1289 * @return the number of spaces we moved 1290 */ 1291 private final int moveCursor(final int num) throws IOException { 1292 int where = num; 1293 1294 if ((buf.cursor == 0) && (where < 0)) { 1295 return 0; 1296 } 1297 1298 if ((buf.cursor == buf.buffer.length()) && (where > 0)) { 1299 return 0; 1300 } 1301 1302 if ((buf.cursor + where) < 0) { 1303 where = -buf.cursor; 1304 } else if ((buf.cursor + where) > buf.buffer.length()) { 1305 where = buf.buffer.length() - buf.cursor; 1306 } 1307 1308 moveInternal(where); 1309 1310 return where; 1311 } 1312 1313 /** 1314 * debug. 1315 * 1316 * @param str 1317 * the message to issue. 1318 */ 1319 public static void debug(final String str) { 1320 if (debugger != null) { 1321 debugger.println(str); 1322 debugger.flush(); 1323 } 1324 } 1325 1326 /** 1327 * Move the cursor <i>where</i> characters, withough checking the current 1328 * buffer. 1329 * 1330 * @see #where 1331 * 1332 * @param where 1333 * the number of characters to move to the right or left. 1334 */ 1335 private final void moveInternal(final int where) throws IOException { 1336 // debug ("move cursor " + where + " (" 1337 // + buf.cursor + " => " + (buf.cursor + where) + ")"); 1338 buf.cursor += where; 1339 1340 char c; 1341 1342 if (where < 0) { 1343 c = BACKSPACE; 1344 } else if (buf.cursor == 0) { 1345 return; 1346 } else if (mask != null) { 1347 c = mask.charValue(); 1348 } else { 1349 c = buf.buffer.charAt(buf.cursor - 1); // draw replacement 1350 } 1351 1352 // null character mask: don't output anything 1353 if (NULL_MASK.equals(mask)) { 1354 return; 1355 } 1356 1357 printCharacters(c, Math.abs(where)); 1358 } 1359 1360 /** 1361 * Read a character from the console. 1362 * 1363 * @return the character, or -1 if an EOF is received. 1364 */ 1365 public final int readVirtualKey() throws IOException { 1366 int c = terminal.readVirtualKey(in); 1367 1368 if (debugger != null) { 1369 debug("keystroke: " + c + ""); 1370 } 1371 1372 // clear any echo characters 1373 clearEcho(c); 1374 1375 return c; 1376 } 1377 1378 public final int readCharacter(final char[] allowed) throws IOException { 1379 // if we restrict to a limited set and the current character 1380 // is not in the set, then try again. 1381 char c; 1382 1383 Arrays.sort(allowed); // always need to sort before binarySearch 1384 1385 while (Arrays.binarySearch(allowed, c = (char) readVirtualKey()) < 0) 1386 ; 1387 1388 return c; 1389 } 1390 1391 public void setHistory(final History history) { 1392 this.history = history; 1393 } 1394 1395 public History getHistory() { 1396 return this.history; 1397 } 1398 1399 public void setCompletionHandler(final CompletionHandler completionHandler) { 1400 this.completionHandler = completionHandler; 1401 } 1402 1403 public CompletionHandler getCompletionHandler() { 1404 return this.completionHandler; 1405 } 1406 1407 /** 1408 * <p> 1409 * Set the echo character. For example, to have "*" entered when a password 1410 * is typed: 1411 * </p> 1412 * 1413 * <pre> 1414 * myConsoleReader.setEchoCharacter(new Character('*')); 1415 * </pre> 1416 * 1417 * <p> 1418 * Setting the character to 1419 * 1420 * <pre> 1421 * null 1422 * </pre> 1423 * 1424 * will restore normal character echoing. Setting the character to 1425 * 1426 * <pre> 1427 * new Character(0) 1428 * </pre> 1429 * 1430 * will cause nothing to be echoed. 1431 * </p> 1432 * 1433 * @param echoCharacter 1434 * the character to echo to the console in place of the typed 1435 * character. 1436 */ 1437 public void setEchoCharacter(final Character echoCharacter) { 1438 this.echoCharacter = echoCharacter; 1439 } 1440 1441 /** 1442 * Returns the echo character. 1443 */ 1444 public Character getEchoCharacter() { 1445 return this.echoCharacter; 1446 } 1447 1448 /** 1449 * No-op for exceptions we want to silently consume. 1450 */ 1451 private void consumeException(final Throwable e) { 1452 } 1453 1454 /** 1455 * Checks to see if the specified character is a delimiter. We consider a 1456 * character a delimiter if it is anything but a letter or digit. 1457 * 1458 * @param c 1459 * the character to test 1460 * @return true if it is a delimiter 1461 */ 1462 private boolean isDelimiter(char c) { 1463 return !Character.isLetterOrDigit(c); 1464 } 1465 1466 /** 1467 * Whether or not to add new commands to the history buffer. 1468 */ 1469 public void setUseHistory(boolean useHistory) { 1470 this.useHistory = useHistory; 1471 } 1472 1473 /** 1474 * Whether or not to add new commands to the history buffer. 1475 */ 1476 public boolean getUseHistory() { 1477 return useHistory; 1478 } 1479 1480 /** 1481 * Whether to use pagination when the number of rows of candidates exceeds 1482 * the height of the temrinal. 1483 */ 1484 public void setUsePagination(boolean usePagination) { 1485 this.usePagination = usePagination; 1486 } 1487 1488 /** 1489 * Whether to use pagination when the number of rows of candidates exceeds 1490 * the height of the temrinal. 1491 */ 1492 public boolean getUsePagination() { 1493 return this.usePagination; 1494 } 1495 1496 }