4 import java
.awt
.Dimension
;
6 import java
.awt
.Insets
;
7 import java
.awt
.Rectangle
;
8 import java
.awt
.datatransfer
.DataFlavor
;
9 import java
.awt
.datatransfer
.Transferable
;
10 import java
.awt
.event
.ActionEvent
;
11 import java
.awt
.event
.ActionListener
;
12 import java
.awt
.event
.KeyAdapter
;
13 import java
.awt
.event
.KeyEvent
;
14 import java
.io
.IOException
;
15 import java
.io
.InterruptedIOException
;
16 import java
.io
.OutputStream
;
17 import java
.io
.PrintStream
;
18 import java
.util
.ArrayList
;
19 import java
.util
.Iterator
;
20 import java
.util
.LinkedList
;
21 import java
.util
.List
;
22 import javax
.swing
.JTextPane
;
23 import javax
.swing
.SwingUtilities
;
24 import javax
.swing
.Timer
;
25 import javax
.swing
.text
.*;
28 * A TextArea that supports terminal like functionality.
29 * @author Tony Johnson (tonyj@slac.stanford.edu)
30 * @version $Id: Console.java,v 1.1 2006/07/12 14:59:23 esbenzeuthen Exp $
34 * TODO: Support insert/overstrike mode
35 * TODO: Cut should only be allowed in input area
36 * TODO: Limit size of document
37 * TODO: Make Ctrl^C work properly
38 * TODO: Add support for command completion
39 * TODO: Abilty to write images (a la BeanShell) -- maybe done JTextPane has insertIcon method
40 * TODO: Worry about key bindings (PageUp, PageDown etc)
41 * TODO: *Document uses too much memory when lots of output!
43 public class Console
extends JTextPane
48 private static final long serialVersionUID
= -6492939159097843176L;
49 private final static String defaultPrompt
= "> ";
50 private String currentPrompt
;
51 private List history
= new LinkedList();
52 private List list
= new ArrayList();
53 private List queue
= new LinkedList();
54 private ListEntry last
;
55 private Object lock
= new Object();
56 private PrintStream logStream
;
57 private ConsoleInputStream theInputStream
;
58 private SimpleAttributeSet defStyle
;
59 private SimpleAttributeSet promptStyle
;
61 // We maintain several queues in this code. They are:
62 // 1) The typeAhead buffer, stuff which has been typed while the program attached to the console
63 // is busy. As soon as an Return is entered the contents of the typeAhead buffer is moved to the
65 // 2) The pasteAhead buffer. Complete commands that have been entered, but not yet echoed to the
66 // console because the program attached to the console is busy.
67 // 3) The queue, commands that have been entered, echoed to the conole (if appropriate) but not
68 // yet read by the input stream.
70 // Stuff pasted while not waiting for input goes into the pasteAhead buffer
71 private StringBuffer pasteAhead
= new StringBuffer();
73 // Stuff typed while not waiting for input goes into the typeAhead buffer
74 private StringBuffer typeAhead
= new StringBuffer();
75 private MyTimer timer
;
77 private boolean waitingForInput
= false; // True while inputStream waiting for input
78 private int historyPos
= -1;
79 private int maxHistory
= 100;
80 private int start
; // Of input area!
81 private int startLastLine
; // Start of last line (before prompt)
83 /** Create a new Console */
86 setFont(new Font("Courier", Font
.PLAIN
, 12));
87 // setPreferredSize(new Dimension(300, 200));
88 CKeyListener l
= new CKeyListener();
90 timer
= new MyTimer(l
);
93 defStyle
= new SimpleAttributeSet()
98 private static final long serialVersionUID
= 1L;
100 public Object
getAttribute(Object key
)
102 if (key
.equals(StyleConstants
.Foreground
))
104 return getForeground();
106 else return super.getAttribute(key
);
109 promptStyle
= new SimpleAttributeSet();
110 promptStyle
.addAttribute(StyleConstants
.Foreground
, new Color(0, 153, 51));
113 /** Create an input stream for reading user input from the console.
114 * The input stream will use the default prompt.
115 * @return The newly created input stream
117 public ConsoleInputStream
getInputStream()
119 return getInputStream(defaultPrompt
);
122 /** Create an input stream for reading user input from the console.
123 * @param initialPrompt The prompt to use for user input
124 * @return The newly created input stream
126 public ConsoleInputStream
getInputStream(String initialPrompt
)
128 if (theInputStream
== null) theInputStream
= new CInputStream(initialPrompt
);
129 return theInputStream
;
132 /** Sets a stream to use for writing logging output.
133 * All input/output to the console will be logged to this
135 * @param out The output stream to use, or <CODE>null</CODE> to turn off logging.
137 public synchronized void setLogStream(OutputStream out
)
139 log
= !(out
== null);
140 if (logStream
!= null) logStream
.flush();
141 if (log
) logStream
= new PrintStream(out
);
142 else logStream
= null;
145 /** Get the current log stream
146 * @return The current log stream, or <CODE>null</CODE> if no current stream
148 public OutputStream
getLogStream()
153 /** Temporarily disables/enables logging.
154 * @param log <CODE>true</CODE> to enable logging.
156 public synchronized void setLoggingEnabled(boolean log
)
158 this.log
= log
&& (logStream
!= null);
161 /** Test if logging is currently enabled
162 * @return <CODE>true</CODE> if logging enabled.
164 public boolean isLoggingEnabled()
169 /** Get an output stream for writing to the console.
170 * @param set The attributes for text created by this output stream, or <CODE>null</CODE> for the default
172 * @param autoShow If true the console will "pop to the front" when new output is written.
173 * @return The newly created output stream
175 public ConsoleOutputStream
getOutputStream(AttributeSet set
, boolean autoShow
)
177 return new COutputStream(set
, autoShow
);
179 public ConsoleOutputStream
getOutputStream(AttributeSet set
)
181 return getOutputStream(set
, false);
184 /** Adds a listener for CTRL^C events
185 * @param l The listener to add.
187 public void addInterruptListener(ActionListener l
)
189 listenerList
.add(ActionListener
.class, l
);
191 /** Cleans up resources associated with this console. Closes any input streams
192 * associated with this console.
194 public void dispose()
197 if (theInputStream
!= null)
203 theInputStream
.close();
204 theInputStream
= null;
208 catch (IOException x
)
214 * Insert text (command(s)) as if it were typed by the user.
215 * The text will be sent to the Console's input stream. Any text already
216 * typed by the user will not be included in the text sent.
217 * @param text text to send
219 public void insertTextAsIfTypedByUser(String text
)
221 if (!isEditable()) throw new RuntimeException("Cannot send text to non-editable console");
222 if (!text
.endsWith("\n")) text
+= "\n";
227 int pos
= text
.indexOf('\n');
228 String firstLine
= text
.substring(0, pos
);
229 // Take anything currently on the input line and move it to the end of the pasteAhead buffer
230 String originalText
= lastLine();
231 Document doc
= getDocument();
232 doc
.remove(start
, doc
.getLength() - start
);
233 doc
.insertString(start
, firstLine
+ "\n", null);
234 sendToInputStream(firstLine
);
235 pasteAhead
.append(text
.substring(pos
+1));
236 pasteAhead
.append(originalText
);
240 x
.printStackTrace(); // Should never happen
245 pasteAhead
.append(text
);
246 if (!text
.endsWith("\n")) pasteAhead
.append('\n');
259 Transferable t
= getToolkit().getSystemClipboard().getContents(this);
260 String text
= (String
) t
.getTransferData(DataFlavor
.stringFlavor
);
261 text
= removeIllegalCharacters(text
);
264 int pos
= text
.indexOf('\n');
265 boolean containsEOL
= pos
>= 0;
266 String firstLine
= containsEOL ? text
.substring(0, pos
) : text
;
268 // If there is a current selection _in the editable area_ delete it
269 int sStart
= Math
.max(start
,getSelectionStart());
270 int sEnd
= Math
.max(start
,getSelectionEnd());
271 if (sEnd
-sStart
> 0) getDocument().remove(sStart
,sEnd
-sStart
);
272 // Add first line of pasted text to any current added text, at the insertion point
273 int cPos
= getCaretPosition();
275 // If there is a new line in the pasted text, queue the whole line.
278 getDocument().insertString(cPos
, firstLine
+ "\n", null);
279 sendToInputStream(firstLine
);
280 pasteAhead
.append(text
.substring(pos
+ 1));
284 getDocument().insertString(cPos
, firstLine
, null);
289 pasteAhead
.append(text
);
298 private String
removeIllegalCharacters(String in
)
300 StringBuffer out
= null;
302 for (int i
=0; i
<l
; i
++)
304 char c
= in
.charAt(i
);
305 if (c
< ' ' && c
!= '\n')
309 out
= new StringBuffer(in
.substring(0,i
));
312 else if (out
!= null) out
.append(c
);
314 if (out
== null) return in
;
317 return out
.toString();
321 * A method to be called to request that the console be closed. Typically called by
322 * scripting engines when the user types "quit" or the equivalent. This close method
323 * does nothing, but is designed to be overriden by subclasses.
328 /** Remove a listener for CTRL^C events.
329 * @param l The listener to remove
331 public void removeInterruptListener(ActionListener l
)
333 listenerList
.remove(ActionListener
.class, l
);
336 /** Called when CTRL^C is detected. Calls all the registered listeners. */
337 protected void fireInterruptAction()
339 int count
= listenerList
.getListenerCount(ActionListener
.class);
342 ActionEvent event
= new ActionEvent(this, ActionEvent
.ACTION_FIRST
, "Break");
343 ActionListener
[] listeners
= (ActionListener
[]) listenerList
.getListeners(ActionListener
.class);
344 for (int i
= listeners
.length
; i
-- > 0;)
346 listeners
[i
].actionPerformed(event
);
351 * Clears any output from the Console
357 Document d
= getDocument();
358 d
.remove(0, d
.getLength());
367 catch (BadLocationException x
)
372 private void setLastLine(String line
)
376 int end
= getDocument().getLength();
379 getDocument().remove(start
, end
- start
);
381 getDocument().insertString(start
, line
, null);
383 catch (BadLocationException x
)
389 private void consume(KeyEvent event
)
395 private void prepareToFlush()
401 // Take anything currently on the input line and move it to the typeAhead buffer
402 typeAhead
.append(lastLine());
403 Document doc
= getDocument();
404 doc
.remove(startLastLine
, doc
.getLength() - startLastLine
);
406 // TODO: Fixme, what if temporary prompt was set! Plus we dont want to add the
407 // initial entry again, as it was already put into the typeahead.
408 writePrompt(theInputStream
.getCurrentPrompt(), null);
412 x
.printStackTrace(); // Should never happen
424 list
= new ArrayList();
428 Document doc
= getDocument();
429 for (Iterator i
= old
.iterator(); i
.hasNext();)
431 ListEntry entry
= (ListEntry
) i
.next();
432 doc
.insertString(doc
.getLength(), entry
.string
.toString(), entry
.set
);
434 setCaretPosition(doc
.getLength());
436 catch (BadLocationException x
)
441 private boolean inLastLine()
443 int caret
= getCaretPosition();
444 return caret
>= start
;
447 private String
lastLine()
451 Document doc
= getDocument();
452 return doc
.getText(start
, doc
.getLength() - start
);
454 catch (BadLocationException x
)
460 private void sendToInputStream(String line
)
462 if (log
) logStream
.println(currentPrompt
+line
);
465 boolean wasEmpty
= queue
.isEmpty();
466 queue
.add(line
+ "\n");
467 if (line
.length() > 0)
471 while (history
.size() >= maxHistory
) history
.remove(0);
472 historyPos
= history
.size();
476 waitingForInput
= false;
480 // Overriden to make sure text entered at the keyboard doesnt inherit the prompt color
481 public MutableAttributeSet
getInputAttributes() {
484 private void writeMessage(String line
, AttributeSet style
)
488 Document doc
= getDocument();
489 doc
.insertString(doc
.getLength(), line
, style
== null ? defStyle
: style
);
490 setCaretPosition(doc
.getLength());
492 catch (BadLocationException x
)
496 // This method can be called on any thread, so rather than directly updating the
497 // console data is added to a queue (list).
498 private void writeOutput(String s
, AttributeSet set
) throws IOException
502 boolean wasEmpty
= list
.isEmpty();
503 // If the attributes are the same as last time, simply append to
504 // existing ListEntry, otherwise create a new entry.
505 if ((last
!= null) && (set
== last
.set
))
507 last
.string
.append(s
);
511 list
.add(last
= new ListEntry(s
, set
));
520 private void writePrompt(String prompt
, String initialEntry
)
522 flush(); // Flush any output still to be sent to the console
523 startLastLine
= getDocument().getLength();
526 Rectangle r
= modelToView(startLastLine
);
527 Insets insets
= this.getInsets();
528 if (r
.x
> insets
.left
)
530 writeMessage("\n", defStyle
);
531 startLastLine
= getDocument().getLength();
534 catch (BadLocationException x
) {} // Can't happen
535 currentPrompt
= prompt
;
538 writeMessage(prompt
, promptStyle
);
540 start
= getDocument().getLength();
542 if (pasteAhead
.length() > 0)
544 int pos
= pasteAhead
.indexOf("\n");
545 boolean hasEOL
= pos
>= 0;
548 String ta
= pasteAhead
.substring(0, pos
);
549 writeMessage(ta
+ "\n", null);
550 pasteAhead
.delete(0, pos
+ 1);
552 String text
= lastLine();
553 setCaretPosition(getDocument().getLength());
554 text
= text
.substring(0, text
.length() - 1); // trim \n
555 sendToInputStream(text
);
556 return; // without setting waitingForInput to true
560 writeMessage(pasteAhead
.toString(), null);
561 pasteAhead
.setLength(0);
562 if (typeAhead
.length() > 0)
564 writeMessage(typeAhead
.toString(), null);
565 typeAhead
.setLength(0);
571 if (initialEntry
!= null)
573 writeMessage(initialEntry
, null);
575 if (typeAhead
.length() > 0)
577 writeMessage(typeAhead
.toString(), null);
578 typeAhead
.setLength(0);
582 // Normally the cursor is automatically made visible when the focus is
583 // gained. However if setEditable(true) is done when we already have the
584 // focus then this doesn't work, so...
585 if (!getCaret().isVisible() && hasFocus()) getCaret().setVisible(true);
586 waitingForInput
= true;
589 /** Getter for property promptColor.
590 * @return Value of property promptColor.
593 public Color
getPromptColor()
595 return (Color
) promptStyle
.getAttribute(StyleConstants
.Foreground
);
598 /** Setter for property promptColor.
599 * @param promptColor New value of property promptColor.
602 public void setPromptColor(Color promptColor
)
604 promptStyle
.addAttribute(StyleConstants
.Foreground
, promptColor
);
607 public void addNotify()
610 requestFocusInWindow();
613 private class CInputStream
extends ConsoleInputStream
implements Runnable
616 private byte[] buffer
;
618 CInputStream(String prompt
)
623 public int read() throws IOException
625 if ((buffer
== null) || (pos
>= buffer
.length
))
627 if (!fillBuffer()) return -1;
630 return buffer
[pos
++];
633 public int read(byte[] b
, int off
, int len
) throws IOException
635 if ((buffer
== null) || (pos
>= buffer
.length
))
637 if (!fillBuffer()) return -1;
640 int l
= Math
.min(len
, buffer
.length
- pos
);
641 System
.arraycopy(buffer
, pos
, b
, off
, l
);
649 writePrompt(getCurrentPrompt(), getInitialEntry());
651 // Called by the input stream when it needs more input
652 private boolean fillBuffer() throws IOException
656 while (queue
.isEmpty())
660 SwingUtilities
.invokeLater(this);
662 // In case someone disposed of the console while we were waiting
663 if (theInputStream
== null) return false;
665 catch (InterruptedException x
)
667 throw new InterruptedIOException();
671 String line
= queue
.remove(0).toString();
672 buffer
= line
.getBytes("ISO-8859-1");
679 private class CKeyListener
extends KeyAdapter
implements ActionListener
681 public void keyPressed(KeyEvent keyEvent
)
683 if ((keyEvent
.getKeyCode() == KeyEvent
.VK_C
) && ((keyEvent
.getModifiers() & keyEvent
.CTRL_MASK
) != 0))
685 fireInterruptAction();
687 else if (keyEvent
.getKeyCode() == KeyEvent
.VK_ENTER
)
689 if (!waitingForInput
)
695 setCaretPosition(getDocument().getLength());
698 else if (!inLastLine())
700 keyEvent
.consume(); // No Beep
702 else if (keyEvent
.getKeyCode() == keyEvent
.VK_LEFT
)
704 if (getCaretPosition() == start
)
709 else if (keyEvent
.getKeyCode() == KeyEvent
.VK_UP
)
717 String line
= history
.get(--historyPos
).toString();
722 else if (keyEvent
.getKeyCode() == KeyEvent
.VK_DOWN
)
724 if (historyPos
>= (history
.size() - 1))
730 String line
= history
.get(++historyPos
).toString();
737 public void keyTyped(KeyEvent keyEvent
)
743 setCaretPosition(getDocument().getLength());
747 // if the selection spans more than the current line (for instance the prompt)
748 // Then we need to remove the extra region
749 Caret caret
= getCaret();
750 if (caret
.getMark() < start
)
752 int pos
= caret
.getDot();
757 if (keyEvent
.getKeyChar() == KeyEvent
.VK_BACK_SPACE
)
759 int cPos
= getCaretPosition();
765 else if (keyEvent
.getKeyChar() == keyEvent
.VK_ENTER
)
767 theInputStream
.clearOneTimePrompt();
768 String text
= lastLine();
769 setCaretPosition(getDocument().getLength());
770 text
= text
.substring(0, text
.length() - 1); // trim \n
771 sendToInputStream(text
);
776 if (keyEvent
.getKeyChar() == KeyEvent
.VK_BACK_SPACE
)
778 int l
= typeAhead
.length();
781 typeAhead
.deleteCharAt(l
- 1);
784 else if (keyEvent
.getKeyChar() == keyEvent
.VK_ENTER
)
786 // Move the contents of the typeAhead buffer to the pasteAhead buffer
787 pasteAhead
.append(typeAhead
);
788 pasteAhead
.append('\n');
789 typeAhead
.setLength(0);
791 else if ((keyEvent
.getModifiers() & keyEvent
.CTRL_MASK
) == 0)
793 typeAhead
.append(keyEvent
.getKeyChar());
798 // called (on event thread), when new output to be written to console.
799 public void actionPerformed(ActionEvent e
)
801 // If there is stuff which still needs to be flushed, then flush it
802 if (!list
.isEmpty()) prepareToFlush();
806 class COutputStream
extends ConsoleOutputStream
808 COutputStream(AttributeSet set
, boolean autoRun
)
813 public void write(byte[] b
, int off
, int len
, AttributeSet set
) throws IOException
815 if (log
) logStream
.write(b
,off
,len
);
816 writeOutput(new String(b
, off
, len
), set
);
820 private static class ListEntry
825 ListEntry(String string
, AttributeSet set
)
827 this.string
= new StringBuffer(string
);
831 private static class MyTimer
extends Timer
implements Runnable
833 MyTimer(ActionListener l
)
844 SwingUtilities
.invokeLater(this);