Add a crude interactive terminal implementation

This commit is contained in:
Mike Cifelli 2017-03-18 16:49:46 -04:00
parent 462673ba64
commit 31ca72e534
5 changed files with 434 additions and 52 deletions

View File

@ -9,24 +9,26 @@ public class InteractiveLispInterpreter extends LispInterpreter {
protected void printGreeting() {
environment.getOutput().println(GREETING);
environment.getOutput().println();
environment.getOutput().flush();
}
@Override
protected void displayPrompt() {
environment.getOutput().print(PROMPT);
environment.getOutput().print(environment.decorateOutput(PROMPT));
environment.getOutput().flush();
}
@Override
protected void erasePrompt() {
for (int i = 0; i < PROMPT.length(); i++) {
environment.getOutput().print("\b");
}
protected void printValueOfNextSExpression() {
environment.getOutput().println();
super.printValueOfNextSExpression();
}
@Override
protected void printFarewell() {
environment.getOutput().println();
environment.getOutput().println();
environment.getOutput().close();
}
}

View File

@ -34,11 +34,10 @@ public abstract class LispInterpreter {
protected void displayPrompt() {}
private void printValueOfNextSExpression() {
protected void printValueOfNextSExpression() {
try {
printValueOfNextSExpressionWithException();
} catch (LispException e) {
erasePrompt();
environment.getErrorManager().handle(e);
}
}
@ -47,12 +46,10 @@ public abstract class LispInterpreter {
SExpression sExpression = parser.getNextSExpression();
String result = environment.decorateValueOutput(String.valueOf(eval(sExpression)));
erasePrompt();
environment.getOutput().println(result);
environment.getOutput().flush();
}
protected void erasePrompt() {}
protected void printFarewell() {
environment.getOutput().println();
}

View File

@ -1,5 +1,6 @@
package main;
import java.io.*;
import java.util.function.Function;
import interpreter.*;
@ -12,38 +13,96 @@ public class LispMain {
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_PURPLE = "\u001B[35m";
private PipedInputStream inputReader;
private PipedOutputStream inputWriter;
private PipedInputStream outputReader;
private PipedOutputStream outputWriter;
private LispTerminal terminal;
private LispMain() {}
public static void main(String[] args) {
LispTerminal terminal = new LispTerminal();
terminal.run();
// LispInterpreter interpreter = buildInterpreter(args);
// interpreter.interpret();
private LispMain() throws IOException {
inputReader = new PipedInputStream();
inputWriter = new PipedOutputStream(inputReader);
outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
terminal = new LispTerminal(inputWriter, outputReader);
}
private static LispInterpreter buildInterpreter(String[] args) {
public static void main(String[] args) throws IOException {
LispMain main = new LispMain();
main.run(args);
}
private void run(String[] args) {
if (args.length == 0)
terminal.run();
LispInterpreter interpreter = buildInterpreter(args);
interpreter.interpret();
}
private LispInterpreter buildInterpreter(String[] args) {
LispInterpreterBuilder builder = LispInterpreterBuilderImpl.getInstance();
configureInput(args, builder);
builder.setOutput(System.out);
builder.setErrorOutput(System.err);
builder.setTerminationFunction(() -> System.exit(0));
builder.setErrorTerminationFunction(() -> System.exit(1));
builder.setValueOutputDecorator(makeColorDecorator(ANSI_GREEN));
builder.setWarningOutputDecorator(makeColorDecorator(ANSI_YELLOW));
builder.setErrorOutputDecorator(makeColorDecorator(ANSI_RED));
builder.setCriticalOutputDecorator(makeColorDecorator(ANSI_PURPLE));
configureOutput(args, builder);
configureTerminatingFunctions(args, builder);
configureDecorators(args, builder);
return builder.build();
}
private static void configureInput(String[] args, LispInterpreterBuilder builder) {
private void configureTerminatingFunctions(String[] args, LispInterpreterBuilder builder) {
if (args.length > 0) {
builder.setTerminationFunction(() -> System.exit(0));
builder.setErrorTerminationFunction(() -> System.exit(1));
} else {
builder.setTerminationFunction(this::shutdown);
builder.setErrorTerminationFunction(this::shutdown);
}
}
private void configureDecorators(String[] args, LispInterpreterBuilder builder) {
if (args.length > 0) {
builder.setOutputDecorator(makeColorDecorator(ANSI_GREEN));
builder.setValueOutputDecorator(makeColorDecorator(ANSI_GREEN));
builder.setWarningOutputDecorator(makeColorDecorator(ANSI_YELLOW));
builder.setErrorOutputDecorator(makeColorDecorator(ANSI_RED));
builder.setCriticalOutputDecorator(makeColorDecorator(ANSI_PURPLE));
} else {
builder.setOutputDecorator(makeInteractiveDecorator(ANSI_GREEN));
builder.setValueOutputDecorator(makeInteractiveDecorator(ANSI_GREEN));
builder.setWarningOutputDecorator(makeInteractiveDecorator(ANSI_YELLOW));
builder.setErrorOutputDecorator(makeInteractiveDecorator(ANSI_RED));
builder.setCriticalOutputDecorator(makeInteractiveDecorator(ANSI_PURPLE));
}
}
private void shutdown() {
try {
terminal.finish();
outputWriter.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void configureOutput(String[] args, LispInterpreterBuilder builder) {
if (args.length > 0) {
builder.setOutput(System.out);
builder.setErrorOutput(System.err);
} else {
PrintStream outputStream = new PrintStream(outputWriter);
builder.setOutput(outputStream);
builder.setErrorOutput(outputStream);
}
}
private void configureInput(String[] args, LispInterpreterBuilder builder) {
if (args.length > 0)
builder.useFile(args[0]);
else
builder.setInput(System.in, "stdin");
builder.setInput(inputReader, "stdin");
}
private static Function<String, String> makeColorDecorator(String color) {
@ -56,4 +115,14 @@ public class LispMain {
};
}
private static Function<String, String> makeInteractiveDecorator(String color) {
return new Function<String, String>() {
@Override
public String apply(String s) {
return s + 'x';
}
};
}
}

View File

@ -1,19 +1,34 @@
package terminal;
import java.io.IOException;
import java.io.*;
import java.util.concurrent.*;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.terminal.*;
public class LispTerminal implements Runnable {
public class LispTerminal {
// private static final String ANSI_RESET = "001B[0m";
// private static final String ANSI_RED = "\u001B[31m";
// private static final String ANSI_GREEN = "\u001B[32m";
// private static final String ANSI_YELLOW = "\u001B[33m";
// private static final String ANSI_PURPLE = "\u001B[35m";
private IOSafeTerminal terminal;
private boolean isFinished;
private String currentLine;
private TerminalPosition origin;
private PipedOutputStream inputWriter;
private PipedInputStream outputReader;
private ExecutorService executor;
public LispTerminal() {
LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.terminal = terminal;
initialize(inputWriter, outputReader);
}
public LispTerminal(PipedOutputStream inputWriter, PipedInputStream outputReader) {
try {
Terminal unsafe = new DefaultTerminalFactory().createTerminal();
this.terminal = IOSafeTerminalAdapter.createRuntimeExceptionConvertingAdapter(unsafe);
@ -22,15 +37,52 @@ public class LispTerminal implements Runnable {
e.printStackTrace();
}
initialize(inputWriter, outputReader);
}
private void initialize(PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.isFinished = false;
this.currentLine = "";
this.origin = terminal.getCursorPosition();
this.inputWriter = inputWriter;
this.outputReader = outputReader;
this.executor = Executors.newFixedThreadPool(2);
}
public void run() {
executor.execute(this::readInput);
executor.execute(this::writeOutput);
executor.shutdown();
}
public void readInput() {
while (!isFinished) {
handleKeyStroke(getKeyStroke());
terminal.flush();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
isFinished = true;
}
}
}
public void writeOutput() {
try {
for (int c = outputReader.read(); c != -1; c = outputReader.read()) {
synchronized (this) {
if (c == 'x') {
terminal.flush();
origin = terminal.getCursorPosition();
} else {
terminal.putCharacter((char) c);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
terminal.close();
@ -40,23 +92,24 @@ public class LispTerminal implements Runnable {
KeyStroke keyStroke = null;
try {
keyStroke = terminal.readInput();
keyStroke = terminal.pollInput();
} catch (IllegalStateException e) {
// TODO - lantera is trying to exit private mode on ctrl-c (which we didn't enter)
terminal.putCharacter('`');
isFinished = true;
terminal.putCharacter('\n');
System.exit(0);
}
return keyStroke;
}
private void handleKeyStroke(KeyStroke keyStroke) {
private synchronized void handleKeyStroke(KeyStroke keyStroke) {
if (keyStroke == null)
return;
KeyType keyType = keyStroke.getKeyType();
if (keyType == KeyType.ArrowLeft)
if (keyStroke.isCtrlDown())
doControlKeyCharacter(keyStroke);
else if (keyType == KeyType.ArrowLeft)
moveCursorLeft();
else if (keyType == KeyType.ArrowRight)
moveCursorRight();
@ -70,13 +123,33 @@ public class LispTerminal implements Runnable {
doCharacter(keyStroke);
}
private void doEnter() {
private synchronized void doControlKeyCharacter(KeyStroke keyStroke) {
KeyType keyType = keyStroke.getKeyType();
if (keyType == KeyType.Character)
if (keyStroke.getCharacter() == 'd') {
doEnter();
finish();
}
}
private synchronized void doEnter() {
terminal.putCharacter('\n');
currentLine += "\n";
try {
inputWriter.write(currentLine.getBytes());
inputWriter.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
currentLine = "";
origin = terminal.getCursorPosition();
}
private void moveCursorLeft() {
private synchronized void moveCursorLeft() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition))
@ -86,38 +159,43 @@ public class LispTerminal implements Runnable {
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
}
private boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) {
private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) > 0;
}
private int distanceFromOrigin(TerminalPosition cursorPosition) {
return cursorPosition.getColumn()
+ (terminal.getTerminalSize().getColumns() * (cursorPosition.getRow() - origin.getRow()));
private synchronized int distanceFromOrigin(TerminalPosition cursorPosition) {
int cursorColumn = cursorPosition.getColumn();
int cursorRow = cursorPosition.getRow();
int originColumn = origin.getColumn();
int originRow = origin.getRow();
int totalColumns = terminal.getTerminalSize().getColumns();
return cursorColumn - originColumn + (totalColumns * (cursorRow - originRow));
}
private void moveCursorRight() {
private synchronized void moveCursorRight() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition))
advanceCursor(cursorPosition);
}
private void advanceCursor(TerminalPosition cursorPosition) {
private synchronized void advanceCursor(TerminalPosition cursorPosition) {
if (isCursorAtEndOfRow(cursorPosition))
terminal.setCursorPosition(0, cursorPosition.getRow() + 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(1));
}
private boolean isCursorAtEndOfRow(TerminalPosition cursorPosition) {
private synchronized boolean isCursorAtEndOfRow(TerminalPosition cursorPosition) {
return cursorPosition.getColumn() == terminal.getTerminalSize().getColumns() - 1;
}
private boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) < currentLine.length();
}
private void doBackspace() {
private synchronized void doBackspace() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition)) {
@ -141,7 +219,7 @@ public class LispTerminal implements Runnable {
}
}
private void doDelete() {
private synchronized void doDelete() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) {
@ -156,7 +234,7 @@ public class LispTerminal implements Runnable {
}
}
private void doCharacter(KeyStroke keyStroke) {
private synchronized void doCharacter(KeyStroke keyStroke) {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) {
@ -176,4 +254,15 @@ public class LispTerminal implements Runnable {
}
}
public void finish() {
isFinished = true;
try {
inputWriter.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,225 @@
package terminal;
import static org.junit.Assert.assertTrue;
import java.io.*;
import org.junit.*;
import com.googlecode.lanterna.*;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.terminal.virtual.*;
public class LispTerminalAttempt {
private PipedInputStream inputReader;
private PipedOutputStream inputWriter;
private PipedInputStream outputReader;
private PipedOutputStream outputWriter;
private VirtualTerminal virtualTerminal;
private LispTerminal lispTerminal;
private void pressKey(KeyType keyType) {
virtualTerminal.addInput(new KeyStroke(keyType));
sleep();
}
private void enterCharacter(char character) {
virtualTerminal.addInput(new KeyStroke(character, false, false));
sleep();
}
private void enterCharacters(String characters) {
for (char c : characters.toCharArray())
virtualTerminal.addInput(new KeyStroke(c, false, false));
sleep();
}
private void sleep() {
try {
Thread.sleep(15);
} catch (InterruptedException e) {}
}
private void assertCursorPosition(int column, int row) {
assertTrue(virtualTerminal.getCursorBufferPosition().getColumn() == column);
assertTrue(virtualTerminal.getCursorBufferPosition().getRow() == row);
}
private void assertCharacterAtPosition(char character, int column, int row) {
TerminalPosition position = new TerminalPosition(column, row);
assertTrue(virtualTerminal.getBufferCharacter(position).getCharacter() == character);
}
private void setColumns(int columns) {
virtualTerminal.setTerminalSize(new TerminalSize(columns, virtualTerminal.getTerminalSize().getRows()));
}
@Before
public void setUp() throws IOException {
inputReader = new PipedInputStream();
inputWriter = new PipedOutputStream(inputReader);
outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
virtualTerminal = new DefaultVirtualTerminal();
lispTerminal = new LispTerminal(virtualTerminal, inputWriter, outputReader);
lispTerminal.run();
}
@After
public void tearDown() throws IOException {
lispTerminal.finish();
outputWriter.close();
}
@Test
public void leftArrowDoesNotMovePastOrigin() {
pressKey(KeyType.ArrowLeft);
assertCursorPosition(0, 0);
}
@Test
public void leftArrowWorksAfterEnteringCharacters() {
enterCharacters("abc");
assertCursorPosition(3, 0);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(2, 0);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(1, 0);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(0, 0);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(0, 0);
}
@Test
public void leftArrowWorksAcrossRows() {
setColumns(5);
enterCharacters("123451");
assertCursorPosition(1, 1);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(4, 0);
}
@Test
public void rightArrowDoesNotMovePastEndOfInput() {
pressKey(KeyType.ArrowRight);
assertCursorPosition(0, 0);
}
@Test
public void rightArrowWorksAfterMovingLeft() {
enterCharacters("12");
assertCursorPosition(2, 0);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(1, 0);
pressKey(KeyType.ArrowRight);
assertCursorPosition(2, 0);
pressKey(KeyType.ArrowRight);
assertCursorPosition(2, 0);
}
@Test
public void rightArrowWorksAcrossRow() {
setColumns(5);
enterCharacters("123451");
assertCursorPosition(1, 1);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
assertCursorPosition(3, 0);
pressKey(KeyType.ArrowRight);
pressKey(KeyType.ArrowRight);
pressKey(KeyType.ArrowRight);
assertCursorPosition(1, 1);
}
@Test
public void characterKeyIsEchoed() {
enterCharacter('a');
assertCursorPosition(1, 0);
assertCharacterAtPosition('a', 0, 0);
}
@Test
public void characterIsInserted() {
enterCharacters("abcd");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
enterCharacter('x');
assertCharacterAtPosition('a', 0, 0);
assertCharacterAtPosition('b', 1, 0);
assertCharacterAtPosition('x', 2, 0);
assertCharacterAtPosition('c', 3, 0);
assertCharacterAtPosition('d', 4, 0);
}
@Test
public void characterIsInserted_PushesInputToNextRow() {
setColumns(4);
enterCharacters("abcd");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
enterCharacter('x');
assertCharacterAtPosition('a', 0, 0);
assertCharacterAtPosition('b', 1, 0);
assertCharacterAtPosition('x', 2, 0);
assertCharacterAtPosition('c', 3, 0);
assertCharacterAtPosition('d', 0, 1);
}
@Test
public void backspaceDoesNothingAtOrigin() {
pressKey(KeyType.Backspace);
assertCursorPosition(0, 0);
}
@Test
public void backspaceWorksAfterInput() {
enterCharacters("12345");
pressKey(KeyType.Backspace);
pressKey(KeyType.Backspace);
assertCursorPosition(3, 0);
assertCharacterAtPosition('1', 0, 0);
assertCharacterAtPosition('2', 1, 0);
assertCharacterAtPosition('3', 2, 0);
assertCharacterAtPosition(' ', 3, 0);
assertCharacterAtPosition(' ', 4, 0);
assertCharacterAtPosition(' ', 5, 0);
}
@Test
public void backspaceWorksAcrossRow() {
setColumns(4);
enterCharacters("1234567");
pressKey(KeyType.Backspace);
pressKey(KeyType.Backspace);
pressKey(KeyType.Backspace);
pressKey(KeyType.Backspace);
pressKey(KeyType.Backspace);
assertCursorPosition(2, 0);
assertCharacterAtPosition('1', 0, 0);
assertCharacterAtPosition('2', 1, 0);
assertCharacterAtPosition(' ', 2, 0);
assertCharacterAtPosition(' ', 3, 0);
assertCharacterAtPosition(' ', 0, 1);
assertCharacterAtPosition(' ', 1, 1);
assertCharacterAtPosition(' ', 2, 1);
}
@Test
public void backspaceWorksInMiddleOfInput() {
enterCharacters("12345");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.Backspace);
assertCursorPosition(2, 0);
assertCharacterAtPosition('1', 0, 0);
assertCharacterAtPosition('2', 1, 0);
assertCharacterAtPosition('4', 2, 0);
assertCharacterAtPosition('5', 3, 0);
}
}