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}