From 31ca72e534305961e8fde8336dc347b29b13a81b Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 18 Mar 2017 16:49:46 -0400 Subject: [PATCH] Add a crude interactive terminal implementation --- .../InteractiveLispInterpreter.java | 12 +- src/interpreter/LispInterpreter.java | 7 +- src/main/LispMain.java | 109 +++++++-- src/terminal/LispTerminal.java | 133 +++++++++-- test/terminal/LispTerminalAttempt.java | 225 ++++++++++++++++++ 5 files changed, 434 insertions(+), 52 deletions(-) create mode 100644 test/terminal/LispTerminalAttempt.java diff --git a/src/interpreter/InteractiveLispInterpreter.java b/src/interpreter/InteractiveLispInterpreter.java index edcc2a2..47f6050 100644 --- a/src/interpreter/InteractiveLispInterpreter.java +++ b/src/interpreter/InteractiveLispInterpreter.java @@ -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(); } } diff --git a/src/interpreter/LispInterpreter.java b/src/interpreter/LispInterpreter.java index 40231eb..e0e1033 100644 --- a/src/interpreter/LispInterpreter.java +++ b/src/interpreter/LispInterpreter.java @@ -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(); } diff --git a/src/main/LispMain.java b/src/main/LispMain.java index e1abdd7..d0031fe 100644 --- a/src/main/LispMain.java +++ b/src/main/LispMain.java @@ -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 makeColorDecorator(String color) { @@ -56,4 +115,14 @@ public class LispMain { }; } + private static Function makeInteractiveDecorator(String color) { + return new Function() { + + @Override + public String apply(String s) { + return s + 'x'; + } + }; + } + } diff --git a/src/terminal/LispTerminal.java b/src/terminal/LispTerminal.java index 97709c8..3374873 100644 --- a/src/terminal/LispTerminal.java +++ b/src/terminal/LispTerminal.java @@ -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(); + } + } + } diff --git a/test/terminal/LispTerminalAttempt.java b/test/terminal/LispTerminalAttempt.java new file mode 100644 index 0000000..0ea6366 --- /dev/null +++ b/test/terminal/LispTerminalAttempt.java @@ -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); + } + +}