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