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}