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