io.termd.core.readline.Readline Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of termd-core Show documentation
Show all versions of termd-core Show documentation
An open source terminal daemon library providing terminal handling in Java,
back ported to Alibaba by core engine team to support running on JDK 6+.
/*
* Copyright 2015 Julien Viet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.termd.core.readline;
import io.termd.core.function.BiConsumer;
import io.termd.core.function.Consumer;
import io.termd.core.tty.TtyConnection;
import io.termd.core.tty.TtyEvent;
import io.termd.core.util.Logging;
import io.termd.core.util.Vector;
import io.termd.core.util.Helper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Make this class thread safe as SSH will access this class with different threds [sic].
*
* @author Julien Viet
*/
public class Readline {
/**
* The max number of history item that will be saved in memory.
*/
private static final int MAX_HISTORY_SIZE = 500;
// private final Device device;
private final Map functions = new HashMap();
private final EventQueue decoder;
private Interaction interaction;
private Vector size;
private volatile List history;
public Readline(Keymap keymap) {
// https://github.com/alibaba/termd/issues/42
// this.device = TermInfo.defaultInfo().getDevice("xterm"); // For now use xterm
this.decoder = new EventQueue(keymap);
this.history = new ArrayList();
addFunction(ACCEPT_LINE);
}
/**
* @return the current history
*/
public List getHistory() {
return history;
}
/**
* Set the history
*
* @param history the history
*/
public void setHistory(List history) {
this.history = history;
}
/**
* @return the last known size
*/
public Vector size() {
return size;
}
public Readline addFunction(Function function) {
functions.put(function.name(), function);
return this;
}
public Readline addFunctions(Iterable functions) {
for (Function function : functions) {
addFunction(function);
}
return this;
}
/**
* Cancel the current readline interaction if there one, the request handler is called with {@code null}.
*/
public boolean cancel() {
Interaction interaction;
synchronized (this) {
interaction = this.interaction;
if (interaction == null) {
return false;
}
}
return interaction.end(null);
}
private void deliver() {
while (true) {
Interaction handler;
KeyEvent event;
synchronized (this) {
if (decoder.hasNext() && interaction != null && !interaction.paused) {
event = decoder.next();
handler = interaction;
} else {
return;
}
}
handler.handle(event);
}
}
/**
* Read a line until a request can be processed.
*
* @param requestHandler the requestHandler
*/
public void readline(TtyConnection conn, String prompt, Consumer requestHandler) {
readline(conn, prompt, requestHandler, null);
}
/**
* Read a line until a request can be processed.
*
* @param requestHandler the requestHandler
*/
public void readline(TtyConnection conn, String prompt, Consumer requestHandler, Consumer completionHandler) {
synchronized (this) {
if (interaction != null) {
throw new IllegalStateException("Already reading a line");
}
interaction = new Interaction(conn, prompt, requestHandler, completionHandler);
}
interaction.install();
conn.write(prompt);
schedulePendingEvent();
}
/**
* Schedule delivery of pending events in the event queue.
*/
public void schedulePendingEvent() {
TtyConnection conn;
synchronized (this) {
if (interaction == null) {
throw new IllegalStateException("No interaction!");
}
if (decoder.hasNext()) {
conn = interaction.conn;
} else {
return;
}
}
conn.execute(new Runnable() {
@Override
public void run() {
deliver();
}
});
}
public synchronized Readline queueEvent(int[] codePoints) {
decoder.append(codePoints);
return this;
}
public synchronized boolean hasEvent() {
return decoder.hasNext();
}
public synchronized KeyEvent nextEvent() {
return decoder.next();
}
public class Interaction {
final TtyConnection conn;
private Consumer prevReadHandler;
private Consumer prevSizeHandler;
private BiConsumer prevEventHandler;
private final String prompt;
private final Consumer requestHandler;
private final Consumer completionHandler;
private final Map data;
private final LineBuffer line = new LineBuffer();
private final LineBuffer buffer = new LineBuffer();
private int historyIndex = -1;
private String currentPrompt;
private boolean paused;
private Interaction(
TtyConnection conn,
String prompt,
Consumer requestHandler,
Consumer completionHandler) {
this.conn = conn;
this.prompt = prompt;
this.data = new HashMap();
this.currentPrompt = prompt;
this.requestHandler = requestHandler;
this.completionHandler = completionHandler;
}
/**
* End the current interaction with a callback.
*
* @param s the
*/
private boolean end(String s) {
synchronized (Readline.this) {
if (interaction == null) {
return false;
}
interaction = null;
conn.setStdinHandler(prevReadHandler);
conn.setSizeHandler(prevSizeHandler);
conn.setEventHandler(prevEventHandler);
}
requestHandler.accept(s);
return true;
}
private void handle(KeyEvent event) {
// Very specific behavior that cannot be encapsulated in a function flow
if (event.length() == 1) {
if (event.getCodePointAt(0) == 4 && buffer.getSize() == 0) {
// Specific behavior for Ctrl-D with empty line
end(null);
return;
} else if (event.getCodePointAt(0) == 3) {
// Specific behavior Ctrl-C
line.clear();
buffer.clear();
data.clear();
historyIndex = -1;
currentPrompt = prompt;
conn.stdoutHandler().accept(new int[]{'\n'});
conn.write(interaction.prompt);
return;
}
else if (event.getCodePointAt(0) == 12) {
// Specific behavior Ctrl-L
// \033 is the control character, \033[H means move the cursor to (0,0), \033[2J means clear screen
conn.write("\033[H\033[2J");
this.redraw();
return;
}
}
if (event instanceof FunctionEvent) {
FunctionEvent fname = (FunctionEvent) event;
Function function = functions.get(fname.name());
if (function != null) {
synchronized (this) {
paused = true;
}
function.apply(this);
} else {
Logging.READLINE.warn("Unimplemented function " + fname.name());
}
} else {
LineBuffer buf = buffer.copy();
for (int i = 0;i < event.length();i++) {
int codePoint = event.getCodePointAt(i);
try {
buf.insert(codePoint);
} catch (IllegalArgumentException e) {
conn.stdoutHandler().accept(new int[]{'\007'});
}
}
refresh(buf);
}
}
void resize(int oldWith, int newWidth) {
// Erase screen
LineBuffer abc = new LineBuffer(buffer.getCapacity());
abc.insert(currentPrompt);
abc.insert(buffer.toArray());
abc.setCursor(currentPrompt.length() + buffer.getCursor());
// Recompute new cursor
Vector pos = abc.getCursorPosition(newWidth);
int curWidth = pos.x();
int curHeight = pos.y();
// Recompute new end
Vector end = abc.getPosition(abc.getSize(), oldWith);
int endHeight = end.y() + end.x() / newWidth;
// Position at the bottom / right
Consumer out = conn.stdoutHandler();
out.accept(new int[]{'\r'});
while (curHeight != endHeight) {
if (curHeight > endHeight) {
out.accept(new int[]{'\033','[','1','A'});
curHeight--;
} else {
out.accept(new int[]{'\n'});
curHeight++;
}
}
// Now erase and redraw
while (curHeight > 0) {
out.accept(new int[]{'\033','[','1','K'});
out.accept(new int[]{'\033','[','1','A'});
curHeight--;
}
out.accept(new int[]{'\033','[','1','K'});
// Now redraw
out.accept(Helper.toCodePoints(currentPrompt));
refresh(new LineBuffer(), newWidth);
}
public Consumer completionHandler() {
return completionHandler;
}
public Map data() {
return data;
}
public List history() {
return history;
}
public int getHistoryIndex() {
return historyIndex;
}
public void setHistoryIndex(int historyIndex) {
this.historyIndex = historyIndex;
}
public LineBuffer line() {
return line;
}
public LineBuffer buffer() {
return buffer;
}
public String currentPrompt() {
return currentPrompt;
}
public Vector size() {
return size;
}
/**
* Redraw the current line.
*/
public void redraw() {
LineBuffer toto = new LineBuffer(buffer.getCapacity());
toto.insert(Helper.toCodePoints(currentPrompt));
toto.insert(buffer.toArray());
toto.setCursor(currentPrompt.length() + buffer.getCursor());
LineBuffer abc = new LineBuffer(toto.getCapacity());
abc.update(toto, conn.stdoutHandler(), size.x());
}
/**
* Refresh the current buffer with the argument buffer.
*
* @param buffer the new buffer
*/
public Interaction refresh(LineBuffer buffer) {
refresh(buffer, size.x());
return this;
}
private void refresh(LineBuffer update, int width) {
LineBuffer copy3 = new LineBuffer(update.getCapacity());
final List codePoints = new LinkedList();
copy3.insert(Helper.toCodePoints(currentPrompt));
copy3.insert(buffer().toArray());
copy3.setCursor(currentPrompt.length() + buffer().getCursor());
LineBuffer copy2 = new LineBuffer(copy3.getCapacity());
copy2.insert(Helper.toCodePoints(currentPrompt));
copy2.insert(update.toArray());
copy2.setCursor(currentPrompt.length() + update.getCursor());
copy3.update(copy2, new Consumer() {
@Override
public void accept(int[] data) {
for (int cp : data) {
codePoints.add(cp);
}
}
}, width);
conn.stdoutHandler().accept(Helper.convert(codePoints));
buffer.clear();
buffer.insert(update.toArray());
buffer.setCursor(update.getCursor());
}
public void resume() {
synchronized (Readline.this) {
if (!paused) {
throw new IllegalStateException();
}
paused = false;
}
schedulePendingEvent();
}
private void install() {
prevReadHandler = conn.getStdinHandler();
prevSizeHandler = conn.getSizeHandler();
prevEventHandler = conn.getEventHandler();
conn.setStdinHandler(new Consumer() {
@Override
public void accept(int[] data) {
synchronized (Readline.this) {
decoder.append(data);
}
deliver();
}
});
size = conn.size();
conn.setSizeHandler(new Consumer() {
@Override
public void accept(Vector dim) {
if (size != null) {
// Not supported for now
// interaction.resize(size.width(), dim.width());
}
size = dim;
}
});
conn.setEventHandler(null);
}
}
// Need to access internal state
private final Function ACCEPT_LINE = new Function() {
@Override
public String name() {
return "accept-line";
}
@Override
public void apply(Interaction interaction) {
interaction.line.insert(interaction.buffer.toArray());
LineStatus pb = new LineStatus();
for (int i = 0;i < interaction.line.getSize();i++) {
pb.accept(interaction.line.getAt(i));
}
interaction.buffer.clear();
if (pb.isEscaping()) {
interaction.line.delete(-1); // Remove \
interaction.currentPrompt = "> ";
interaction.conn.write("\n> ");
interaction.resume();
} else {
if (pb.isQuoted()) {
interaction.line.insert('\n');
interaction.conn.write("\n> ");
interaction.currentPrompt = "> ";
interaction.resume();
} else {
String raw = interaction.line.toString();
if (interaction.line.getSize() > 0) {
addToHistory(interaction.line.toArray());
}
interaction.line.clear();
interaction.conn.write("\n");
interaction.end(raw);
}
}
}
private void addToHistory(int[] command) {
// copy and save. https://github.com/alibaba/termd/issues/44
synchronized (Readline.class) {
List tmp = new ArrayList(history.size());
// add to first
tmp.add(command);
for (int[] c : history) {
tmp.add(c);
if (tmp.size() >= MAX_HISTORY_SIZE) {
break;
}
}
history = tmp;
}
}
};
}