1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.tools.jjs;
27
28import java.awt.event.ActionListener;
29import java.io.File;
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.PrintStream;
33import java.io.Writer;
34import java.nio.file.Files;
35import java.util.function.Function;
36import java.util.stream.Collectors;
37import jdk.internal.jline.NoInterruptUnixTerminal;
38import jdk.internal.jline.Terminal;
39import jdk.internal.jline.TerminalFactory;
40import jdk.internal.jline.TerminalFactory.Flavor;
41import jdk.internal.jline.WindowsTerminal;
42import jdk.internal.jline.console.ConsoleReader;
43import jdk.internal.jline.console.KeyMap;
44import jdk.internal.jline.extra.EditingHistory;
45import jdk.internal.misc.Signal;
46import jdk.internal.misc.Signal.Handler;
47
48class Console implements AutoCloseable {
49    private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB
50    private final ConsoleReader in;
51    private final File historyFile;
52
53    Console(final InputStream cmdin, final PrintStream cmdout, final File historyFile,
54            final NashornCompleter completer, final Function<String, String> docHelper) throws IOException {
55        this.historyFile = historyFile;
56
57        TerminalFactory.registerFlavor(Flavor.WINDOWS, isCygwin()? JJSUnixTerminal::new : JJSWindowsTerminal::new);
58        TerminalFactory.registerFlavor(Flavor.UNIX, JJSUnixTerminal::new);
59        in = new ConsoleReader(cmdin, cmdout);
60        in.setExpandEvents(false);
61        in.setHandleUserInterrupt(true);
62        in.setBellEnabled(true);
63        in.setCopyPasteDetection(true);
64        final Iterable<String> existingHistory = historyFile.exists() ? Files.readAllLines(historyFile.toPath()) : null;
65        in.setHistory(new EditingHistory(in, existingHistory) {
66            @Override protected boolean isComplete(CharSequence input) {
67                return completer.isComplete(input.toString());
68            }
69        });
70        in.addCompleter(completer);
71        Runtime.getRuntime().addShutdownHook(new Thread((Runnable)this::saveHistory));
72        bind(DOCUMENTATION_SHORTCUT, (ActionListener)evt -> showDocumentation(docHelper));
73        try {
74            Signal.handle(new Signal("CONT"), new Handler() {
75                @Override public void handle(Signal sig) {
76                    try {
77                        in.getTerminal().reset();
78                        in.redrawLine();
79                        in.flush();
80                    } catch (Exception ex) {
81                        ex.printStackTrace();
82                    }
83                }
84            });
85        } catch (IllegalArgumentException ignored) {
86            //the CONT signal does not exist on this platform
87        }
88    }
89
90    String readLine(final String prompt) throws IOException {
91        return in.readLine(prompt);
92    }
93
94    @Override
95    public void close() {
96        saveHistory();
97    }
98
99    private void saveHistory() {
100        try (Writer out = Files.newBufferedWriter(historyFile.toPath())) {
101            String lineSeparator = System.getProperty("line.separator");
102
103            out.write(getHistory().save()
104                                  .stream()
105                                  .collect(Collectors.joining(lineSeparator)));
106        } catch (final IOException exp) {}
107    }
108
109    EditingHistory getHistory() {
110        return (EditingHistory) in.getHistory();
111    }
112
113    boolean terminalEditorRunning() {
114        Terminal terminal = in.getTerminal();
115        if (terminal instanceof JJSUnixTerminal) {
116            return ((JJSUnixTerminal) terminal).isRaw();
117        }
118        return false;
119    }
120
121    void suspend() {
122        try {
123            in.getTerminal().restore();
124        } catch (Exception ex) {
125            throw new IllegalStateException(ex);
126        }
127    }
128
129    void resume() {
130        try {
131            in.getTerminal().init();
132        } catch (Exception ex) {
133            throw new IllegalStateException(ex);
134        }
135    }
136
137    static final class JJSUnixTerminal extends NoInterruptUnixTerminal {
138        JJSUnixTerminal() throws Exception {
139        }
140
141        boolean isRaw() {
142            try {
143                return getSettings().get("-a").contains("-icanon");
144            } catch (IOException | InterruptedException ex) {
145                return false;
146            }
147        }
148
149        @Override
150        public void disableInterruptCharacter() {
151        }
152
153        @Override
154        public void enableInterruptCharacter() {
155        }
156    }
157
158    static final class JJSWindowsTerminal extends WindowsTerminal {
159        public JJSWindowsTerminal() throws Exception {
160        }
161
162        @Override
163        public void init() throws Exception {
164            super.init();
165            setAnsiSupported(false);
166        }
167    }
168
169    private static boolean isCygwin() {
170        return System.getenv("SHELL") != null;
171    }
172
173    private void bind(String shortcut, Object action) {
174        KeyMap km = in.getKeys();
175        for (int i = 0; i < shortcut.length(); i++) {
176            final Object value = km.getBound(Character.toString(shortcut.charAt(i)));
177            if (value instanceof KeyMap) {
178                km = (KeyMap) value;
179            } else {
180                km.bind(shortcut.substring(i), action);
181            }
182        }
183    }
184
185    private void showDocumentation(final Function<String, String> docHelper) {
186        final String buffer = in.getCursorBuffer().buffer.toString();
187        final int cursor = in.getCursorBuffer().cursor;
188        final String doc = docHelper.apply(buffer.substring(0, cursor));
189        try {
190            if (doc != null) {
191                in.println();
192                in.println(doc);
193                in.redrawLine();
194                in.flush();
195            } else {
196                in.beep();
197            }
198        } catch (IOException ex) {
199            throw new IllegalStateException(ex);
200        }
201    }
202}
203