org.crsh.console.jline.JLineProcessor Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.crsh.console.jline;
import jline.Terminal;
import jline.console.ConsoleReader;
import jline.console.KeyMap;
import jline.console.Operation;
import jline.internal.NonBlockingInputStream;
import org.crsh.console.Console;
import org.crsh.console.ConsoleDriver;
import org.crsh.shell.Shell;
import org.crsh.text.Style;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Stack;
import java.util.concurrent.CountDownLatch;
public class JLineProcessor implements Runnable, ConsoleDriver {
/** . */
private final Console console;
/** Whether or not we switched on the alternate screen. */
boolean useAlternate;
// *********
final CountDownLatch done;
final Terminal terminal;
final PrintStream writer;
final ConsoleReader reader;
final String lineSeparator;
final boolean ansi;
public JLineProcessor(
boolean ansi,
Shell shell,
ConsoleReader reader,
PrintStream out) {
this(ansi, shell, reader, out, System.getProperty("line.separator"));
}
public JLineProcessor(
boolean ansi,
Shell shell,
final ConsoleReader reader,
PrintStream out,
String lineSeparator) {
//
this.console = new Console(shell, this);
this.writer = out;
this.useAlternate = false;
this.terminal = reader.getTerminal();
this.reader = reader;
this.lineSeparator = lineSeparator;
this.done = new CountDownLatch(1);
this.ansi = ansi;
// Update the mode according to the notification
console.addModeListener(new Runnable() {
@Override
public void run() {
reader.setKeyMap(console.getMode().getKeyMap());
}
});
}
public void interrupt() {
console.on(Operation.INTERRUPT);
}
// *****
public void closed() throws InterruptedException {
done.await();
}
public void run() {
//
int escapeTimeout = 100;
//
console.init();
StringBuilder sb = new StringBuilder();
Stack pushBackChar = new Stack();
while (console.isRunning()) {
try {
//
int c = pushBackChar.isEmpty() ? reader.readCharacter() : pushBackChar.pop ();
if (c == -1) {
break;
}
//
sb.appendCodePoint(c);
//
Object o = reader.getKeys().getBound( sb );
/*
* A KeyMap indicates that the key that was struck has a
* number of keys that can follow it as indicated in the
* map. This is used primarily for Emacs style ESC-META-x
* lookups. Since more keys must follow, go back to waiting
* for the next key.
*/
if ( o instanceof KeyMap) {
/*
* The ESC key (#27) is special in that it is ambiguous until
* you know what is coming next. The ESC could be a literal
* escape, like the user entering vi-move mode, or it could
* be part of a terminal control sequence. The following
* logic attempts to disambiguate things in the same
* fashion as regular vi or readline.
*
* When ESC is encountered and there is no other pending
* character in the pushback queue, then attempt to peek
* into the input stream (if the feature is enabled) for
* 150ms. If nothing else is coming, then assume it is
* not a terminal control sequence, but a raw escape.
*/
if (c == 27
&& pushBackChar.isEmpty()
&& ((NonBlockingInputStream)reader.getInput()).isNonBlockingEnabled()
&& ((NonBlockingInputStream)reader.getInput()).peek(escapeTimeout) == -2) {
o = ((KeyMap) o).getAnotherKey();
if (o == null || o instanceof KeyMap) {
continue;
}
sb.setLength(0);
}
else {
continue;
}
}
/*
* If we didn't find a binding for the key and there is
* more than one character accumulated then start checking
* the largest span of characters from the beginning to
* see if there is a binding for them.
*
* For example if our buffer has ESC,CTRL-M,C the getBound()
* called previously indicated that there is no binding for
* this sequence, so this then checks ESC,CTRL-M, and failing
* that, just ESC. Each keystroke that is pealed off the end
* during these tests is stuffed onto the pushback buffer so
* they won't be lost.
*
* If there is no binding found, then we go back to waiting for
* input.
*/
while ( o == null && sb.length() > 0 ) {
c = sb.charAt( sb.length() - 1 );
sb.setLength( sb.length() - 1 );
Object o2 = reader.getKeys().getBound( sb );
if ( o2 instanceof KeyMap ) {
o = ((KeyMap) o2).getAnotherKey();
if ( o == null ) {
continue;
} else {
pushBackChar.push( (char) c );
}
}
}
if ( o == null ) {
continue;
}
// It must be that unless it is a macro (...) -> not yet handled
if (o instanceof Operation) {
Operation operation = (Operation)o;
int[] buffer = new int[sb.length()];
for (int i = 0;i < buffer.length;i++) {
buffer[i] = sb.codePointAt(i);
}
sb.setLength(0);
//
console.on(operation, buffer);
} else {
System.out.println("No operation: " + o);
}
}
catch (IOException e) {
e.printStackTrace();
return;
}
}
}
@Override
public int getWidth() {
return terminal.getWidth();
}
@Override
public int getHeight() {
return terminal.getHeight();
}
@Override
public String getProperty(String name) {
return null;
}
@Override
public boolean takeAlternateBuffer() throws IOException {
if (ansi) {
if (!useAlternate) {
useAlternate = true;
// To get those codes I captured the output of telnet running top
// on OSX:
// 1/ sudo /usr/libexec/telnetd -debug #run telnet
// 2/ telnet localhost >output.txt
// 3/ type username + enter
// 4/ type password + enter
// 5/ type top + enter
// 6/ ctrl-c
// 7/ type exit + enter
// Save screen and erase
writer.print("\033[?47h"); // Switches to the alternate screen
// writer.print("\033[1;43r");
// processor.writer.print("\033[m"); // Reset to normal (Sets SGR parameters : 0 m == m)
// writer.print("\033[4l");
// writer.print("\033[?1h");
// writer.print("\033[=");
// processor.writer.print("\033[H"); // Move the cursor to home
// processor.writer.print("\033[2J"); // Clear screen
// processor.writer.flush();
}
return true;
} else {
return false;
}
}
@Override
public boolean releaseAlternateBuffer() throws IOException {
if (ansi && useAlternate) {
useAlternate = false;
writer.print("\033[?47l"); // Switches back to the normal screen
}
return true;
}
@Override
public void flush() throws IOException {
writer.flush();
}
@Override
public void write(CharSequence s) throws IOException {
write(s, 0, s.length());
}
@Override
public void write(CharSequence s, int start, int end) throws IOException {
while (start < end) {
char c = s.charAt(start++);
write(c);
}
}
@Override
public void write(char c) throws IOException {
if (c == '\r') {
// Skip it
} else if (c == '\n') {
writeCRLF();
} else {
writer.print(c);
}
}
@Override
public void write(Style d) throws IOException {
if (ansi) {
d.writeAnsiTo(writer);
}
}
@Override
public void writeDel() throws IOException {
writer.append("\b \b");
}
@Override
public void writeCRLF() throws IOException {
writer.append(lineSeparator);
}
@Override
public void cls() throws IOException {
if (ansi) {
writer.print("\033[2J");
writer.print("\033[1;1H");
}
}
@Override
public boolean moveRight(char c) throws IOException {
writer.append(c);
return true;
}
@Override
public boolean moveLeft() throws IOException {
writer.append("\b");
return true;
}
@Override
public void close() throws IOException {
done.countDown();
reader.shutdown();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy