001/* 002 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * bstefanescu 016 */ 017package org.nuxeo.shell.swing; 018 019import java.awt.Color; 020import java.awt.Font; 021import java.awt.Insets; 022import java.awt.Toolkit; 023import java.awt.event.KeyEvent; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.Writer; 027import java.lang.reflect.Method; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map; 031 032import javax.swing.JTextArea; 033 034import jline.ConsoleReader; 035import jline.History; 036 037import org.nuxeo.shell.Shell; 038import org.nuxeo.shell.cmds.ConsoleReaderFactory; 039import org.nuxeo.shell.swing.widgets.HistoryFinder; 040 041/** 042 * The conversation with jline ConsoleReader is limited to execute a command and get the command output. All the other 043 * detials like typing, auto completion, moving cursor, history etc. is using pure swing code without any transfer 044 * between the jline console reader and the swing component. 045 * 046 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 047 */ 048@SuppressWarnings("serial") 049public class Console extends JTextArea implements ConsoleReaderFactory { 050 051 protected Theme theme; 052 053 protected ConsoleReader reader; 054 055 protected final In in; 056 057 protected final Writer out; 058 059 protected CmdLine cline; 060 061 protected Method complete; 062 063 protected HistoryFinder finder; 064 065 /** 066 * If not null should use a mask when typing 067 */ 068 protected Character mask; 069 070 protected StringBuilder pwd; 071 072 public Console() throws Exception { 073 setMargin(new Insets(6, 6, 6, 6)); 074 setEditable(true); 075 in = new In(); 076 out = new Out(); 077 reader = new ConsoleReader(in, out, null, new SwingTerminal(this)); 078 reader.setCompletionHandler(new SwingCompletionHandler(this)); 079 complete = reader.getClass().getDeclaredMethod("complete"); 080 complete.setAccessible(true); 081 Shell shell = Shell.get(); 082 shell.putContextObject(Console.class, this); 083 registerThemes(shell); 084 registerCommands(shell); 085 } 086 087 protected void registerCommands(Shell shell) { 088 shell.getRegistry("config").addAnnotatedCommand(org.nuxeo.shell.swing.cmds.FontCommand.class); 089 shell.getRegistry("config").addAnnotatedCommand(org.nuxeo.shell.swing.cmds.ThemeCommand.class); 090 shell.getRegistry("config").addAnnotatedCommand(org.nuxeo.shell.swing.cmds.ColorCommand.class); 091 shell.getRegistry("config").addAnnotatedCommand(org.nuxeo.shell.swing.cmds.BgColorCommand.class); 092 } 093 094 protected void registerThemes(Shell shell) { 095 int len = "theme.".length(); 096 for (Map.Entry<Object, Object> entry : shell.getSettings().entrySet()) { 097 String key = entry.getKey().toString(); 098 if (key.startsWith("theme.")) { 099 String t = key.substring(len); 100 Theme.addTheme(Theme.fromString(t, entry.getValue().toString())); 101 } 102 } 103 loadDefaultTheme(shell); 104 } 105 106 public void loadDefaultTheme(Shell shell) { 107 String tname = shell.getSetting("theme", "Default"); 108 Theme theme = Theme.getTheme(tname); 109 if (theme == null) { 110 theme = Theme.getTheme("Default"); 111 } 112 setTheme(theme); 113 } 114 115 public Theme getTheme() { 116 return theme; 117 } 118 119 public void setTheme(Theme theme) { 120 this.theme = theme; 121 setFont(theme.getFont()); 122 setCaretColor(theme.getFgColor()); 123 setBackground(theme.getBgColor()); 124 setForeground(theme.getFgColor()); 125 Shell.get().setSetting("theme", theme.getName()); 126 } 127 128 public ConsoleReader getReader() { 129 return reader; 130 } 131 132 public void setFinder(HistoryFinder finder) { 133 this.finder = finder; 134 } 135 136 public void setMask(Character mask) { 137 if (mask != null) { 138 pwd = new StringBuilder(); 139 } else { 140 pwd = null; 141 } 142 this.mask = mask; 143 } 144 145 public CmdLine getCmdLine() { 146 if (cline == null) { 147 cline = new CmdLine(this); 148 } 149 return cline; 150 } 151 152 public History getHistory() { 153 return reader.getHistory(); 154 } 155 156 public void complete() { 157 try { 158 getCmdLine().sync(); 159 if (!((Boolean) complete.invoke(reader))) { 160 beep(); 161 } 162 } catch (Exception e) { 163 throw new RuntimeException(e); 164 } finally { 165 cline = null; 166 } 167 } 168 169 public void killLine() { 170 getCmdLine().setText(""); 171 } 172 173 public void killLineBefore() { 174 int p = getCmdLine().getLocalCaretPosition(); 175 getCmdLine().setText(getCmdLine().getText().substring(p)); 176 } 177 178 public void killLineAfter() { 179 int p = getCmdLine().getLocalCaretPosition(); 180 getCmdLine().setText(getCmdLine().getText().substring(0, p)); 181 } 182 183 public void execute() { 184 try { 185 String cmd = getCmdLine().getText().trim(); 186 append("\n"); 187 setCaretPosition(getDocument().getLength()); 188 if (pwd != null) { 189 cline = null; 190 in.put(pwd.toString() + "\n"); 191 pwd = null; 192 return; 193 } 194 if (cmd.length() > 0 && reader.getUseHistory()) { 195 reader.getHistory().addToHistory(cmd); 196 reader.getHistory().moveToEnd(); 197 } 198 cline = null; 199 in.put(cmd + "\n"); 200 } catch (Exception e) { 201 throw new RuntimeException(e); 202 } 203 } 204 205 public ConsoleReader getConsoleReader() { 206 return reader; 207 } 208 209 public InputStream in() { 210 return in; 211 } 212 213 public Writer out() { 214 return out; 215 } 216 217 protected void moveHistory(boolean next) { 218 if (next && !reader.getHistory().next()) { 219 beep(); 220 } else if (!next && !reader.getHistory().previous()) { 221 beep(); 222 } 223 224 String text = reader.getHistory().current(); 225 getCmdLine().setText(text); 226 227 } 228 229 @Override 230 protected void processComponentKeyEvent(KeyEvent e) { 231 if (e.isControlDown()) { 232 return; 233 } 234 int id = e.getID(); 235 if (id == KeyEvent.KEY_PRESSED) { 236 int code = e.getKeyCode(); 237 if (handleControlChars(e, code)) { 238 e.consume(); 239 return; 240 } 241 // handle passwords 242 if (mask != null) { 243 char c = e.getKeyChar(); 244 if (c >= 32 && c < 127) { 245 append(mask.toString()); 246 pwd.append(c); 247 } 248 e.consume(); 249 } 250 } else if (mask != null) { 251 e.consume(); // do not show password 252 } 253 } 254 255 public void beep() { 256 if (Boolean.parseBoolean((String) Shell.get().getProperty("shell.visual_bell", "false"))) { 257 visualBell(); 258 } 259 audibleBell(); 260 } 261 262 public void audibleBell() { 263 Toolkit.getDefaultToolkit().beep(); 264 } 265 266 public void visualBell() { 267 setBackground(Color.GREEN); 268 try { 269 Thread.sleep(10); 270 } catch (InterruptedException e) { 271 e.printStackTrace(); 272 } 273 setBackground(Color.BLACK); 274 } 275 276 /** 277 * Return true if should consume the event. 278 * 279 * @param code 280 * @return 281 */ 282 protected boolean handleControlChars(KeyEvent event, int code) { 283 switch (code) { 284 case KeyEvent.VK_LEFT: 285 if (event.isMetaDown()) { 286 setCaretPosition(getCmdLine().getCmdStart()); 287 return true; 288 } 289 if (!getCmdLine().canMoveCaret(-1)) { 290 beep(); 291 return true; 292 } 293 return false; 294 case KeyEvent.VK_RIGHT: 295 if (event.isMetaDown()) { 296 setCaretPosition(getCmdLine().getEnd()); 297 return true; 298 } 299 if (!getCmdLine().canMoveCaret(1)) { 300 beep(); 301 return true; 302 } 303 return false; 304 case KeyEvent.VK_UP: 305 if (event.isMetaDown()) { 306 reader.getHistory().moveToFirstEntry(); 307 getCmdLine().setText(reader.getHistory().current()); 308 return true; 309 } 310 moveHistory(false); 311 return true; 312 case KeyEvent.VK_DOWN: 313 if (event.isMetaDown()) { 314 reader.getHistory().moveToLastEntry(); 315 getCmdLine().setText(reader.getHistory().current()); 316 return true; 317 } 318 moveHistory(true); 319 return true; 320 case KeyEvent.VK_ENTER: 321 execute(); 322 return true; 323 case KeyEvent.VK_BACK_SPACE: 324 if (!getCmdLine().canMoveCaret(-1)) { 325 beep(); 326 return true; 327 } 328 return false; 329 case KeyEvent.VK_TAB: 330 complete(); 331 return true; 332 case KeyEvent.VK_K: 333 if (event.isMetaDown()) { 334 killLineAfter(); 335 return true; 336 } 337 case KeyEvent.VK_U: 338 if (event.isMetaDown()) { 339 killLineBefore(); 340 return true; 341 } 342 case KeyEvent.VK_L: 343 if (event.isMetaDown()) { 344 killLine(); 345 return true; 346 } 347 case KeyEvent.VK_X: 348 if (event.isMetaDown()) { 349 reset(); 350 in.put("\n"); 351 return true; 352 } 353 case KeyEvent.VK_I: 354 if (event.isMetaDown()) { 355 Font font = new Font(Font.MONOSPACED, Font.PLAIN, getFont().getSize() + 1); 356 setFont(font); 357 return true; 358 } 359 case KeyEvent.VK_O: 360 if (event.isMetaDown()) { 361 Font font = new Font(Font.MONOSPACED, Font.PLAIN, getFont().getSize() - 1); 362 setFont(font); 363 return true; 364 } 365 case KeyEvent.VK_EQUALS: 366 if (event.isMetaDown()) { 367 Font font = new Font(Font.MONOSPACED, Font.PLAIN, 14); 368 setFont(font); 369 return true; 370 } 371 case KeyEvent.VK_S: 372 if (event.isMetaDown()) { 373 if (finder != null) { 374 finder.setVisible(true); 375 finder.getParent().validate(); 376 finder.requestFocus(); 377 return true; 378 } 379 } 380 } 381 return false; 382 } 383 384 class In extends InputStream { 385 protected StringBuilder buf = new StringBuilder(); 386 387 public synchronized void put(int key) { 388 buf.append((char) key); 389 notify(); 390 } 391 392 public synchronized void put(String text) { 393 buf.append(text); 394 notify(); 395 } 396 397 @Override 398 public synchronized int read() throws IOException { 399 if (buf.length() > 0) { 400 char c = buf.charAt(0); 401 buf.deleteCharAt(0); 402 return c; 403 } 404 try { 405 wait(); 406 } catch (InterruptedException e) { 407 e.printStackTrace(); 408 } 409 if (buf.length() == 0) { 410 throw new IllegalStateException("invalid state for console input stream"); 411 } 412 char c = buf.charAt(0); 413 buf.deleteCharAt(0); 414 return c; 415 } 416 } 417 418 class Out extends Writer { 419 420 protected void _write(char[] cbuf, int off, int len) throws IOException { 421 _write(new String(cbuf, off, len)); 422 } 423 424 protected void _write(String str) throws IOException { 425 Console.this.append(str); 426 setCaretPosition(getDocument().getLength()); 427 } 428 429 protected boolean handleOutputChar(char c) { 430 try { 431 if (c == 7) { // beep 432 beep(); 433 } else if (c < 32 && c != '\n' && c != '\t') { 434 return true; 435 } else { 436 return false; 437 } 438 } catch (Exception e) { 439 e.printStackTrace(); 440 } 441 return true; 442 } 443 444 @Override 445 public void write(char[] cbuf, int off, int len) throws IOException { 446 if (len == 1) { 447 char c = cbuf[off]; 448 if (!handleOutputChar(c)) { 449 _write(cbuf, off, len); 450 } 451 } else { 452 StringBuilder buf = new StringBuilder(); 453 for (int i = off, end = off + len; i < end; i++) { 454 char c = cbuf[i]; 455 if (!handleOutputChar(c)) { 456 buf.append(c); 457 } 458 } 459 if (buf.length() > 0) { 460 _write(buf.toString()); 461 } 462 } 463 } 464 465 @Override 466 public void flush() throws IOException { 467 } 468 469 @Override 470 public void close() throws IOException { 471 flush(); 472 } 473 } 474 475 public void printColumns(List<String> items) { 476 int maxlen = 0; 477 for (String item : items) { 478 int len = item.length(); 479 if (maxlen < len) { 480 maxlen = len; 481 } 482 } 483 int w = reader.getTermwidth(); 484 int tab = 4; 485 int cols = (w + tab) / (maxlen + tab); 486 Iterator<String> it = items.iterator(); 487 while (it.hasNext()) { 488 for (int i = 0; i < cols; i++) { 489 if (!it.hasNext()) { 490 break; 491 } 492 append(makeColumn(it.next(), maxlen)); 493 if (i < cols - 1) { 494 append(" "); 495 } 496 } 497 if (it.hasNext()) { 498 append("\n"); 499 } 500 } 501 } 502 503 private String makeColumn(String text, int maxlen) { 504 int pad = maxlen - text.length(); 505 if (pad <= 0) { 506 return text; 507 } 508 StringBuilder buf = new StringBuilder(text); 509 for (int i = 0; i < pad; i++) { 510 buf.append(' '); 511 } 512 return buf.toString(); 513 } 514 515 public void reset() { 516 try { 517 setText(""); 518 Shell.get().hello(); 519 } catch (Exception e) { 520 e.printStackTrace(); 521 } 522 } 523 524 public void exit(int code) { 525 in.put("exit " + code); 526 } 527 528}