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    }