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 }