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            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}