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