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 (Exception 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 try { 187 String cmd = getCmdLine().getText().trim(); 188 append("\n"); 189 setCaretPosition(getDocument().getLength()); 190 if (pwd != null) { 191 cline = null; 192 in.put(pwd.toString() + "\n"); 193 pwd = null; 194 return; 195 } 196 if (cmd.length() > 0 && reader.getUseHistory()) { 197 reader.getHistory().addToHistory(cmd); 198 reader.getHistory().moveToEnd(); 199 } 200 cline = null; 201 in.put(cmd + "\n"); 202 } catch (Exception e) { 203 throw new RuntimeException(e); 204 } 205 } 206 207 public ConsoleReader getConsoleReader() { 208 return reader; 209 } 210 211 public InputStream in() { 212 return in; 213 } 214 215 public Writer out() { 216 return out; 217 } 218 219 protected void moveHistory(boolean next) { 220 if (next && !reader.getHistory().next()) { 221 beep(); 222 } else if (!next && !reader.getHistory().previous()) { 223 beep(); 224 } 225 226 String text = reader.getHistory().current(); 227 getCmdLine().setText(text); 228 229 } 230 231 @Override 232 protected void processComponentKeyEvent(KeyEvent e) { 233 if (e.isControlDown()) { 234 return; 235 } 236 int id = e.getID(); 237 if (id == KeyEvent.KEY_PRESSED) { 238 int code = e.getKeyCode(); 239 if (handleControlChars(e, code)) { 240 e.consume(); 241 return; 242 } 243 // handle passwords 244 if (mask != null) { 245 char c = e.getKeyChar(); 246 if (c >= 32 && c < 127) { 247 append(mask.toString()); 248 pwd.append(c); 249 } 250 e.consume(); 251 } 252 } else if (mask != null) { 253 e.consume(); // do not show password 254 } 255 } 256 257 public void beep() { 258 if (Boolean.parseBoolean((String) Shell.get().getProperty("shell.visual_bell", "false"))) { 259 visualBell(); 260 } 261 audibleBell(); 262 } 263 264 public void audibleBell() { 265 Toolkit.getDefaultToolkit().beep(); 266 } 267 268 public void visualBell() { 269 setBackground(Color.GREEN); 270 try { 271 Thread.sleep(10); 272 } catch (InterruptedException e) { 273 e.printStackTrace(); 274 } 275 setBackground(Color.BLACK); 276 } 277 278 /** 279 * Return true if should consume the event. 280 * 281 * @param code 282 * @return 283 */ 284 protected boolean handleControlChars(KeyEvent event, int code) { 285 switch (code) { 286 case KeyEvent.VK_LEFT: 287 if (event.isMetaDown()) { 288 setCaretPosition(getCmdLine().getCmdStart()); 289 return true; 290 } 291 if (!getCmdLine().canMoveCaret(-1)) { 292 beep(); 293 return true; 294 } 295 return false; 296 case KeyEvent.VK_RIGHT: 297 if (event.isMetaDown()) { 298 setCaretPosition(getCmdLine().getEnd()); 299 return true; 300 } 301 if (!getCmdLine().canMoveCaret(1)) { 302 beep(); 303 return true; 304 } 305 return false; 306 case KeyEvent.VK_UP: 307 if (event.isMetaDown()) { 308 reader.getHistory().moveToFirstEntry(); 309 getCmdLine().setText(reader.getHistory().current()); 310 return true; 311 } 312 moveHistory(false); 313 return true; 314 case KeyEvent.VK_DOWN: 315 if (event.isMetaDown()) { 316 reader.getHistory().moveToLastEntry(); 317 getCmdLine().setText(reader.getHistory().current()); 318 return true; 319 } 320 moveHistory(true); 321 return true; 322 case KeyEvent.VK_ENTER: 323 execute(); 324 return true; 325 case KeyEvent.VK_BACK_SPACE: 326 if (!getCmdLine().canMoveCaret(-1)) { 327 beep(); 328 return true; 329 } 330 return false; 331 case KeyEvent.VK_TAB: 332 complete(); 333 return true; 334 case KeyEvent.VK_K: 335 if (event.isMetaDown()) { 336 killLineAfter(); 337 return true; 338 } 339 case KeyEvent.VK_U: 340 if (event.isMetaDown()) { 341 killLineBefore(); 342 return true; 343 } 344 case KeyEvent.VK_L: 345 if (event.isMetaDown()) { 346 killLine(); 347 return true; 348 } 349 case KeyEvent.VK_X: 350 if (event.isMetaDown()) { 351 reset(); 352 in.put("\n"); 353 return true; 354 } 355 case KeyEvent.VK_I: 356 if (event.isMetaDown()) { 357 Font font = new Font(Font.MONOSPACED, Font.PLAIN, getFont().getSize() + 1); 358 setFont(font); 359 return true; 360 } 361 case KeyEvent.VK_O: 362 if (event.isMetaDown()) { 363 Font font = new Font(Font.MONOSPACED, Font.PLAIN, getFont().getSize() - 1); 364 setFont(font); 365 return true; 366 } 367 case KeyEvent.VK_EQUALS: 368 if (event.isMetaDown()) { 369 Font font = new Font(Font.MONOSPACED, Font.PLAIN, 14); 370 setFont(font); 371 return true; 372 } 373 case KeyEvent.VK_S: 374 if (event.isMetaDown()) { 375 if (finder != null) { 376 finder.setVisible(true); 377 finder.getParent().validate(); 378 finder.requestFocus(); 379 return true; 380 } 381 } 382 } 383 return false; 384 } 385 386 class In extends InputStream { 387 protected StringBuilder buf = new StringBuilder(); 388 389 public synchronized void put(int key) { 390 buf.append((char) key); 391 notify(); 392 } 393 394 public synchronized void put(String text) { 395 buf.append(text); 396 notify(); 397 } 398 399 @Override 400 public synchronized int read() throws IOException { 401 if (buf.length() > 0) { 402 char c = buf.charAt(0); 403 buf.deleteCharAt(0); 404 return c; 405 } 406 try { 407 wait(); 408 } catch (InterruptedException e) { 409 e.printStackTrace(); 410 } 411 if (buf.length() == 0) { 412 throw new IllegalStateException("invalid state for console input stream"); 413 } 414 char c = buf.charAt(0); 415 buf.deleteCharAt(0); 416 return c; 417 } 418 } 419 420 class Out extends Writer { 421 422 protected void _write(char[] cbuf, int off, int len) throws IOException { 423 _write(new String(cbuf, off, len)); 424 } 425 426 protected void _write(String str) throws IOException { 427 Console.this.append(str); 428 setCaretPosition(getDocument().getLength()); 429 } 430 431 protected boolean handleOutputChar(char c) { 432 try { 433 if (c == 7) { // beep 434 beep(); 435 } else if (c < 32 && c != '\n' && c != '\t') { 436 return true; 437 } else { 438 return false; 439 } 440 } catch (Exception e) { 441 e.printStackTrace(); 442 } 443 return true; 444 } 445 446 @Override 447 public void write(char[] cbuf, int off, int len) throws IOException { 448 if (len == 1) { 449 char c = cbuf[off]; 450 if (!handleOutputChar(c)) { 451 _write(cbuf, off, len); 452 } 453 } else { 454 StringBuilder buf = new StringBuilder(); 455 for (int i = off, end = off + len; i < end; i++) { 456 char c = cbuf[i]; 457 if (!handleOutputChar(c)) { 458 buf.append(c); 459 } 460 } 461 if (buf.length() > 0) { 462 _write(buf.toString()); 463 } 464 } 465 } 466 467 @Override 468 public void flush() throws IOException { 469 } 470 471 @Override 472 public void close() throws IOException { 473 flush(); 474 } 475 } 476 477 public void printColumns(List<String> items) { 478 int maxlen = 0; 479 for (String item : items) { 480 int len = item.length(); 481 if (maxlen < len) { 482 maxlen = len; 483 } 484 } 485 int w = reader.getTermwidth(); 486 int tab = 4; 487 int cols = (w + tab) / (maxlen + tab); 488 Iterator<String> it = items.iterator(); 489 while (it.hasNext()) { 490 for (int i = 0; i < cols; i++) { 491 if (!it.hasNext()) { 492 break; 493 } 494 append(makeColumn(it.next(), maxlen)); 495 if (i < cols - 1) { 496 append(" "); 497 } 498 } 499 if (it.hasNext()) { 500 append("\n"); 501 } 502 } 503 } 504 505 private String makeColumn(String text, int maxlen) { 506 int pad = maxlen - text.length(); 507 if (pad <= 0) { 508 return text; 509 } 510 StringBuilder buf = new StringBuilder(text); 511 for (int i = 0; i < pad; i++) { 512 buf.append(' '); 513 } 514 return buf.toString(); 515 } 516 517 public void reset() { 518 try { 519 setText(""); 520 Shell.get().hello(); 521 } catch (Exception e) { 522 e.printStackTrace(); 523 } 524 } 525 526 public void exit(int code) { 527 in.put("exit " + code); 528 } 529 530}