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:[email protected]">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 while (buf.length() == 0) { 408 try { 409 wait(); 410 } catch (InterruptedException e) { 411 Thread.currentThread().interrupt(); 412 throw new RuntimeException(e); 413 } 414 } 415 char c = buf.charAt(0); 416 buf.deleteCharAt(0); 417 return c; 418 } 419 } 420 421 class Out extends Writer { 422 423 protected void _write(char[] cbuf, int off, int len) throws IOException { 424 _write(new String(cbuf, off, len)); 425 } 426 427 protected void _write(String str) throws IOException { 428 Console.this.append(str); 429 setCaretPosition(getDocument().getLength()); 430 } 431 432 protected boolean handleOutputChar(char c) { 433 try { 434 if (c == 7) { // beep 435 beep(); 436 } else if (c < 32 && c != '\n' && c != '\t') { 437 return true; 438 } else { 439 return false; 440 } 441 } catch (Exception e) { 442 e.printStackTrace(); 443 } 444 return true; 445 } 446 447 @Override 448 public void write(char[] cbuf, int off, int len) throws IOException { 449 if (len == 1) { 450 char c = cbuf[off]; 451 if (!handleOutputChar(c)) { 452 _write(cbuf, off, len); 453 } 454 } else { 455 StringBuilder buf = new StringBuilder(); 456 for (int i = off, end = off + len; i < end; i++) { 457 char c = cbuf[i]; 458 if (!handleOutputChar(c)) { 459 buf.append(c); 460 } 461 } 462 if (buf.length() > 0) { 463 _write(buf.toString()); 464 } 465 } 466 } 467 468 @Override 469 public void flush() throws IOException { 470 } 471 472 @Override 473 public void close() throws IOException { 474 flush(); 475 } 476 } 477 478 public void printColumns(List<String> items) { 479 int maxlen = 0; 480 for (String item : items) { 481 int len = item.length(); 482 if (maxlen < len) { 483 maxlen = len; 484 } 485 } 486 int w = reader.getTermwidth(); 487 int tab = 4; 488 int cols = (w + tab) / (maxlen + tab); 489 Iterator<String> it = items.iterator(); 490 while (it.hasNext()) { 491 for (int i = 0; i < cols; i++) { 492 if (!it.hasNext()) { 493 break; 494 } 495 append(makeColumn(it.next(), maxlen)); 496 if (i < cols - 1) { 497 append(" "); 498 } 499 } 500 if (it.hasNext()) { 501 append("\n"); 502 } 503 } 504 } 505 506 private String makeColumn(String text, int maxlen) { 507 int pad = maxlen - text.length(); 508 if (pad <= 0) { 509 return text; 510 } 511 StringBuilder buf = new StringBuilder(text); 512 for (int i = 0; i < pad; i++) { 513 buf.append(' '); 514 } 515 return buf.toString(); 516 } 517 518 public void reset() { 519 try { 520 setText(""); 521 Shell.get().hello(); 522 } catch (Exception e) { 523 e.printStackTrace(); 524 } 525 } 526 527 public void exit(int code) { 528 in.put("exit " + code); 529 } 530 531}