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}