001    // GraphLab Project: http://graphlab.sharif.edu
002    // Copyright (C) 2008 Mathematical Science Department of Sharif University of Technology
003    // Distributed under the terms of the GNU General Public License (GPL): http://www.gnu.org/licenses/
004    
005    package graphlab.plugins.commandline;
006    
007    import bsh.ConsoleInterface;
008    import bsh.util.GUIConsoleInterface;
009    import bsh.util.NameCompletion;
010    import graphlab.plugins.commandline.parsers.DefaultParser;
011    import graphlab.plugins.commandline.util.Utils;
012    
013    import javax.swing.*;
014    import javax.swing.text.*;
015    import java.awt.*;
016    import java.awt.event.*;
017    import java.beans.PropertyChangeEvent;
018    import java.beans.PropertyChangeListener;
019    import java.io.*;
020    import java.util.Vector;
021    
022    public class ShellConsole extends JScrollPane
023            implements GUIConsoleInterface, Runnable, KeyListener,
024            MouseListener, ActionListener, PropertyChangeListener {
025        private final static String CUT = "Cut";
026        private final static String COPY = "Copy";
027        private final static String PASTE = "Paste";
028    
029        private OutputStream outPipe;
030        private InputStream inPipe;
031        private InputStream in;
032        private PrintStream out;
033        public Shell shell;
034    
035        public InputStream getInputStream() {
036            return in;
037        }
038    
039        public Reader getIn() {
040            return new InputStreamReader(in);
041        }
042    
043        public PrintStream getOut() {
044            return out;
045        }
046    
047        public PrintStream getErr() {
048            return out;
049        }
050    
051        private int cmdStart = 0;
052        private Vector history = new Vector();
053        private String startedLine;
054        private int histLine = 0;
055    
056        private JPopupMenu menu;
057        private JTextPane text;
058        private DefaultStyledDocument doc;
059    
060        NameCompletion nameCompletion;
061        final int SHOW_AMBIG_MAX = 15;
062    
063        // hack to prevent key repeat for some reason?
064        private boolean gotUp = true;
065    
066        public ShellConsole() {
067            this(null, null);
068        }
069    
070        public ConsoleInterface console_interface;
071        public boolean is_interface = false;
072    
073        public void set(ConsoleInterface ci) {
074            console_interface = ci;
075            is_interface = true;
076        }
077    
078        public ShellConsole(InputStream cin, OutputStream cout) {
079            super();
080            //  parser.put(dp.getName(), dp);
081            is_interface = false;
082    
083            // Special TextPane which catches for cut and paste, both L&F keys and
084            // programmatic behaviour
085            text = new JTextPane(doc = new DefaultStyledDocument()) {
086                public void cut() {
087                    if (text.getCaretPosition() < cmdStart) {
088                        super.copy();
089                    } else {
090                        super.cut();
091                    }
092                }
093    
094                public void paste() {
095                    forceCaretMoveToEnd();
096                    super.paste();
097                }
098            };
099    
100    
101            Font font = new Font("Monospaced", Font.PLAIN, 12);
102    
103            text.setText("");
104            text.setFont(font);
105    
106            text.setMargin(new Insets(7, 5, 7, 5));
107            text.addKeyListener(this);
108            setViewportView(text);
109    
110            // create popup menu
111            menu = new JPopupMenu("MyConsole        Menu");
112            menu.add(new JMenuItem(CUT)).addActionListener(this);
113            menu.add(new JMenuItem(COPY)).addActionListener(this);
114            menu.add(new JMenuItem(PASTE)).addActionListener(this);
115    
116            text.addMouseListener(this);
117    
118            // make sure popup menu follows Look & Feel
119            UIManager.addPropertyChangeListener(this);
120    
121            outPipe = cout;
122            if (outPipe == null) {
123                outPipe = new PipedOutputStream();
124                try {
125                    in = new PipedInputStream((PipedOutputStream) outPipe);
126                } catch (IOException e) {
127                    print("Console internal error (1)...", Color.red);
128                }
129            }
130    
131            inPipe = cin;
132            if (inPipe == null) {
133                PipedOutputStream pout = new PipedOutputStream();
134                out = new PrintStream(pout);
135                try {
136                    inPipe = new BlockingPipedInputStream(pout);
137                } catch (IOException e) {
138                    print("Console internal error: " + e);
139                }
140            }
141            // Start the inpipe watcher
142            new Thread(this).start();
143    
144            requestFocus();
145        }
146    
147        public void requestFocus() {
148            super.requestFocus();
149            text.requestFocus();
150        }
151    
152        public void keyPressed(KeyEvent e) {
153            type(e);
154            gotUp = false;
155        }
156    
157        public void keyTyped(KeyEvent e) {
158            type(e);
159        }
160    
161        public void keyReleased(KeyEvent e) {
162            gotUp = true;
163            type(e);
164        }
165    
166        private synchronized void type(KeyEvent e) {
167            switch (e.getKeyCode()) {
168                case(KeyEvent.VK_ENTER):
169                    if (e.getID() == KeyEvent.KEY_PRESSED) {
170                        if (gotUp) {
171                            enter();
172                            resetCommandStart();
173                            text.setCaretPosition(cmdStart);
174                        }
175                    }
176                    e.consume();
177                    text.repaint();
178                    break;
179    
180                case(KeyEvent.VK_UP):
181                    if (e.getID() == KeyEvent.KEY_PRESSED) {
182                        historyUp();
183                    }
184                    e.consume();
185                    break;
186    
187                case(KeyEvent.VK_DOWN):
188                    if (e.getID() == KeyEvent.KEY_PRESSED) {
189                        historyDown();
190                    }
191                    e.consume();
192                    break;
193    
194                case(KeyEvent.VK_LEFT):
195                case(KeyEvent.VK_BACK_SPACE):
196                case(KeyEvent.VK_DELETE):
197                    if (text.getCaretPosition() <= cmdStart) {
198                        // This doesn't work for backspace.
199                        // See default case for workaround
200                        e.consume();
201                    }
202                    break;
203    
204                case(KeyEvent.VK_RIGHT):
205                    forceCaretMoveToStart();
206                    break;
207    
208                case(KeyEvent.VK_HOME):
209                    text.setCaretPosition(cmdStart);
210                    e.consume();
211                    break;
212    
213                case(KeyEvent.VK_U):    // clear line
214                    if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
215                        replaceRange("", cmdStart, textLength());
216                        histLine = 0;
217                        e.consume();
218                    }
219                    break;
220    
221                case(KeyEvent.VK_ALT):
222                case(KeyEvent.VK_CAPS_LOCK):
223                case(KeyEvent.VK_CONTROL):
224                case(KeyEvent.VK_META):
225                case(KeyEvent.VK_SHIFT):
226                case(KeyEvent.VK_PRINTSCREEN):
227                case(KeyEvent.VK_SCROLL_LOCK):
228                case(KeyEvent.VK_PAUSE):
229                case(KeyEvent.VK_INSERT):
230                case(KeyEvent.VK_F1):
231                case(KeyEvent.VK_F2):
232                case(KeyEvent.VK_F3):
233                case(KeyEvent.VK_F4):
234                case(KeyEvent.VK_F5):
235                case(KeyEvent.VK_F6):
236                case(KeyEvent.VK_F7):
237                case(KeyEvent.VK_F8):
238                case(KeyEvent.VK_F9):
239                case(KeyEvent.VK_F10):
240                case(KeyEvent.VK_F11):
241                case(KeyEvent.VK_F12):
242                case(KeyEvent.VK_ESCAPE):
243    
244                    // only modifier pressed
245                    break;
246    
247                    // Control-C
248                case(KeyEvent.VK_C):
249                    if (text.getSelectedText() == null) {
250                        if (((e.getModifiers() & InputEvent.CTRL_MASK) > 0)
251                                && (e.getID() == KeyEvent.KEY_PRESSED)) {
252                            append("^C");
253                        }
254                        e.consume();
255                    }
256                    break;
257    
258                case(KeyEvent.VK_TAB):
259                    if (e.getID() == KeyEvent.KEY_RELEASED) {
260                        String part = text.getText().substring(cmdStart);
261                        doCommandCompletion(part);
262                    }
263                    e.consume();
264                    break;
265    
266                default:
267                    if (
268                            (e.getModifiers() &
269                                    (InputEvent.CTRL_MASK
270                                            | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0) {
271                        // plain character
272                        forceCaretMoveToEnd();
273                    }
274    
275                    /*
276                            The getKeyCode function always returns VK_UNDEFINED for
277                            keyTyped events, so backspace is not fully consumed.
278                        */
279                    if (e.paramString().indexOf("Backspace") != -1) {
280                        if (text.getCaretPosition() <= cmdStart) {
281                            e.consume();
282                            break;
283                        }
284                    }
285    
286                    break;
287            }
288        }
289    
290        public void clear() {
291            text.setText("");
292            text.repaint();
293        }
294    
295        private void doCommandCompletion(String part) {
296            String bk = part;
297            if (nameCompletion == null)
298                return;
299    
300            int i = part.length() - 1;
301    
302            // Character.isJavaIdentifierPart()  How convenient for us!!
303            while (
304                    i >= 0 &&
305                            (Character.isJavaIdentifierPart(part.charAt(i))
306                                    || part.charAt(i) == '.' || part.charAt(i) == '('
307                                    || part.charAt(i) == ')' || part.charAt(i) == '[' || part.charAt(i) == ']')
308                    )
309                i--;
310    
311    
312            part = part.substring(i + 1);
313            if (part.length() < 1)  // reasonable completion length
314                return;
315    
316    
317            int index = Math.max(bk.lastIndexOf("bsh % "), bk.lastIndexOf(">>  "));
318            if (index <= 0) index = 0;
319            else index = Math.max(bk.lastIndexOf("bsh % ") + 6, bk.lastIndexOf(">>  ") + 4);
320            String ret = bk.substring(index, i + 1);
321            ret.trim();
322            // no completion
323            String[] complete = nameCompletion.completeName(part);
324            if (complete.length == 0) {
325                java.awt.Toolkit.getDefaultToolkit().beep();
326                return;
327            }
328    
329            // Found one completion (possibly what we already have)
330            if (complete.length == 1 && !complete.equals(part)) {
331                if (part.endsWith("(")) {
332                    if (complete[0].equals(part + ");")) {
333                        String append = complete[0].substring(part.length());
334                        append(append);
335                        return;
336                    }
337    
338                } else if (!part.startsWith("_")) {
339                    String append = complete[0].substring(part.length()) + "(";
340                    append(append);
341                    return;
342                } else {
343                    if (!complete[0].startsWith("_")) {
344                        text.select(textLength() - part.length(), textLength());
345                        text.replaceSelection(complete[0]);
346                        return;
347                    }
348                }
349            }
350    
351            // Found ambiguous, show (some of) them
352            String line = text.getText();
353    //        String command = line.substring(cmdStart);
354            // Find prompt
355            for (i = cmdStart; line.charAt(i) != '\n' && i > 0; i--) ;
356            String prompt = ">>  ";//line.substring( i+1, cmdStart );
357    
358            // Show ambiguous
359            StringBuffer sb = new StringBuffer("\n");
360            for (i = 0; i < complete.length && i < SHOW_AMBIG_MAX; i++)
361                sb.append(complete[i] + "\n");
362            if (i == SHOW_AMBIG_MAX)
363                sb.append("...\n");
364    
365            print(sb, Color.blue);
366            print(prompt); // print resets command start
367            if (complete.length != 1)
368                append(ret + Utils.getMaximumSimilarities(complete));
369            else append(ret + part);
370        }
371    
372        private void resetCommandStart() {
373            cmdStart = textLength();
374        }
375    
376        private void append(String string) {
377    
378            int slen = textLength();
379            text.select(slen, slen);
380            text.replaceSelection(string);
381        }
382    
383        private String replaceRange(Object s, int start, int end) {
384            String st = s.toString();
385            text.select(start, end);
386            text.replaceSelection(st);
387            //text.repaint();
388            return st;
389        }
390    
391        private void forceCaretMoveToEnd() {
392            if (text.getCaretPosition() < cmdStart) {
393                // move caret first!
394                text.setCaretPosition(textLength());
395            }
396            text.repaint();
397        }
398    
399        private void forceCaretMoveToStart() {
400            if (text.getCaretPosition() < cmdStart) {
401                // move caret first!
402            }
403            text.repaint();
404        }
405    
406    
407        private void enter() {
408            String s = getCmd();
409    
410            if (s.length() == 0)    // special hack for empty return!
411                s = ";\n";
412            else {
413                history.addElement(s);
414                s = s + "\n";
415            }
416    
417            append("\n");
418            histLine = 0;
419            acceptLine(s);
420            text.repaint();
421        }
422    
423        private String getCmd() {
424            String s = "";
425            try {
426                s = text.getText(cmdStart, textLength() - cmdStart);
427            } catch (BadLocationException e) {
428                // should not happen
429                System.out.println("Internal MyConsole Error: " + e);
430            }
431            return s;
432        }
433    
434        private void historyUp() {
435            if (history.size() == 0)
436                return;
437            if (histLine == 0)  // save current line
438                startedLine = getCmd();
439            if (histLine < history.size()) {
440                histLine++;
441                showHistoryLine();
442            }
443        }
444    
445        private void historyDown() {
446            if (histLine == 0)
447                return;
448    
449            histLine--;
450            showHistoryLine();
451        }
452    
453        private void showHistoryLine() {
454            String showline;
455            if (histLine == 0)
456                showline = startedLine;
457            else
458                showline = (String) history.elementAt(history.size() - histLine);
459    
460            replaceRange(showline, cmdStart, textLength());
461            text.setCaretPosition(textLength());
462            text.repaint();
463        }
464    
465        String ZEROS = "000";
466    
467        //boolean is_equal = ;
468    
469        //HashMap<String, ExtParser> parser = new HashMap<String, ExtParser>();
470        //ExtParser defaultExtParser;
471    
472        boolean is_accepted = true;
473        String me_buffered = "";
474    
475        private void acceptLine(String line) {
476            if (line.contains(";"))
477                if (!is_accepted) {
478                    line = me_buffered + line;
479                    me_buffered = "";
480                    is_accepted = true;
481                } else me_buffered = "";
482    
483            else {
484                me_buffered += line;
485                is_accepted = false;
486                return;
487            }
488            line = new DefaultParser(shell).parse(line);
489    
490            // Patch to handle Unicode characters
491            // Submitted by Daniel Leuck
492            StringBuffer buf = new StringBuffer();
493            int lineLength = line.length();
494            for (int i = 0; i < lineLength; i++) {
495                String val = Integer.toString(line.charAt(i), 16);
496                val = ZEROS.substring(0, 4 - val.length()) + val;
497                buf.append("\\u" + val);
498            }
499            line = buf.toString();
500            // End unicode patch
501    
502            if (outPipe == null)
503                print("Console internal     error: cannot output ...", Color.red);
504            else
505                try {
506                    outPipe.write(line.getBytes());
507                    outPipe.flush();
508                } catch (IOException e) {
509                    outPipe = null;
510                    throw new RuntimeException("Console pipe broken...");
511                }
512            text.repaint();
513        }
514    
515        public void println(Object o) {
516            if (is_interface) {
517                console_interface.println(o);
518                return;
519            }
520    
521            print(String.valueOf(o) + "\n");
522            text.repaint();
523        }
524    
525        public void print(final Object o) {
526            if (is_interface) {
527                console_interface.print(o);
528                return;
529            }
530            invokeAndWait(new Runnable() {
531                public void run() {
532                    append(String.valueOf(o));
533                    resetCommandStart();
534                    text.setCaretPosition(cmdStart);
535                }
536            });
537        }
538    
539        public Color getResultColor() {
540            return Color.blue;
541        }
542    
543        public void printResult(Object s) {
544            print(s, getResultColor());
545        }
546    
547        public void printlnResult(Object s) {
548            println(s, getResultColor());
549        }
550    
551        /**
552         * Prints "\\n" (i.e. newline)
553         */
554        public void println() {
555            print("\n");
556            text.repaint();
557        }
558    
559        public void error(Object o) {
560            if (is_interface) {
561                console_interface.error(o);
562                return;
563            }
564    
565            print("err: " + o, Color.red);
566            println();
567        }
568    
569        public void println(Icon icon) {
570            print(icon);
571            println();
572            text.repaint();
573        }
574    
575        public void print(final Icon icon) {
576            if (icon == null)
577                return;
578    
579            invokeAndWait(new Runnable() {
580                public void run() {
581                    text.insertIcon(icon);
582                    resetCommandStart();
583                    text.setCaretPosition(cmdStart);
584                }
585            });
586        }
587    
588        public void print(Object s, Font font) {
589            if (is_interface) {
590                print(s);
591                return;
592            }
593            print(s, font, null);
594        }
595    
596        public void println(Object s, Color color) {
597            if (is_interface) {
598                println(s);
599                return;
600            }
601            print(s, null, color);
602            println();
603        }
604    
605        public void print(Object s, Color color) {
606            if (is_interface) {
607                print(s);
608                return;
609            }
610            print(s, null, color);
611        }
612    
613        public void print(final Object o, final Font font, final Color color) {
614            invokeAndWait(new Runnable() {
615                public void run() {
616                    AttributeSet old = getStyle();
617                    setStyle(font, color);
618                    append(String.valueOf(o));
619                    resetCommandStart();
620                    text.setCaretPosition(cmdStart);
621                    setStyle(old, true);
622                }
623            });
624        }
625    
626        public void print(
627                Object s,
628                String fontFamilyName,
629                int size,
630                Color color
631        ) {
632    
633            print(s, fontFamilyName, size, color, false, false, false);
634        }
635    
636        public void print(
637                final Object o,
638                final String fontFamilyName,
639                final int size,
640                final Color color,
641                final boolean bold,
642                final boolean italic,
643                final boolean underline
644        ) {
645            invokeAndWait(new Runnable() {
646                public void run() {
647                    AttributeSet old = getStyle();
648                    setStyle(fontFamilyName, size, color, bold, italic, underline);
649                    append(String.valueOf(o));
650                    resetCommandStart();
651                    text.setCaretPosition(cmdStart);
652                    setStyle(old, true);
653                }
654            });
655        }
656    
657        private AttributeSet setStyle(Font font) {
658            return setStyle(font, null);
659        }
660    
661        private AttributeSet setStyle(Color color) {
662            return setStyle(null, color);
663        }
664    
665        private AttributeSet setStyle(Font font, Color color) {
666            if (font != null)
667                return setStyle(font.getFamily(), font.getSize(), color,
668                        font.isBold(), font.isItalic(),
669                        StyleConstants.isUnderline(getStyle()));
670            else
671                return setStyle(null, -1, color);
672        }
673    
674        private AttributeSet setStyle(
675                String fontFamilyName, int size, Color color) {
676            MutableAttributeSet attr = new SimpleAttributeSet();
677            if (color != null)
678                StyleConstants.setForeground(attr, color);
679            if (fontFamilyName != null)
680                StyleConstants.setFontFamily(attr, fontFamilyName);
681            if (size != -1)
682                StyleConstants.setFontSize(attr, size);
683    
684            setStyle(attr);
685    
686            return getStyle();
687        }
688    
689        private AttributeSet setStyle(
690                String fontFamilyName,
691                int size,
692                Color color,
693                boolean bold,
694                boolean italic,
695                boolean underline
696        ) {
697            MutableAttributeSet attr = new SimpleAttributeSet();
698            if (color != null)
699                StyleConstants.setForeground(attr, color);
700            if (fontFamilyName != null)
701                StyleConstants.setFontFamily(attr, fontFamilyName);
702            if (size != -1)
703                StyleConstants.setFontSize(attr, size);
704            StyleConstants.setBold(attr, bold);
705            StyleConstants.setItalic(attr, italic);
706            StyleConstants.setUnderline(attr, underline);
707    
708            setStyle(attr);
709    
710            return getStyle();
711        }
712    
713        private void setStyle(AttributeSet attributes) {
714            setStyle(attributes, false);
715        }
716    
717        private void setStyle(AttributeSet attributes, boolean overWrite) {
718            text.setCharacterAttributes(attributes, overWrite);
719        }
720    
721        private AttributeSet getStyle() {
722            return text.getCharacterAttributes();
723        }
724    
725        public void setFont(Font font) {
726            super.setFont(font);
727    
728            if (text != null)
729                text.setFont(font);
730        }
731    
732        private void inPipeWatcher() throws IOException {
733            byte[] ba = new byte[256]; //   arbitrary blocking factor
734            int read;
735            while ((read = inPipe.read(ba)) != -1) {
736                print(new String(ba, 0, read));
737                //text.repaint();
738            }
739    
740            println("Console: Input closed...");
741        }
742    
743        public void run() {
744            try {
745                inPipeWatcher();
746            } catch (IOException e) {
747                print("Console: I/O Error: " + e + "\n", Color.red);
748            }
749        }
750    
751        public String toString() {
752            return "BeanShell console";
753        }
754    
755        // MouseListener Interface
756        public void mouseClicked(MouseEvent event) {
757        }
758    
759        public void mousePressed(MouseEvent event) {
760            if (event.isPopupTrigger()) {
761                menu.show(
762                        (Component) event.getSource(), event.getX(), event.getY());
763            }
764        }
765    
766        public void mouseReleased(MouseEvent event) {
767            if (event.isPopupTrigger()) {
768                menu.show((Component) event.getSource(), event.getX(),
769                        event.getY());
770            }
771            text.repaint();
772        }
773    
774        public void mouseEntered(MouseEvent event) {
775        }
776    
777        public void mouseExited(MouseEvent event) {
778        }
779    
780        // property change
781        public void propertyChange(PropertyChangeEvent event) {
782            if (event.getPropertyName().equals("lookAndFeel")) {
783                SwingUtilities.updateComponentTreeUI(menu);
784            }
785        }
786    
787        // handle cut, copy and paste
788        public void actionPerformed(ActionEvent event) {
789            String cmd = event.getActionCommand();
790            if (cmd.equals(CUT)) {
791                text.cut();
792            } else if (cmd.equals(COPY)) {
793                text.copy();
794            } else if (cmd.equals(PASTE)) {
795                text.paste();
796            }
797        }
798    
799        /**
800         * If not in the event thread run via SwingUtilities.invokeAndWait()
801         */
802        private void invokeAndWait(Runnable run) {
803            if (!SwingUtilities.isEventDispatchThread()) {
804                try {
805                    SwingUtilities.invokeAndWait(run);
806                } catch (Exception e) {
807                    // shouldn't happen
808                    e.printStackTrace();
809                }
810            } else {
811                run.run();
812            }
813        }
814    
815        /**
816         * The overridden read method in this class will not throw "Broken pipe"
817         * IOExceptions;  It will simply wait for new writers and data.
818         * This is used by the MyConsole internal read thread to allow writers
819         * in different (and in particular ephemeral) threads to write to the pipe.
820         * <p/>
821         * It also checks a little more frequently than the original read().
822         * <p/>
823         * Warning: read() will not even error on a read to an explicitly closed
824         * pipe (override closed to for that).
825         */
826        public static class BlockingPipedInputStream extends PipedInputStream {
827            boolean closed;
828    
829            public BlockingPipedInputStream(PipedOutputStream pout)
830                    throws IOException {
831                super(pout);
832            }
833    
834            public synchronized int read() throws IOException {
835                if (closed)
836                    throw new IOException("stream closed");
837    
838                while (super.in < 0) {    // While no data */
839                    notifyAll();    // Notify any writers to wake up
840                    try {
841                        wait(750);
842                    } catch (InterruptedException e) {
843                        throw new InterruptedIOException();
844                    }
845                }
846                // This is what the superclass does.
847                int ret = buffer[super.out++] & 0xFF;
848                if (super.out >= buffer.length)
849                    super.out = 0;
850                if (super.in == super.out)
851                    super.in = -1;  /* now empty */
852                return ret;
853            }
854    
855            public void close() throws IOException {
856                closed = true;
857                super.close();
858            }
859        }
860    
861        public void setNameCompletion(NameCompletion nc) {
862            this.nameCompletion = nc;
863        }
864    
865        public void setWaitFeedback(boolean on) {
866            if (on)
867                setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
868            else
869                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
870        }
871    
872        private int textLength() {
873            return text.getDocument().getLength();
874        }
875    
876    }
877    
878    
879