411 lines
13 KiB
Java
411 lines
13 KiB
Java
package terminal;
|
|
|
|
import static com.googlecode.lanterna.input.KeyType.*;
|
|
import static terminal.ControlSequenceHandler.isEscape;
|
|
import static util.Characters.EOF;
|
|
import static util.Characters.UNICODE_NULL;
|
|
|
|
import java.io.*;
|
|
import java.util.concurrent.*;
|
|
|
|
import com.googlecode.lanterna.*;
|
|
import com.googlecode.lanterna.input.*;
|
|
import com.googlecode.lanterna.terminal.IOSafeTerminal;
|
|
|
|
import stream.*;
|
|
|
|
public class LispTerminal {
|
|
|
|
public static final char END_OF_SEGMENT = UNICODE_NULL;
|
|
|
|
private IOSafeTerminal terminal;
|
|
private SafeOutputStream inputWriter;
|
|
private SafeInputStream outputReader;
|
|
private ControlSequenceHandler controlSequenceHandler;
|
|
private ExecutorService executorService;
|
|
private TerminalSize terminalSize;
|
|
private String inputLine;
|
|
private String outputSegment;
|
|
private boolean isStopped;
|
|
private int originColumn;
|
|
private int originRow;
|
|
|
|
public LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) {
|
|
this.terminal = terminal;
|
|
this.inputWriter = new SafeOutputStream(inputWriter);
|
|
this.outputReader = new SafeInputStream(outputReader);
|
|
this.controlSequenceHandler = new ControlSequenceHandler();
|
|
this.executorService = Executors.newFixedThreadPool(2);
|
|
this.terminalSize = terminal.getTerminalSize();
|
|
this.inputLine = "";
|
|
this.outputSegment = "";
|
|
this.isStopped = false;
|
|
|
|
setOriginToCurrentPosition();
|
|
terminal.addResizeListener((t, newSize) -> resize());
|
|
}
|
|
|
|
private synchronized void setOriginToCurrentPosition() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
originColumn = cursorPosition.getColumn();
|
|
originRow = cursorPosition.getRow();
|
|
}
|
|
|
|
private synchronized void resize() {
|
|
terminalSize = terminal.getTerminalSize();
|
|
terminal.clearScreen();
|
|
terminal.setCursorPosition(0, 0);
|
|
redisplayInput();
|
|
}
|
|
|
|
private synchronized void redisplayInput() {
|
|
setOriginToCurrentPosition();
|
|
putStringToTerminal(inputLine);
|
|
moveCursorToEndOfInput();
|
|
}
|
|
|
|
private synchronized void putStringToTerminal(String characters) {
|
|
for (char c : characters.toCharArray())
|
|
terminal.putCharacter(c);
|
|
}
|
|
|
|
private synchronized void moveCursorToEndOfInput() {
|
|
terminal.setCursorPosition(getLeadingEdge());
|
|
}
|
|
|
|
private synchronized TerminalPosition getLeadingEdge() {
|
|
int inputLength = inputLine.length();
|
|
int totalColumns = terminalSize.getColumns();
|
|
int rowDifference = inputLength / totalColumns;
|
|
int columnDifference = inputLength % totalColumns;
|
|
|
|
return new TerminalPosition(originColumn + columnDifference, originRow + rowDifference);
|
|
}
|
|
|
|
public void start() {
|
|
executorService.execute(this::readInput);
|
|
executorService.execute(this::writeOutput);
|
|
executorService.shutdown();
|
|
}
|
|
|
|
private void readInput() {
|
|
while (!isStopped)
|
|
processNextKey();
|
|
}
|
|
|
|
private void processNextKey() {
|
|
KeyStroke keyStroke = getKeyStroke();
|
|
|
|
if (keyStroke != null)
|
|
handleKey(keyStroke);
|
|
else
|
|
takeNap();
|
|
}
|
|
|
|
private KeyStroke getKeyStroke() {
|
|
KeyStroke keyStroke = null;
|
|
|
|
// issue #299
|
|
try {
|
|
keyStroke = terminal.pollInput();
|
|
} catch (IllegalStateException e) {
|
|
doControlC();
|
|
}
|
|
|
|
return keyStroke;
|
|
}
|
|
|
|
private synchronized void handleKey(KeyStroke keyStroke) {
|
|
doKey(keyStroke);
|
|
terminal.flush();
|
|
}
|
|
|
|
private synchronized void doKey(KeyStroke keyStroke) {
|
|
if (keyStroke.isCtrlDown())
|
|
doControlKey(keyStroke);
|
|
else
|
|
doNormalKey(keyStroke);
|
|
}
|
|
|
|
private synchronized void doControlKey(KeyStroke keyStroke) {
|
|
KeyType keyType = keyStroke.getKeyType();
|
|
|
|
if (keyType == Character)
|
|
doControlCharacter(keyStroke);
|
|
}
|
|
|
|
private synchronized void doControlCharacter(KeyStroke keyStroke) {
|
|
if (keyStroke.getCharacter() == 'c')
|
|
doControlC();
|
|
else if (keyStroke.getCharacter() == 'd')
|
|
doControlD();
|
|
}
|
|
|
|
private void doControlC() {
|
|
moveCursorToEndOfInput();
|
|
terminal.putCharacter('\n');
|
|
inputLine = "";
|
|
setOriginToCurrentPosition();
|
|
stop();
|
|
}
|
|
|
|
private synchronized void doControlD() {
|
|
doEnter();
|
|
stop();
|
|
}
|
|
|
|
private synchronized void doNormalKey(KeyStroke keyStroke) {
|
|
KeyType keyType = keyStroke.getKeyType();
|
|
|
|
if (keyType == Enter)
|
|
doEnter();
|
|
else if (keyType == ArrowLeft)
|
|
doLeftArrow();
|
|
else if (keyType == ArrowRight)
|
|
doRightArrow();
|
|
else if (keyType == Backspace)
|
|
doBackspace();
|
|
else if (keyType == Delete)
|
|
doDelete();
|
|
else if (keyType == Character)
|
|
doCharacter(keyStroke.getCharacter());
|
|
}
|
|
|
|
private synchronized void doEnter() {
|
|
moveCursorToEndOfInput();
|
|
terminal.putCharacter('\n');
|
|
inputLine += "\n";
|
|
inputWriter.write(inputLine.getBytes());
|
|
inputWriter.flush();
|
|
inputLine = "";
|
|
setOriginToCurrentPosition();
|
|
}
|
|
|
|
private synchronized void doLeftArrow() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (isPossibleToMoveLeft(cursorPosition))
|
|
moveCursorLeft(cursorPosition);
|
|
}
|
|
|
|
private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) {
|
|
return getDistanceFromOrigin(cursorPosition) > 0;
|
|
}
|
|
|
|
private synchronized int getDistanceFromOrigin(TerminalPosition cursorPosition) {
|
|
int columnDifference = cursorPosition.getColumn() - originColumn;
|
|
int rowDifference = cursorPosition.getRow() - originRow;
|
|
int totalColumns = terminalSize.getColumns();
|
|
|
|
return columnDifference + (totalColumns * rowDifference);
|
|
}
|
|
|
|
private synchronized void moveCursorLeft(TerminalPosition cursorPosition) {
|
|
TerminalPosition newPosition = cursorPosition.withRelativeColumn(-1);
|
|
|
|
if (isAtStartOfRow(cursorPosition))
|
|
newPosition = cursorPosition.withColumn(terminalSize.getColumns()).withRelativeRow(-1);
|
|
|
|
terminal.setCursorPosition(newPosition);
|
|
}
|
|
|
|
private synchronized boolean isAtStartOfRow(TerminalPosition cursorPosition) {
|
|
return cursorPosition.getColumn() == 0;
|
|
}
|
|
|
|
private synchronized void doRightArrow() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (isPossibleToMoveRight(cursorPosition))
|
|
moveCursorRight(cursorPosition);
|
|
}
|
|
|
|
private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
|
|
return getDistanceFromOrigin(cursorPosition) < inputLine.length();
|
|
}
|
|
|
|
private synchronized void moveCursorRight(TerminalPosition cursorPosition) {
|
|
if (isEndOfRow(cursorPosition))
|
|
moveCursorToNextRow(cursorPosition);
|
|
else
|
|
terminal.setCursorPosition(cursorPosition.withRelativeColumn(1));
|
|
}
|
|
|
|
private synchronized boolean isEndOfRow(TerminalPosition cursorPosition) {
|
|
return cursorPosition.getColumn() >= terminalSize.getColumns() - 1;
|
|
}
|
|
|
|
private synchronized void moveCursorToNextRow(TerminalPosition cursorPosition) {
|
|
if (isEndOfBuffer(cursorPosition))
|
|
createNewRowForCursor(cursorPosition);
|
|
else
|
|
terminal.setCursorPosition(cursorPosition.withColumn(0).withRelativeRow(1));
|
|
}
|
|
|
|
private synchronized boolean isEndOfBuffer(TerminalPosition cursorPosition) {
|
|
return cursorPosition.getRow() == terminalSize.getRows() - 1;
|
|
}
|
|
|
|
private synchronized void createNewRowForCursor(TerminalPosition cursorPosition) {
|
|
terminal.setCursorPosition(cursorPosition);
|
|
terminal.putCharacter('\n');
|
|
terminal.setCursorPosition(cursorPosition.withColumn(0));
|
|
--originRow;
|
|
}
|
|
|
|
private synchronized void doBackspace() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (isPossibleToMoveLeft(cursorPosition))
|
|
deletePreviousCharacter(cursorPosition);
|
|
}
|
|
|
|
private synchronized void deletePreviousCharacter(TerminalPosition cursorPosition) {
|
|
int distanceFromOrigin = getDistanceFromOrigin(cursorPosition);
|
|
String remaining = inputLine.substring(distanceFromOrigin, inputLine.length());
|
|
inputLine = inputLine.substring(0, distanceFromOrigin - 1) + remaining;
|
|
moveCursorLeft(cursorPosition);
|
|
putStringToTerminal(remaining + " ");
|
|
moveCursorLeft(cursorPosition);
|
|
}
|
|
|
|
private synchronized void doDelete() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (isPossibleToMoveRight(cursorPosition))
|
|
deleteCharacterAtPosition(cursorPosition);
|
|
}
|
|
|
|
private synchronized void deleteCharacterAtPosition(TerminalPosition cursorPosition) {
|
|
int distanceFromOrigin = getDistanceFromOrigin(cursorPosition);
|
|
String remaining = inputLine.substring(distanceFromOrigin + 1, inputLine.length());
|
|
inputLine = inputLine.substring(0, distanceFromOrigin) + remaining;
|
|
putStringToTerminal(remaining + " ");
|
|
terminal.setCursorPosition(cursorPosition);
|
|
}
|
|
|
|
private synchronized void doCharacter(Character character) {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (!isBufferFilled())
|
|
if (isPossibleToMoveRight(cursorPosition))
|
|
insertCharacter(character, cursorPosition);
|
|
else
|
|
appendCharacter(character, cursorPosition);
|
|
}
|
|
|
|
private synchronized boolean isBufferFilled() {
|
|
int row = getLeadingEdge().getRow();
|
|
int column = getLeadingEdge().getColumn();
|
|
|
|
return (row == terminalSize.getRows() - 1) && (column >= terminalSize.getColumns() - 1) && (originRow <= 0);
|
|
}
|
|
|
|
private synchronized void insertCharacter(Character character, TerminalPosition cursorPosition) {
|
|
cursorPosition = shiftPositionIfNewRowAdded(cursorPosition);
|
|
terminal.setCursorPosition(cursorPosition);
|
|
int distanceFromOrigin = getDistanceFromOrigin(cursorPosition);
|
|
String remaining = character + inputLine.substring(distanceFromOrigin, inputLine.length());
|
|
inputLine = inputLine.substring(0, distanceFromOrigin) + remaining;
|
|
putStringToTerminal(remaining);
|
|
moveCursorRight(cursorPosition);
|
|
}
|
|
|
|
private synchronized TerminalPosition shiftPositionIfNewRowAdded(TerminalPosition cursorPosition) {
|
|
int oldOriginRow = originRow;
|
|
moveCursorRight(getLeadingEdge());
|
|
|
|
return isNewRowAdded(oldOriginRow) ? adjustCursorPosition(cursorPosition) : cursorPosition;
|
|
}
|
|
|
|
private synchronized boolean isNewRowAdded(int oldOriginRow) {
|
|
return originRow != oldOriginRow;
|
|
}
|
|
|
|
private synchronized TerminalPosition adjustCursorPosition(TerminalPosition cursorPosition) {
|
|
terminal.setCursorPosition(new TerminalPosition(originColumn, originRow));
|
|
putStringToTerminal(inputLine);
|
|
|
|
return cursorPosition.withRelativeRow(-1);
|
|
}
|
|
|
|
private synchronized void appendCharacter(Character character, TerminalPosition cursorPosition) {
|
|
terminal.putCharacter(character);
|
|
inputLine += character;
|
|
moveCursorRight(cursorPosition);
|
|
}
|
|
|
|
private void takeNap() {
|
|
try {
|
|
Thread.sleep(1);
|
|
} catch (InterruptedException ignored) {}
|
|
}
|
|
|
|
private void writeOutput() {
|
|
for (int c = outputReader.read(); c != EOF; c = outputReader.read())
|
|
processOutput((char) c);
|
|
|
|
terminal.setCursorVisible(true);
|
|
terminal.flush();
|
|
terminal.close();
|
|
}
|
|
|
|
private synchronized void processOutput(char c) {
|
|
if (isEndOfSegment(c))
|
|
writeSegment();
|
|
else
|
|
outputSegment += c;
|
|
}
|
|
|
|
private synchronized boolean isEndOfSegment(char c) {
|
|
return c == END_OF_SEGMENT;
|
|
}
|
|
|
|
private synchronized void writeSegment() {
|
|
terminal.setCursorVisible(false);
|
|
printSegmentCharacters();
|
|
terminal.setCursorVisible(true);
|
|
outputSegment = "";
|
|
redisplayInput();
|
|
terminal.flush();
|
|
}
|
|
|
|
private synchronized void printSegmentCharacters() {
|
|
moveCursorToEndOfInput();
|
|
putOutputToTerminal();
|
|
moveCursorToNextRowIfNecessary();
|
|
}
|
|
|
|
private synchronized void putOutputToTerminal() {
|
|
SafeInputStream input = convertOutputToStream();
|
|
|
|
for (int c = input.read(); c != EOF; c = input.read())
|
|
if (isEscape((char) c))
|
|
applyControlSequence(input);
|
|
else
|
|
terminal.putCharacter((char) c);
|
|
}
|
|
|
|
private synchronized SafeInputStream convertOutputToStream() {
|
|
return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes()));
|
|
}
|
|
|
|
private void applyControlSequence(SafeInputStream input) {
|
|
ControlSequence controlSequence = controlSequenceHandler.parse(input);
|
|
controlSequence.applyTo(terminal);
|
|
}
|
|
|
|
private synchronized void moveCursorToNextRowIfNecessary() {
|
|
TerminalPosition cursorPosition = terminal.getCursorPosition();
|
|
|
|
if (isEndOfRow(cursorPosition))
|
|
moveCursorToNextRow(cursorPosition);
|
|
}
|
|
|
|
public void stop() {
|
|
isStopped = true;
|
|
inputWriter.close();
|
|
}
|
|
|
|
}
|