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() { protected void printGreeting() {
environment.getOutput().println(GREETING); environment.getOutput().println(GREETING);
environment.getOutput().println(); environment.getOutput().println();
environment.getOutput().flush();
} }
@Override @Override
protected void displayPrompt() { protected void displayPrompt() {
environment.getOutput().print(PROMPT); environment.getOutput().print(environment.decorateOutput(PROMPT));
environment.getOutput().flush();
} }
@Override @Override
protected void erasePrompt() { protected void printValueOfNextSExpression() {
for (int i = 0; i < PROMPT.length(); i++) { environment.getOutput().println();
environment.getOutput().print("\b"); super.printValueOfNextSExpression();
}
} }
@Override @Override
protected void printFarewell() { protected void printFarewell() {
environment.getOutput().println(); environment.getOutput().println();
environment.getOutput().println(); environment.getOutput().println();
environment.getOutput().close();
} }
} }

View File

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

View File

@ -1,5 +1,6 @@
package main; package main;
import java.io.*;
import java.util.function.Function; import java.util.function.Function;
import interpreter.*; import interpreter.*;
@ -12,38 +13,96 @@ public class LispMain {
private static final String ANSI_GREEN = "\u001B[32m"; private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_YELLOW = "\u001B[33m"; private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_PURPLE = "\u001B[35m"; 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() {} private LispMain() throws IOException {
inputReader = new PipedInputStream();
public static void main(String[] args) { inputWriter = new PipedOutputStream(inputReader);
LispTerminal terminal = new LispTerminal(); outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
terminal.run(); terminal = new LispTerminal(inputWriter, outputReader);
// LispInterpreter interpreter = buildInterpreter(args);
// interpreter.interpret();
} }
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(); LispInterpreterBuilder builder = LispInterpreterBuilderImpl.getInstance();
configureInput(args, builder); configureInput(args, builder);
builder.setOutput(System.out); configureOutput(args, builder);
builder.setErrorOutput(System.err); configureTerminatingFunctions(args, builder);
builder.setTerminationFunction(() -> System.exit(0)); configureDecorators(args, builder);
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));
return builder.build(); 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) if (args.length > 0)
builder.useFile(args[0]); builder.useFile(args[0]);
else else
builder.setInput(System.in, "stdin"); builder.setInput(inputReader, "stdin");
} }
private static Function<String, String> makeColorDecorator(String color) { 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; package terminal;
import java.io.IOException; import java.io.*;
import java.util.concurrent.*;
import com.googlecode.lanterna.TerminalPosition; import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.*; import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.terminal.*; 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 IOSafeTerminal terminal;
private boolean isFinished; private boolean isFinished;
private String currentLine; private String currentLine;
private TerminalPosition origin; 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 { try {
Terminal unsafe = new DefaultTerminalFactory().createTerminal(); Terminal unsafe = new DefaultTerminalFactory().createTerminal();
this.terminal = IOSafeTerminalAdapter.createRuntimeExceptionConvertingAdapter(unsafe); this.terminal = IOSafeTerminalAdapter.createRuntimeExceptionConvertingAdapter(unsafe);
@ -22,15 +37,52 @@ public class LispTerminal implements Runnable {
e.printStackTrace(); e.printStackTrace();
} }
initialize(inputWriter, outputReader);
}
private void initialize(PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.isFinished = false; this.isFinished = false;
this.currentLine = ""; this.currentLine = "";
this.origin = terminal.getCursorPosition(); this.origin = terminal.getCursorPosition();
this.inputWriter = inputWriter;
this.outputReader = outputReader;
this.executor = Executors.newFixedThreadPool(2);
} }
public void run() { public void run() {
executor.execute(this::readInput);
executor.execute(this::writeOutput);
executor.shutdown();
}
public void readInput() {
while (!isFinished) { while (!isFinished) {
handleKeyStroke(getKeyStroke()); handleKeyStroke(getKeyStroke());
terminal.flush(); 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(); terminal.close();
@ -40,23 +92,24 @@ public class LispTerminal implements Runnable {
KeyStroke keyStroke = null; KeyStroke keyStroke = null;
try { try {
keyStroke = terminal.readInput(); keyStroke = terminal.pollInput();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// TODO - lantera is trying to exit private mode on ctrl-c (which we didn't enter) terminal.putCharacter('\n');
terminal.putCharacter('`'); System.exit(0);
isFinished = true;
} }
return keyStroke; return keyStroke;
} }
private void handleKeyStroke(KeyStroke keyStroke) { private synchronized void handleKeyStroke(KeyStroke keyStroke) {
if (keyStroke == null) if (keyStroke == null)
return; return;
KeyType keyType = keyStroke.getKeyType(); KeyType keyType = keyStroke.getKeyType();
if (keyType == KeyType.ArrowLeft) if (keyStroke.isCtrlDown())
doControlKeyCharacter(keyStroke);
else if (keyType == KeyType.ArrowLeft)
moveCursorLeft(); moveCursorLeft();
else if (keyType == KeyType.ArrowRight) else if (keyType == KeyType.ArrowRight)
moveCursorRight(); moveCursorRight();
@ -70,13 +123,33 @@ public class LispTerminal implements Runnable {
doCharacter(keyStroke); 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'); terminal.putCharacter('\n');
currentLine += "\n";
try {
inputWriter.write(currentLine.getBytes());
inputWriter.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
currentLine = ""; currentLine = "";
origin = terminal.getCursorPosition(); origin = terminal.getCursorPosition();
} }
private void moveCursorLeft() { private synchronized void moveCursorLeft() {
TerminalPosition cursorPosition = terminal.getCursorPosition(); TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition)) if (isPossibleToMoveLeft(cursorPosition))
@ -86,38 +159,43 @@ public class LispTerminal implements Runnable {
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1)); terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
} }
private boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) { private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) > 0; return distanceFromOrigin(cursorPosition) > 0;
} }
private int distanceFromOrigin(TerminalPosition cursorPosition) { private synchronized int distanceFromOrigin(TerminalPosition cursorPosition) {
return cursorPosition.getColumn() int cursorColumn = cursorPosition.getColumn();
+ (terminal.getTerminalSize().getColumns() * (cursorPosition.getRow() - origin.getRow())); 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(); TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) if (isPossibleToMoveRight(cursorPosition))
advanceCursor(cursorPosition); advanceCursor(cursorPosition);
} }
private void advanceCursor(TerminalPosition cursorPosition) { private synchronized void advanceCursor(TerminalPosition cursorPosition) {
if (isCursorAtEndOfRow(cursorPosition)) if (isCursorAtEndOfRow(cursorPosition))
terminal.setCursorPosition(0, cursorPosition.getRow() + 1); terminal.setCursorPosition(0, cursorPosition.getRow() + 1);
else else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(1)); terminal.setCursorPosition(cursorPosition.withRelativeColumn(1));
} }
private boolean isCursorAtEndOfRow(TerminalPosition cursorPosition) { private synchronized boolean isCursorAtEndOfRow(TerminalPosition cursorPosition) {
return cursorPosition.getColumn() == terminal.getTerminalSize().getColumns() - 1; return cursorPosition.getColumn() == terminal.getTerminalSize().getColumns() - 1;
} }
private boolean isPossibleToMoveRight(TerminalPosition cursorPosition) { private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) < currentLine.length(); return distanceFromOrigin(cursorPosition) < currentLine.length();
} }
private void doBackspace() { private synchronized void doBackspace() {
TerminalPosition cursorPosition = terminal.getCursorPosition(); TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition)) { if (isPossibleToMoveLeft(cursorPosition)) {
@ -141,7 +219,7 @@ public class LispTerminal implements Runnable {
} }
} }
private void doDelete() { private synchronized void doDelete() {
TerminalPosition cursorPosition = terminal.getCursorPosition(); TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) { 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(); TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) { 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);
}
}