001 /*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software 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 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019
020 package org.crsh.console.jline;
021
022 import jline.Terminal;
023 import jline.console.ConsoleReader;
024 import jline.console.KeyMap;
025 import jline.console.Operation;
026 import jline.internal.NonBlockingInputStream;
027 import org.crsh.console.Console;
028 import org.crsh.console.ConsoleDriver;
029 import org.crsh.shell.Shell;
030 import org.crsh.text.Style;
031
032 import java.io.IOException;
033 import java.io.PrintStream;
034 import java.util.Stack;
035 import java.util.concurrent.CountDownLatch;
036
037 public class JLineProcessor implements Runnable, ConsoleDriver {
038
039 /** . */
040 private final Console console;
041
042 /** Whether or not we switched on the alternate screen. */
043 boolean useAlternate;
044
045 // *********
046
047 final CountDownLatch done;
048 final Terminal terminal;
049 final PrintStream writer;
050 final ConsoleReader reader;
051 final String lineSeparator;
052
053 public JLineProcessor(
054 Shell shell,
055 ConsoleReader reader,
056 PrintStream out) {
057 this(shell, reader, out, System.getProperty("line.separator"));
058 }
059
060 public JLineProcessor(
061 Shell shell,
062 final ConsoleReader reader,
063 PrintStream out,
064 String lineSeparator) {
065
066 //
067 this.console = new Console(shell, this);
068 this.writer = out;
069 this.useAlternate = false;
070 this.terminal = reader.getTerminal();
071 this.reader = reader;
072 this.lineSeparator = lineSeparator;
073 this.done = new CountDownLatch(1);
074
075 // Update the mode according to the notification
076 console.addModeListener(new Runnable() {
077 @Override
078 public void run() {
079 reader.setKeyMap(console.getMode().getKeyMap());
080 }
081 });
082 }
083
084 public void interrupt() {
085 console.on(Operation.INTERRUPT);
086 }
087
088 // *****
089
090 public void closed() throws InterruptedException {
091 done.await();
092 }
093
094 public void run() {
095
096 //
097 int escapeTimeout = 100;
098
099 //
100 console.init();
101 StringBuilder sb = new StringBuilder();
102 Stack<Character> pushBackChar = new Stack<Character>();
103 while (console.isRunning()) {
104 try {
105
106 //
107 int c = pushBackChar.isEmpty() ? reader.readCharacter() : pushBackChar.pop ();
108 if (c == -1) {
109 break;
110 }
111
112 //
113 sb.appendCodePoint(c);
114
115 //
116 Object o = reader.getKeys().getBound( sb );
117
118 /*
119 * A KeyMap indicates that the key that was struck has a
120 * number of keys that can follow it as indicated in the
121 * map. This is used primarily for Emacs style ESC-META-x
122 * lookups. Since more keys must follow, go back to waiting
123 * for the next key.
124 */
125 if ( o instanceof KeyMap) {
126 /*
127 * The ESC key (#27) is special in that it is ambiguous until
128 * you know what is coming next. The ESC could be a literal
129 * escape, like the user entering vi-move mode, or it could
130 * be part of a terminal control sequence. The following
131 * logic attempts to disambiguate things in the same
132 * fashion as regular vi or readline.
133 *
134 * When ESC is encountered and there is no other pending
135 * character in the pushback queue, then attempt to peek
136 * into the input stream (if the feature is enabled) for
137 * 150ms. If nothing else is coming, then assume it is
138 * not a terminal control sequence, but a raw escape.
139 */
140 if (c == 27
141 && pushBackChar.isEmpty()
142 && ((NonBlockingInputStream)reader.getInput()).isNonBlockingEnabled()
143 && ((NonBlockingInputStream)reader.getInput()).peek(escapeTimeout) == -2) {
144 o = ((KeyMap) o).getAnotherKey();
145 if (o == null || o instanceof KeyMap) {
146 continue;
147 }
148 sb.setLength(0);
149 }
150 else {
151 continue;
152 }
153 }
154
155 /*
156 * If we didn't find a binding for the key and there is
157 * more than one character accumulated then start checking
158 * the largest span of characters from the beginning to
159 * see if there is a binding for them.
160 *
161 * For example if our buffer has ESC,CTRL-M,C the getBound()
162 * called previously indicated that there is no binding for
163 * this sequence, so this then checks ESC,CTRL-M, and failing
164 * that, just ESC. Each keystroke that is pealed off the end
165 * during these tests is stuffed onto the pushback buffer so
166 * they won't be lost.
167 *
168 * If there is no binding found, then we go back to waiting for
169 * input.
170 */
171 while ( o == null && sb.length() > 0 ) {
172 c = sb.charAt( sb.length() - 1 );
173 sb.setLength( sb.length() - 1 );
174 Object o2 = reader.getKeys().getBound( sb );
175 if ( o2 instanceof KeyMap ) {
176 o = ((KeyMap) o2).getAnotherKey();
177 if ( o == null ) {
178 continue;
179 } else {
180 pushBackChar.push( (char) c );
181 }
182 }
183 }
184
185 if ( o == null ) {
186 continue;
187 }
188
189 // It must be that unless it is a macro (...) -> not yet handled
190 if (o instanceof Operation) {
191 Operation operation = (Operation)o;
192
193 int[] buffer = new int[sb.length()];
194 for (int i = 0;i < buffer.length;i++) {
195 buffer[i] = sb.codePointAt(i);
196 }
197 sb.setLength(0);
198
199 //
200 console.on(operation, buffer);
201 } else {
202 System.out.println("No operation: " + o);
203 }
204 }
205 catch (IOException e) {
206 e.printStackTrace();
207 return;
208 }
209 }
210 }
211
212 @Override
213 public int getWidth() {
214 return terminal.getWidth();
215 }
216
217 @Override
218 public int getHeight() {
219 return terminal.getHeight();
220 }
221
222 @Override
223 public String getProperty(String name) {
224 return null;
225 }
226
227 @Override
228 public boolean takeAlternateBuffer() throws IOException {
229 if (!useAlternate) {
230 useAlternate = true;
231
232 // To get those codes I captured the output of telnet running top
233 // on OSX:
234 // 1/ sudo /usr/libexec/telnetd -debug #run telnet
235 // 2/ telnet localhost >output.txt
236 // 3/ type username + enter
237 // 4/ type password + enter
238 // 5/ type top + enter
239 // 6/ ctrl-c
240 // 7/ type exit + enter
241
242 // Save screen and erase
243 writer.print("\033[?47h"); // Switches to the alternate screen
244 // writer.print("\033[1;43r");
245 // processor.writer.print("\033[m"); // Reset to normal (Sets SGR parameters : 0 m == m)
246 // writer.print("\033[4l");
247 // writer.print("\033[?1h");
248 // writer.print("\033[=");
249 // processor.writer.print("\033[H"); // Move the cursor to home
250 // processor.writer.print("\033[2J"); // Clear screen
251 // processor.writer.flush();
252 }
253 return true;
254 }
255
256 @Override
257 public boolean releaseAlternateBuffer() throws IOException {
258 if (useAlternate) {
259 useAlternate = false;
260 writer.print("\033[?47l"); // Switches back to the normal screen
261 }
262 return true;
263 }
264
265 @Override
266 public void flush() throws IOException {
267 writer.flush();
268 }
269
270 @Override
271 public void write(CharSequence s) throws IOException {
272 int len = s.length();
273 for (int i = 0;i < s.length();i++) {
274 char c = s.charAt(i);
275 write(c);
276 }
277 }
278
279 @Override
280 public void write(int c) throws IOException {
281 if (c == '\r') {
282 // Skip it
283 } else if (c == '\n') {
284 writeCRLF();
285 } else {
286 writer.write(c);
287 }
288 }
289
290 @Override
291 public void write(Style d) throws IOException {
292 d.writeAnsiTo(writer);
293 }
294
295 @Override
296 public void writeDel() throws IOException {
297 writer.append("\b \b");
298 }
299
300 @Override
301 public void writeCRLF() throws IOException {
302 writer.append(lineSeparator);
303 }
304
305 @Override
306 public void cls() throws IOException {
307 writer.print("\033[2J");
308 writer.print("\033[1;1H");
309 }
310
311 @Override
312 public boolean moveRight(char c) throws IOException {
313 writer.append(c);
314 return true;
315 }
316
317 @Override
318 public boolean moveLeft() throws IOException {
319 writer.append("\b");
320 return true;
321 }
322
323 @Override
324 public void close() throws IOException {
325 done.countDown();
326 reader.shutdown();
327 }
328 }